[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    \"@babel/preset-typescript\",\n    [\n      \"@babel/preset-env\",\n      {\n        \"corejs\": \"3\",\n        \"useBuiltIns\": \"usage\"\n      }\n    ]\n    // [\n    //   \"minify\",\n    //   {\n    //     \"builtIns\": false,\n    //     \"evaluate\": false,\n    //     \"mangle\": false\n    //   }\n    // ]\n  ],\n  \"plugins\": [\n    \"@babel/plugin-syntax-dynamic-import\",\n    \"@babel/plugin-transform-modules-umd\",\n    \"@babel/plugin-transform-runtime\",\n    \"@babel/plugin-transform-class-properties\"\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintrc.base.cjs",
    "content": "const baseRule = {\n  'no-new': 'off',\n  camelcase: 'off',\n  'no-return-assign': 'off',\n  'space-before-function-paren': ['error', 'never'],\n  'no-var': 'error',\n  'no-fallthrough': 'off',\n  eqeqeq: 'off',\n  'require-atomic-updates': ['error', { allowProperties: true }],\n  'no-multiple-empty-lines': [1, { max: 2 }],\n  'comma-dangle': [2, 'always-multiline'],\n  'standard/no-callback-literal': 'off',\n  'prefer-const': 'off',\n  'no-labels': 'off',\n  'node/no-callback-literal': 'off',\n  'multiline-ternary': 'off',\n}\nconst typescriptRule = {\n  ...baseRule,\n  '@typescript-eslint/strict-boolean-expressions': 'off',\n  '@typescript-eslint/explicit-function-return-type': 'off',\n  '@typescript-eslint/space-before-function-paren': 'off',\n  '@typescript-eslint/no-non-null-assertion': 'off',\n  '@typescript-eslint/restrict-template-expressions': [1, {\n    allowBoolean: true,\n    allowAny: true,\n  }],\n  '@typescript-eslint/restrict-plus-operands': [1, {\n    allowBoolean: true,\n    allowAny: true,\n  }],\n  '@typescript-eslint/no-misused-promises': [\n    'error',\n    {\n      checksVoidReturn: {\n        arguments: false,\n        attributes: false,\n      },\n    },\n  ],\n  '@typescript-eslint/naming-convention': 'off',\n  '@typescript-eslint/return-await': 'off',\n  '@typescript-eslint/ban-ts-comment': 'off',\n  '@typescript-eslint/comma-dangle': 'off',\n  '@typescript-eslint/no-unsafe-argument': 'off',\n}\nconst vueRule = {\n  ...typescriptRule,\n  'vue/multi-word-component-names': 'off',\n  'vue/max-attributes-per-line': 'off',\n  'vue/singleline-html-element-content-newline': 'off',\n  'vue/use-v-on-exact': 'off',\n}\n\nexports.base = {\n  extends: ['standard'],\n  rules: baseRule,\n  parser: '@babel/eslint-parser',\n}\n\nexports.html = {\n  files: ['*.html'],\n  plugins: ['html'],\n}\n\nexports.typescript = {\n  files: ['*.ts'],\n  rules: typescriptRule,\n  parser: '@typescript-eslint/parser',\n  extends: [\n    'standard-with-typescript',\n  ],\n}\n\nexports.vue = {\n  files: ['*.vue'],\n  rules: vueRule,\n  parser: 'vue-eslint-parser',\n  extends: [\n    // 'plugin:vue/vue3-essential',\n    'plugin:vue/base',\n    'plugin:vue/vue3-recommended',\n    'plugin:vue-pug/vue3-recommended',\n    // \"plugin:vue/strongly-recommended\"\n    'standard-with-typescript',\n  ],\n  parserOptions: {\n    sourceType: 'module',\n    parser: {\n      // Script parser for `<script>`\n      js: '@typescript-eslint/parser',\n\n      // Script parser for `<script lang=\"ts\">`\n      ts: '@typescript-eslint/parser',\n    },\n    extraFileExtensions: ['.vue'],\n  },\n}\n"
  },
  {
    "path": ".eslintrc.cjs",
    "content": "const { base, typescript } = require('./.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n  ignorePatterns: [\n    'node_modules',\n    '*.min.js',\n    'dist',\n    'build',\n  ],\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: 🐞 报告错误\ndescription: 报告一个错误（Bug），请先查看常见问题及搜索 Issue 列表中有无你要提的问题。\ntitle: \"[Bug]: \"\nbody:\n- type: checkboxes\n  id: check-answer\n  attributes:\n    label: 解决方案检查\n    description: 请确保你已完成以下所有操作。\n    options:\n      - label: 我已阅读 [常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)，并没有找到解决方案。\n        required: true\n      - label: 我已搜索 [Issue 列表](https://github.com/lyswhut/lx-music-desktop/issues?q=is%3Aissue+)，并没有发现类似的问题。\n        required: true\n- type: textarea\n  id: expected-behavior\n  attributes:\n    label: 预期行为\n    description: 对期望发生的事情的清晰简明描述。\n  validations:\n    required: true\n- type: textarea\n  id: actual-behavior\n  attributes:\n    label: 实际行为\n    description: 对实际发生的事情的清晰简明描述。\n  validations:\n    required: true\n- type: input\n  id: version\n  attributes:\n    label: LX Music 版本\n    description: 你使用什么版本的 LX Music？\n    placeholder: 例如 2.9.0\n  validations:\n    required: true\n- type: input\n  id: last-known-working-version\n  attributes:\n    label: 最后正常的版本\n    description: 如果有，请在此处填写最后正常的版本。\n    placeholder: 例如 2.8.0\n- type: input\n  id: operating-system-version\n  attributes:\n    label: 操作系统版本\n    description: |\n      你使用什么版本的操作系统？\n      在 macOS 上，单击「Apple 菜单 > 关于本机」；\n      在 Linux 上，执行 `lsb_release` 或 `uname -a` 命令；\n      在 Windows 上，单击「开始按钮 > 设置 > 系统 > 关于」。\n    placeholder: \"例如 Windows 11 版本 24H2、macOS Sequoia 15.1.1 或 Ubuntu 24.10\"\n  validations:\n    required: true\n- type: textarea\n  id: additional-information\n  attributes:\n    label: 附加信息\n    description: 如果你的问题需要进一步解释，或者你所遇到的问题不容易重现，请在此处添加更多信息。（直接把图片/视频拖到编辑框即可添加图片/视频）\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: ✨ 功能请求\ndescription: 为这个项目提出一个想法，请先查看常见问题及搜索 Issue 列表中有无你要提的问题。\ntitle: \"[Feature]: \"\nbody:\n- type: checkboxes\n  id: check-answer\n  attributes:\n    label: 解决方案检查\n    description: 请确保你已完成以下所有操作。\n    options:\n      - label: 我已阅读 [常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)，但没有找到解决方案。\n        required: true\n      - label: 我已搜索 [Issue 列表](https://github.com/lyswhut/lx-music-desktop/issues?q=is%3Aissue+)，但没有发现类似的问题。\n        required: true\n- type: textarea\n  id: problem-description\n  attributes:\n    label: 问题描述\n    description: 请添加清晰简洁的描述，说明你希望通过此功能请求解决的问题。\n  validations:\n    required: true\n- type: textarea\n  id: proposed-solution\n  attributes:\n    label: 描述你想要的解决方案\n    description: 简洁明了地描述你要发生的事情。\n  validations:\n    required: true\n- type: textarea\n  id: alternatives-considered\n  attributes:\n    label: 描述你考虑过的替代方案\n    description: 对你考虑过的所有替代解决方案或功能的简洁明了的描述。\n- type: textarea\n  id: additional-information\n  attributes:\n    label: 附加信息\n    description: 如果你的问题需要进一步解释，或者想要表达其他内容，请在此处添加更多信息。（直接把图片/视频拖到编辑框即可添加图片/视频）\n"
  },
  {
    "path": ".github/actions/setup/action.yml",
    "content": "name: Setup\ndescription: Setup Node Env\n\nruns:\n  using: composite\n  steps:\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: '22'\n\n    # - name: Get npm cache directory\n    #   run: node -p -e '`NPM_CACHE_DIR=${require(\"child_process\").execSync(\"npm config get cache\").toString()}`' >> $GITHUB_ENV\n    #   run: echo \"NPM_CACHE_DIR=$(npm config get cache)\" >> $GITHUB_ENV\n\n    # https://docs.npmjs.com/cli/v10/configuring-npm/folders#cache\n    - name: Cache node modules\n      id: cache-npm\n      uses: actions/cache@v4\n      with:\n        path: ${{ env.NPM_CACHE }}\n        key: ${{ runner.os }}-npm-cache-${{ hashFiles('package-lock.json') }}\n        restore-keys: |\n          ${{ runner.os }}-npm-cache-\n\n    - name: Install dependencies\n      run: npm ci\n      shell: bash\n"
  },
  {
    "path": ".github/workflows/beta-pack.yml",
    "content": "name: Build Beta\n\non:\n  push:\n    branches:\n      - beta\n\nenv:\n  IS_CI: 'true'\n\njobs:\n  # CheckCode:\n  #   name: Lint Code\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - name: Check out git repository\n  #       uses: actions/checkout@v4\n\n  #     - name: Install Node.js\n  #       uses: actions/setup-node@v4\n  #       with:\n  #         node-version: 20\n\n  #     - name: Cache file\n  #       uses: actions/cache@v4\n  #       with:\n  #         path: |\n  #           node_modules\n  #           $HOME/.cache/electron\n  #           $HOME/.cache/electron-builder\n  #           $HOME/.npm/_prebuilds\n  #         key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }}\n  #         restore-keys: |\n  #           ${{ runner.os }}-build-\n\n  #     - name: Install Dependencies\n  #       run: |\n  #         npm ci\n\n  #     - name: Lint src code\n  #       run: npm run lint\n\n  Windows:\n    name: Windows\n    runs-on: windows-latest\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: pwsh\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $env:GITHUB_ENV\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Build Package Setup x64\n        run: npm run pack:win:setup:x64\n      - name: Upload Artifact Setup x64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-x64-Setup\n          path: build/*-x64-Setup.exe\n\n      - name: Build Package 7z x64\n        run: npm run pack:win:7z:x64\n      - name: Upload Artifact 7z x64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-win_x64-green\n          path: build/*win_x64-green.7z\n\n      - name: Build Package Setup arm64\n        run: npm run pack:win:setup:arm64\n      - name: Upload Artifact Setup arm64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-arm64-Setup\n          path: build/*-arm64-Setup.exe\n\n      - name: Build Package 7z arm64\n        run: npm run pack:win:7z:arm64\n      - name: Upload Artifact 7z arm64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-win_arm64-green\n          path: build/*win_arm64-green.7z\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List\n\n  Windows_7:\n    name: Windows_7\n    runs-on: windows-latest\n    env:\n      BUILD_WIN7: true\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: pwsh\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $env:GITHUB_ENV\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Prepare win7 undici env\n        run: |\n          npm install undici@5\n          Set-Content -Path .\\src\\common\\utils\\request.ts -Value \"export * from './request_node16'\"\n\n      - name: Build src code\n        env:\n          BUILD_WIN7: true\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Prepare win7 electron env\n        run: |\n          npm install electron@22\n          pip.exe install setuptools\n\n      - name: Build Package win7 Setup x64\n        run: npm run pack:win7:setup:x64\n      - name: Upload Artifact win7 Setup x64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-win7_x64-Setup\n          path: build/*win7_x64-Setup.exe\n\n      - name: Build Package win7 7z x64\n        run: npm run pack:win7:7z:x64\n      - name: Upload Artifact win7 7z x64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-win7_x64-green\n          path: build/*win7_x64-green.7z\n\n      - name: Build Package win7 7z x86\n        run: npm run pack:win7:7z:x86\n      - name: Upload Artifact win7 7z x86\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-win7_x86-green\n          path: build/*win7_x86-green.7z\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List\n\n  Mac:\n    name: Mac\n    runs-on: macos-latest\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Install python setuptools\n        run: brew install python-setuptools\n\n      - name: Get npm cache directory\n        shell: bash\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $GITHUB_ENV\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Build Package dmg\n        run: |\n          npm run pack:mac:dmg\n          npm run pack:mac:dmg:arm64\n\n      - name: Upload Artifact dmg\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-mac-dmg\n          path: |\n            build/*.dmg\n            !build/*-arm64.dmg\n      - name: Upload Artifact dmg\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-mac-dmg-arm64\n          path: build/*-arm64.dmg\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          md5 *.dmg\n\n  Linux:\n    name: Linux\n    runs-on: ubuntu-latest\n    # needs: CheckCode\n    steps:\n      - name: Install package\n        run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools\n\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: bash\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $GITHUB_ENV\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Build Package deb amd64\n        run: npm run pack:linux:deb:amd64\n      - name: Upload Artifact deb amd64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-deb-amd64\n          path: build/*_amd64.deb\n\n      - name: Build Package deb arm64\n        run: npm run pack:linux:deb:arm64\n      - name: Upload Artifact deb arm64\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-deb-arm64\n          path: build/*_arm64.deb\n\n      - name: Build Package deb armv7l\n        run: npm run pack:linux:deb:armv7l\n      - name: Upload Artifact deb armv7l\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-deb-armv7l\n          path: build/*_armv7l.deb\n\n      - name: Build Package x64 appImage\n        run: npm run pack:linux:appImage\n      - name: Upload Artifact x64 appImage\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-x64-appImage\n          path: build/*_x64.AppImage\n\n      - name: Build Package x64 rpm\n        run: npm run pack:linux:rpm\n      - name: Upload Artifact x64 rpm\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-x64-rpm\n          path: build/*.x64.rpm\n\n      - name: Build Package x64 pacman\n        run: npm run pack:linux:pacman\n      - name: Upload Artifact x64 pacman\n        uses: actions/upload-artifact@v4\n        with:\n          name: lx-music-desktop-x64-pacman\n          path: build/*_x64.pacman\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          md5sum *.deb *.rpm *.pacman *.AppImage\n"
  },
  {
    "path": ".github/workflows/build-test.yml",
    "content": "name: Run build test\n\non:\n  pull_request:\n    branches:\n      - dev\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22\n\n      - name: Install Dependencies\n        run: npm ci\n\n      - name: Eslint check\n        run: npm run lint\n\n      - name: Test Build\n        run: npm run build\n"
  },
  {
    "path": ".github/workflows/publish-version-info.yml",
    "content": "name: Publish NPM Version Info\n\non:\n  release:\n    types: [published]\n\njobs:\n  dispatch:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Repository Dispatch\n        uses: peter-evans/repository-dispatch@v2\n        with:\n          token: ${{ secrets.PAT }}\n          repository: lyswhut/lx-music-desktop-version-info\n          event-type: npm-release\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Build\n\non:\n  push:\n    branches:\n      - master\n\nenv:\n  IS_CI: 'true'\n\njobs:\n  # CheckCode:\n  #   name: Lint Code\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - name: Check out git repository\n  #       uses: actions/checkout@v4\n\n  #     - name: Install Node.js\n  #       uses: actions/setup-node@v4\n  #       with:\n  #         node-version: 20\n\n  #     - name: Cache file\n  #       uses: actions/cache@v4\n  #       with:\n  #         path: |\n  #           node_modules\n  #           $HOME/.cache/electron\n  #           $HOME/.cache/electron-builder\n  #           $HOME/.npm/_prebuilds\n  #         key: ${{ runner.os }}-build-caches-${{ hashFiles('**/package-lock.json') }}\n  #         restore-keys: |\n  #           ${{ runner.os }}-build-\n\n  #     - name: Install Dependencies\n  #       run: |\n  #         npm ci\n\n  #     - name: Lint src code\n  #       run: npm run lint\n\n  Windows:\n    name: Windows\n    runs-on: windows-latest\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: pwsh\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $env:GITHUB_ENV\n\n      - name: Show Env\n        run: echo \"${{ env.NPM_CACHE }}\"\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Release package\n        run: |\n          npm run publish:win:7z:x64\n          npm run publish:win:7z:arm64\n          npm run publish:win:setup:arm64\n          npm run publish:win:setup:x64\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BT_TOKEN: ${{ secrets.BT_TOKEN }}\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List\n\n\n  Windows_7:\n    name: Windows_7\n    runs-on: windows-latest\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: pwsh\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $env:GITHUB_ENV\n\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Prepare win7 undici env\n        run: |\n          npm install undici@5\n          Set-Content -Path .\\src\\common\\utils\\request.ts -Value \"export * from './request_node16'\"\n\n      - name: Build src code\n        env:\n          BUILD_WIN7: true\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Prepare win7 electron env\n        run: |\n          npm install electron@22\n          pip.exe install setuptools\n\n      - name: Release win7 package\n        run: |\n          npm run publish:win7:setup:x64\n          npm run publish:win7:7z:x64\n          npm run publish:win7:7z:x86\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BT_TOKEN: ${{ secrets.BT_TOKEN }}\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List\n\n  Mac:\n    name: Mac\n    runs-on: macos-latest\n    # needs: CheckCode\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Install python3 setuptools\n        run: brew install python-setuptools\n\n      - name: Get npm cache directory\n        shell: bash\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $GITHUB_ENV\n\n      - name: Show Env\n        run: echo \"${{ env.NPM_CACHE }}\"\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Release package\n        run: |\n          npm run publish:mac:dmg\n          npm run publish:mac:dmg:arm64\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BT_TOKEN: ${{ secrets.BT_TOKEN }}\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          md5 *.dmg\n\n  Linux:\n    name: Linux\n    runs-on: ubuntu-latest\n    # needs: CheckCode\n    steps:\n      - name: Install package\n        run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools\n\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Get npm cache directory\n        shell: bash\n        run: echo \"NPM_CACHE=$(npm config get cache)\" >> $GITHUB_ENV\n\n      - name: Show Env\n        run: echo \"${{ env.NPM_CACHE }}\"\n      - name: Setup Node Env\n        env:\n          NPM_CACHE: ${{ env.NPM_CACHE }}\n        uses: ./.github/actions/setup\n\n      - name: Build src code\n        run: |\n          git status --porcelain\n          npm run build\n\n      - name: Release package\n        run: |\n          npm run publish:linux:deb:amd64\n          npm run publish:linux:deb:arm64\n          npm run publish:linux:deb:armv7l\n          npm run publish:linux:appImage\n          npm run publish:linux:rpm\n          npm run publish:linux:pacman\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          BT_TOKEN: ${{ secrets.BT_TOKEN }}\n\n      - name: Generate file MD5\n        run: |\n          cd build\n          md5sum *.deb *.rpm *.pacman *.AppImage\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\nnode_modules.bak*/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n\n\nbuild\n\ndist\n\npublish/assets\n\npublish/utils/githubToken.js\n\nsrc/**/*-internal.js\n"
  },
  {
    "path": ".ncurc.js",
    "content": "module.exports = {\n  upgrade: true,\n  reject: [\n    'electron',\n    'chalk',\n    'del',\n    'comlink',\n    'vue',\n    'vue-router',\n    'image-size',\n    'message2call',\n    '@types/ws',\n    'eslint',\n    '@types/node',\n    'electron-debug',\n    'eslint-webpack-plugin',\n\n    'eslint-plugin-vue',\n    'vue-eslint-parser',\n    // 'eslint-config-standard-with-typescript',\n  ],\n\n  // target: 'newest',\n  // filter: [\n  //   'electron-builder',\n  //   'electron-updater',\n  // ],\n\n  // target: 'patch',\n  // filter: [\n  //   'electron',\n  //   'vue',\n  //   'vue-router',\n  // ],\n\n  // target: 'minor',\n  // filter: [\n  //  // 'electron',\n  //   'eslint',\n  //   'eslint-webpack-plugin',\n  //   'electron-debug',\n  //   '@types/node',\n\n  //   'eslint-plugin-vue',\n  //   'vue-eslint-parser',\n  // ],\n}\n"
  },
  {
    "path": ".vscode/i18n-ally-custom-framework.yml",
    "content": "# .vscode/i18n-ally-custom-framework.yml\n\n# An array of strings which contain Language Ids defined by VS Code\n# You can check avaliable language ids here: https://code.visualstudio.com/docs/languages/overview#_language-id\nlanguageIds:\n  - javascript\n  - typescript\n  - vue\n\n# An array of RegExes to find the key usage. **The key should be captured in the first match group**.\n# You should unescape RegEx strings in order to fit in the YAML file\n# To help with this, you can use https://www.freeformatter.com/json-escape.html\nusageMatchRegex:\n  # The following example shows how to detect `t(\"your.i18n.keys\")`\n  # the `{key}` will be placed by a proper keypath matching regex,\n  # you can ignore it and use your own matching rules as well\n  - \"[^\\\\w\\\\d]t\\\\(['\\\"`]({key})['\\\"`]\"\n\n\n# An array of strings containing refactor templates.\n# The \"$1\" will be replaced by the keypath specified.\n# Optional: uncomment the following two lines to use\n\n# refactorTemplates:\n#  - i18n.get(\"$1\")\n\n\n# If set to true, only enables this custom framework (will disable all built-in frameworks)\nmonopoly: true\n"
  },
  {
    "path": ".vscode/javascript.code-snippets",
    "content": "{\n\t// Place your lx-music-desktop-new 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and\n\t// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope\n\t// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is\n\t// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:\n\t// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.\n\t// Placeholders with the same ids are connected.\n\t// Example:\n\t// \"Print to console\": {\n\t// \t\"scope\": \"javascript,typescript\",\n\t// \t\"prefix\": \"log\",\n\t// \t\"body\": [\n\t// \t\t\"console.log('$1');\",\n\t// \t\t\"$2\"\n\t// \t],\n\t// \t\"description\": \"Log output to console\"\n\t// }\n  \"use i18n\": {\n\t\t\"prefix\": \"ui18n\",\n\t\t\"body\": [\n\t\t\t\"import { useI18n } from '@renderer/plugins/i18n'\",\n\t\t\t\"${1:const t = useI18n()}\"\n\t\t]\n\t},\n  \"list action\": {\n\t\t\"prefix\": \"listacion\",\n\t\t\"body\": [\n\t\t\t\"import { $1 } from '@renderer/store/list/action'\",\n\t\t]\n\t},\n  \"import vue tools\": {\n\t\t\"prefix\": \"imvt\",\n\t\t\"body\": [\n\t\t\t\"import { $1 } from '@common/utils/vueTools'\",\n\t\t]\n\t}\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"i18n-ally.localesPaths\": [\n    \"src/lang\"\n  ],\n  // \"i18n-ally.fullReloadOnChanged\": true,\n  \"i18n-ally.keystyle\": \"nested\",\n  \"i18n-ally.displayLanguage\": \"zh-cn\",\n  \"i18n-ally.sourceLanguage\": \"zh-cn\",\n  \"i18n-ally.translate.engines\": [\n    \"google-cn\",\n    \"google\"\n  ],\n  \"i18n-ally.sortKeys\": true,\n  \"javascript.preferences.importModuleSpecifier\": \"non-relative\",\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"vue.codeActions.enabled\": false\n}\n"
  },
  {
    "path": ".vscode/typescript.code-snippets",
    "content": "{\n\t// Place your lx-music-desktop-new 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and\n\t// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope\n\t// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is\n\t// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:\n\t// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.\n\t// Placeholders with the same ids are connected.\n\t// Example:\n\t// \"Print to console\": {\n\t// \t\"scope\": \"javascript,typescript\",\n\t// \t\"prefix\": \"log\",\n\t// \t\"body\": [\n\t// \t\t\"console.log('$1');\",\n\t// \t\t\"$2\"\n\t// \t],\n\t// \t\"description\": \"Log output to console\"\n\t// }\n  \"use i18n\": {\n\t\t\"prefix\": \"ui18n\",\n\t\t\"body\": [\n\t\t\t\"import { useI18n } from '@renderer/plugins/i18n'\",\n\t\t\t\"${1:const t = useI18n()}\"\n\t\t]\n\t},\n  \"import vue tools\": {\n\t\t\"prefix\": \"imvt\",\n\t\t\"body\": [\n\t\t\t\"import { $1 } from '@common/utils/vueTools'\",\n\t\t]\n\t}\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# lx-music-desktop change log\n\nAll notable changes to this project will be documented in this file.\n\nProject versioning adheres to [Semantic Versioning](http://semver.org/).\nCommit convention is based on [Conventional Commits](http://conventionalcommits.org).\nChange log format is based on [Keep a Changelog](http://keepachangelog.com/).\n\n## [2.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.0...v2.12.1) - 2026-02-16\n\n我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 优化\n\n- 优化托盘图标行为：在非 Windows 系统中，点击托盘图标时不再显示主窗口\n\n### 修复\n\n- 修复音量条在调整音量时实际音量与显示的数值不一致的问题（#2606）\n- 修复某些情况下搜索框的搜索按钮布局错位的问题（#2622）\n\n## [2.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.11.0...v2.12.0) - 2025-11-29\n\n我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 新增\n\n- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项 (#2360)\n  *默认启用，关闭后将使用系统原生的窗口样式，该选项重启软件后生效*\n- 开放 API 新增播放器声音大小、静音、播放进度控制、完整歌词获取，详情看接入文档 (#2386)\n- 新增「设置 → 播放设置 → 调换歌词翻译与歌词罗马音位置」选项，默认关闭 (#2451)\n- 新增启动参数 `-hidden`，在启动时将软件最小化到系统托盘 (#2459)\n- 新增 Any Listen 歌词（用于支持已下载歌曲的歌词逐字播放）标签数据读取与播放 (#2485)\n- 新增 Any Listen 歌词（包含逐字歌词、翻译、罗马音歌词，如果有）嵌入与下载，默认启用\n- 下载列表菜单新增歌曲添加弹窗，允许将所选歌曲的在线版本添加到收藏列表 (#2537)\n\n### 修复\n\n- 尝试修复进度为0时仍然显示下载完成的问题 (#2471)\n- 修复TX源搜索失败 (#2575 @Folltoshe)\n- 修复MG源歌单加载失败\n- 修复MG源评论加载失败\n\n### 变更\n\n- 调换「歌词翻译」与「歌词罗马音」的位置，现在歌词罗马音在歌词翻译的上方展示\n  *若你想要恢复以前的行为，可以开启「调换歌词翻译与歌词罗马音位置」选项*\n- 更新代理配置规则，现在不启用代理时，图片、音频加载将不再走系统代理 (#2382 @Folltoshe)\n- 字体设置可以最多设置两种字体（[any-listen#82](https://github.com/any-listen/any-listen/issues/82)）\n\n### 其他\n\n- 更新 Electron 到 37.6.0\n\n## [2.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.10.0...v2.11.0) - 2025-05-01\n\n### 新增\n\n- 新增「快进/快退5秒」自定义快捷键设置（#2289）\n- 新增「设置 → 桌面歌词设置 → 暂停时提高歌词透明度」设置，默认启用（#2294）\n\n### 修复\n\n- 修复 Windows 下桌面歌词最小高度与宽度设置问题（#2244）\n- 修复 Windows 下界面缩放后移动桌面歌词会改变歌词窗口大小的问题（#2244）\n- 修复 tx 歌单搜索名字、描述出现乱码的问题（#2250）\n- 修复本地 FLAC 文件内嵌歌词无法读取的问题\n- 修复潜在播放暂停的问题\n- 修复 kw 歌单详情出现打开失败的问题（#2317）\n- 修复 kg 热门评论无法获取的问题\n- 修复桌面歌词被遮挡时会被暂停的问题（#2320）\n- 修复 kg 歌单打开失败的问题（thanks @Folltoshe）\n\n### 优化\n\n- 允许更小的桌面歌词窗口宽度\n- 允许拖动桌面歌词控制栏空白处移动歌词窗口（#2280）\n- 优化「自定义源管理」对话框在小窗口下的布局（#2247, @3gf8jv4dv）\n- 优化软件文案编排（#2259, #2266, #2269, #2296, @3gf8jv4dv）\n\n### 变更\n\n- 我的列表-歌曲菜单中的 歌曲换源 功能从之前的类似软连接的形式改成替换歌曲的形式，也就是说，现在该功能相当于快速在线搜索歌曲，确认换源后将自动将原来的歌曲删除再将选择的歌曲插入被删除歌曲的位置。\n\n### 其他\n\n- 更新项目文档（@3gf8jv4dv）\n- 更新 Electron 到 35.2.2\n\n## [2.10.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.9.0...v2.10.0) - 2025-01-27\n\n落雪祝大家新年快乐！\n\n### 关于之前提到的新项目\n\n新项目我取名叫 Any Listen，希望它能像它的名字一样让我们能到处任意听歌。\n经过一年多的开发，因各种原因，实际进度比预期的慢，但还是赶在年前发布了第一个web服务预览版，第一个版本仅支持播放服务器上的歌曲，扩展功能暂时未能开放，但已趋于完成，一两个月内可以搞定。\n目前的版本仅是“能用”的状态，因时间关系，部分UI未能重新设计，但后面会继续完善。\n该项目目前的目标用户是拥有自己服务器且上面存储有歌曲的人使用。\n\n项目刚发布，文档未能完善，遇到使用问题或有任何建议欢迎提 issue 交流，\n项目地址： https://github.com/any-listen/any-listen\n\n---\n\n*感谢 @3gf8jv4dv 对 LX 系列项目翻译、文档等文案的大幅修订优化。*\n\n### 不兼容性变更\n\nLinux 系统至少需要 `GLIBC_2.29` 版本才能运行。\n\n由于将 Electron 升级到 v32.x，原生库的编译被限制到不低于 C++ 20，试了几次无法在 docker 镜像 `node:16` 安装 gcc-10，最终将构建使用镜像更新到 `node:18`。\n\n### 新增\n\n- 新增下载的歌曲按列表名分组的功能，默认关闭，可以通过「设置 → 下载设置 → 将文件保存到以对应列表命名的子目录中」启用（#2145）\n- 新增托盘图标样式「跟随系统亮暗模式」设置，可以在「设置 → 其他」里启用 （#2016）\n- 支持本地同名 `.krc` 格式歌词文件的读取（#2053）\n- 开放 API 新增播放器播放/暂停、切歌、收藏当前播放歌曲等接口调用，详情看文档「开放 API 服务」部分（#2077, @14Kay）\n\n### 优化\n\n- 优化正常播放结束时的下一首歌曲播放衔接度，在歌曲即将结束播放时将预获取下一首歌曲的播放链接，减少自动切歌时的等待时间（#2126）\n- 优化歌曲换源机制，提升换源正确率\n- 优化 Windows 平台上桌面歌词窗口大小调整机制，改用原生的窗口调整方式（#2137）\n- 修正搜索歌曲提示框文案（#2050）\n- 优化播放详情页 UI，修复「歌曲名」「艺术家」等文字过长时被截断的问题（#2049）\n- Scheme URL 的播放歌曲允许更长的专辑名称\n- 播放本地歌曲时，将优先尝试读取本地同名 `.jpg` 或 `.png` 图片作为播放封面显示，若文件不存在则从音频文件内读取，最后再尝试使用在线图片（#2096）\n- 客户端模式的同步服务连接允许重定向 5 次（#2109）\n- 更新软件默认使用的字体，修复 macOS Sequoia (15) 上界面出现乱码的问题（#2076）\n- 优化简体、繁体中文文案编排，大幅修订英语文案编排（#2159, #2166, #2174 等, @3gf8jv4dv）\n- 优化排序歌曲、主题名称、添加/编辑主题、列表更新管理等对话框布局及长文本显示效果（#2176, #2188, #2189, #2198 等, @3gf8jv4dv）\n\n### 修复\n\n- 修复歌单详情页内歌单名字过长时的 UI 显示问题（#2028）\n- 修复获取自定义环境音效预设列表逻辑问题\n- 修复 `.m4a` 文件内嵌歌词无法读取的问题（#2090）\n- 修复 Windows 任务管理器中的进程名显示为软件描述的问题（#2147）\n- 修复本地歌曲同名歌词文件调整偏移时间后，下次再播放时调整的设置未被应用的问题（#2139）\n- 修复首次打开软件后直接创建并删除列表时的报错问题（#2175, @14Kay）\n\n### 变更\n\n- 不再长期缓存换源歌曲信息\n- 更新软件默认使用的字体，现在软件尽量使用系统自带的默认字体\n- Linux 系统至少需要 `GLIBC_2.29` 版本才能运行\n\n### 其他\n\n- 更新 Readme 文档，优化文案编排（#2146, Thanks @3gf8jv4dv）\n- 更新 Issue 模板（#2153, @3gf8jv4dv）\n- 更新项目文档（@3gf8jv4dv）\n- 修订项目协议文件（#2146, #2152, @3gf8jv4dv）\n- 更新 Electron 到 v32.3.0\n\n## [2.9.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.8.0...v2.9.0) - 2024-08-24\n\n### 新增\n\n- 新增 设置-播放设置-是否将歌词显示在状态栏 设置，默认关闭，该功能只在 MacOS 下可用（#1940）\n- 新增设置-播放详情页设置-延迟歌词滚动设置（#1985）\n- 新增鼠标在音量按钮使用滚轮时可以调整音量大小的功能（#2000）\n- 新增设置-下载设置-同时下载任务数设置（#1498）\n- 新增 我的列表-歌曲右击菜单-歌曲换源 功能，换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲，该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题\n\n### 优化\n\n- 优化侧栏图标显示，修复图标可能被裁切的问题（#1960）\n- 托盘图标添加当前播放歌曲名字显示\n- 优化本地歌曲内嵌封面过大时的加载方式\n- 将下载歌曲的歌手信息中的分隔符从 `、` 替换为 `;` 以确保音乐元数据在写入时的兼容性和一致性（#1989 @qnnp-me）\n\n### 修复\n\n- 修复 MacOS 下点击 dock 右键菜单的退出按钮时，程序没有退出的问题（#1923）\n- 修复 OpenAPI 的 `lyricLineAllText` 在切换到无歌词的音乐时内容没有更新的问题（#1925）\n- 修复切换音源时可能出现切换死循环的问题\n- 尝试修复某些情况下播放音频时，处于播放状态但是进度条不走的问题\n- 修复程序目录路径存在 `#` 或 `%` 时，自定义源、托盘等图标异常的问题（#1997）\n\n### 变更\n\n- 简化了应用退出行为，据测试，现在 linux 下，若启用了托盘，dock 右键菜单的 退出、关闭所有 之类的功能将不再退出程序，需改用托盘的退出按钮退出程序\n- 现在如果在设置或者启动参数配置了代理服务，那么应用内的图片、音频加载，歌曲下载也将走代理\n\n### 其他\n\n- 更新 electron 到 v30.4.0\n\n## [2.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.7.0...v2.8.0) - 2024-06-01\n\n我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\n\n### 新增\n\n- 新增 设置-播放设置-使用设备能处理的最大声道数输出音频 设置（未启用时固定为2声道输出），由于这用到高级音频API，考虑到在某些设备上的兼容问题，默认禁用（#1873）\n- 允许添加 `m4a`、`oga` 格式的本地歌曲到列表中（#1864）\n- 开放API支持跨域请求（#1872 @Ceale）\n- Scheme URL API新增 `music/searchPlay` 支持，用于搜索并播放指定的歌曲名字，详细入参请阅读 Scheme URL 支持文档（#1886）\n\n### 优化\n\n- 优化白色托盘图标显示，修复windows下托盘图标不清晰的问题（#1842）\n\n### 修复\n\n- 修复存在多级弹窗时的背景显示问题\n- 增大在线导入自定义源文件的大小限制问题（#1857）\n- 修复Mac下窗口出现残留阴影的问题，这解决了Mac下桌面歌词出现残留阴影的远古bug，感谢 @zclorne （#1869, Thanks @zclorne）\n- 增大在线导入自定义源文件的大小限制，解决某些音源无法导入的问题（#1857）\n- 修复Mac下即使开启了托盘， `cmd+w` 仍会中断播放的问题（#1844）\n- 修复播放详情页的歌词无法使用触碰拖动的问题（#1865）\n- 修复与优化繁体中文、英语翻译显示（#1845）\n- 修复歌曲时文件名过长导致歌曲无法下载的问题（#1877）\n- 修复文本提示气泡在内容过长时，文本未被换行而被截断的问题\n- 修复翻页按钮栏切页按钮只显示前几页的问题\n\n### 变更\n\n- 设置-播放设置-优先播放320k音质选项改为“优先播放的音质”，允许选择更高优先播放的音质，如果歌曲及音源支持的话（#1839）\n\n### 开放API变更\n\n- `/status` 的入参现在与 `/subscribe-player-status` 保持一致\n- `/status` 新增 `filter` 入参用于过滤返回的字段，并内置了默认值，与之前相比默认不再返回 `picUrl`\n- `/status` 及 `/subscribe-player-status` 的可用字段名添加了 `lyricLineAllText`，它对应的值是当前句歌词及扩展歌词文本（扩展歌词包含翻译、罗马音等，按换行符分割）\n\n详情看开放API接入文档\n\n### 其他\n\n- 更新 electron 到 v28.3.3\n\n## [2.7.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.6.0...v2.7.0) - 2024-04-14\n\n### 新增\n\n- 主题编辑器添加“深色字体”选项，启用后将减少字体颜色梯度，各类字体（正文、标签字体等）颜色将更接近，这有助于解决创建全透明主题时可能出现的字体配色问题（#1799）\n- 新增在线自定义源导入功能，允许通过http/https链接导入自定义源\n- 新增HTTP开放API服务，默认关闭，该服务可以为第三方软件提供调用LX的能力，可用API看[说明文档](https://lyswhut.github.io/lx-music-doc/desktop/open-api)（#1824）\n- 托盘菜单新增播放、切歌、收藏控制\n- 添加当前软件版本所对应的代码提交版本、提交时间的显示，可到设置-版本更新查看\n\n### 优化\n\n- 主题设置默认折叠其他主题以优化进入设置界面时的性能\n- 不再丢弃kg源逐行歌词（@helloplhm-qwq）\n- 支持kw源排行榜显示大小（revert @Folltoshe #1460）\n- 托盘菜单添加多语言支持（#1802）\n- 优化本地歌曲换源匹配机制\n\n### 修复\n\n- 修复某些情况下歌曲加载时间过长时不会自动跳到下一首的问题\n- 修复mg歌词在某些情况下获取失败的问题（#1783）\n- 修复mg歌单搜索（@helloplhm-qwq）\n- 修复kg最新评论无法获取的问题（@helloplhm-qwq）\n- 修复更新超时弹窗在非更新阶段意外弹出的问题（#1797）\n- 修复网络代理设置没有对自定义源的网络请求生效的问题（#1814）\n\n### 移除\n\n- 移除未使用的网络代理设置用户名、密码设置，实际上在 v1.20.0 起这两个设置就没有在被内部使用\n\n### 其他\n\n- 更新 electron 到 v28.3.0\n\n## [2.6.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.5.0...v2.6.0) - 2024-02-01\n\n提交祝大家新年快乐！\n\n更新前需要注意：\n由于自定义源的调用方式变更，可能会导致某些第三方源停止工作，如果出现这种情况，你需要将LX回退到 v2.5.0\n\n### 新增\n\n- 若自定义源初始化失败，将会出现弹窗提示初始化失败的详情\n- 添加win7_x64架构的安装版安装包构建\n- 新增播放歌曲时阻止电脑休眠，默认启用，可到设置-播放设置关闭（#1563）\n\n### 优化\n\n- 更新zh-tw翻译\n- 自定义源列显示源版本号、作者名字\n- 优化列表全选机制，修复列表未获得焦点时仍然可以全选的问题\n- 优化搜索框交互逻辑，防止鼠标操作时意外搜索候选列表的内容\n- 添加对wy源某些歌曲有问题的歌词进行修复\n- 改进本地音乐在线信息的匹配机制\n- 优化任务下载状态显示，现在下载时若数据传输完成但数据写入未完成时会显示相应的状态\n- 添加对下载歌曲时封面图片大小的控制处理（#1609）\n- 添加创建同名列表时的二次确认（#1621）\n\n### 修复\n\n- 修复备份文件无法导入json格式的问题\n- Windows、MacOS平台下的字体列表取消使用原生方式获取以修复某些字体应用后无效的问题（#1596）\n- 修复亮暗主题自动切换功能无效的问题（#1697）\n- 修复 MacOS 平台在 Finder 打开文件或目录时应用卡死的问题（#1684）\n- 修复下载模块在数据写入速度较慢的情况下出现任务及文件异常的问题\n- 修复临时列表变更会意外触发同步的问题\n- 修复最小化后再隐藏窗口时，托盘菜单的显示主界面功能异常的问题\n\n### 变更\n\n- 播放歌曲时默认会阻止系统进入休眠状态，若你不行软件阻止系统休眠，可以到设置-播放设置取消勾选“播放歌曲时阻止电脑休眠”设置\n\n### 其他\n\n- 移除所有内置源，由于收到腾讯投诉要求停止提供软件内置的连接到他们平台的在线播放及下载服务，所以从即日（2023年10月18日）起LX本身不再提供上述服务\n- 更新 electron 到 v25.9.8\n- 更新许可协议的排版，使其看起来更加清晰明了，更新数据来源原理说明\n\n### 自定义源的不兼容变更与新增内容（源开发者需要看）\n\n自定义源的调用方式已改变：\n\n- 为了与移动端的调用方式统一，不再推荐使用 `window.lx` 对象（移动端无`window`对象），改用 `globalThis.lx`\n- `inited` 事件不再需要传递 `status` 属性，脚本运行过程中，在成功调用 `inited` 事件之前的任何首次未捕获的错误都将视为初始化失败，所以现在若想人为让脚本初始化失败，直接抛出一个错误即可\n- 新增 `globalThis.lx.env` 属性，桌面端环境固定为 `desktop`，移动端环境固定为 `mobile`\n- 新增 `globalThis.lx.currentScriptInfo` 对象，可以从这里获取解析后的脚本头部注释信息及脚本原始内容，具体可用属性看文档说明\n- `globalThis.lx.version` 属性更新到 `2.0.0`\n- 自定义源不再使用`script`标签的形式执行，若要获取脚本原始代码字符串需从 `globalThis.lx.currentScriptInfo.rawScript` 属性获取\n- 自定义源新增支持`local`源的`musicUrl`、`pic`、`lyric`的获取操作详情看自定义源文档说明\n\n## [2.5.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.4.1...v2.5.0) - 2023-09-28\n\n落雪提前祝大家中秋快乐~🥮😘！\n\n### 不兼容性变更\n\n- 由于微软及Electron即将结束对 Windows 7、Windows 8 的支持，所以从这个版本起，LX的默认 Windows 版也不再支持这些版本的系统，但考虑到仍然有许多人使用 Windows 7，我们特别构建了能在 Windows 7 上使用的免安装版（文件名带win7），需要注意的是这个版本将缺乏安全更新，若非必要情况，不要使用该版本\n- 由于微软在 Windows 10 2004版本已删除对32位的OEM支持，所以在这个版本起，LX的默认 Windows 版已不再提供32位的支持\n- 更改构建的文件名格式，主要修改linux下deb、rpm文件命名格式\n\n### 新增\n\n- 新增Scheme URL对播放器的控制操作，新增的操作包含 播放、暂停、下一首、上一首等，详情看Scheme URL文档\n\n### 优化\n\n- 通过歌曲菜单添加不喜欢歌曲时需要二次确认防止手抖\n\n### 修复\n\n- 修复音频输出设备设置在重启软件后被重置的问题（#1568）\n- 修复更换语言设置后源名称未更新的问题\n- 修复点击搜索、排行榜等在线列表歌曲右键菜单歌曲详情页会意外将该歌曲添加不喜欢的问题\n\n### 其他\n\n- 更新 electron 到 v25.8.3\n\n## [2.4.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.4.0...v2.4.1) - 2023-09-09\n\n目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** ，其他渠道均为第三方转载发布，可信度请自行鉴别。\n本项目无微信公众号之类的官方账号，谨防被骗。\n\n### 修复\n\n- 修复 v2.4.0 的默认数据库版本号不对导致首次安装该版本的用户无法再次启动软件的问题\n\n## [2.4.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.3.0...v2.4.0) - 2023-09-09\n\n目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** ，其他渠道均为第三方转载发布，可信度请自行鉴别。\n本项目无微信公众号之类的官方账号，谨防被骗。\n\n### 不兼容性变更\n\n该版本修改了同步协议逻辑，同步功能至少需要PC端v2.4.0或移动端v1.1.0版本或同步服务v2.0.0才能连接使用。\n\n### 新增\n\n- 新增我的列表名右键菜单-排序歌曲-随机乱序功能，使用它可以对选中列表内歌曲进行随机重排（#1440）\n- 新增数据同步服务端模式已认证设备列表管理，该功能位置：设置-数据同步-服务端模式-已认证设备列表\n- 新增“不喜欢歌曲”功能，可以在我的列表或者在线列表内歌曲的右击菜单使用，还可以去“设置-其他”手动编辑不喜欢规则，注：“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲，但你仍可以手动播放这些歌曲\n- 新增同步功能对“不喜欢歌曲”列表的同步\n- 新增软件内快捷键“不喜欢该歌曲”设置，全局快捷键“收藏歌曲”、“取消收藏”、“不喜欢该歌曲”设置\n- 新增设置-播放设置-点击相同列表内的歌曲切歌时是否清空已播放列表（随机模式下列表内所有歌曲会重新参与随机）选项，默认关闭\n\n### 优化\n\n- 优化音效设置-环境音效启用、禁用时的操作效果显示，修复禁用环境音效时仍然可以调整增益、新增预设的问题\n- 过滤翻译歌词或罗马音歌词中只有“//”的行（#1499）\n- 点击打开歌单弹窗背景将不再自动关闭弹窗，防止选择输入框里的内容时意外关闭弹窗\n- 优化数据传输逻辑，列表同步指令使用队列机制，保证列表同步操作的顺序\n- 优化桌面歌词在开启 缩放当前播放的歌词 并关闭 延迟歌词滚动 时的歌词滚动位置计算问题，现在歌词滚动应该可以正确滚动到目标位置了\n- 优化歌词在短时间内快速播放时的滚动效果，现在遇到这种情况时滚动将更平滑\n\n### 修复\n\n- 修复字体设置某些字体无法应用的问题\n- 修复搜索提示功能失效的问题（#1452, @Folltoshe）\n- 修复我的列表名右键菜单-排序歌曲按专辑名排序无效的问题（#1440）\n- 修复若路径存在 # 字符时，软件无法启动的问题\n- 修复搜索框在某些情况下输入内容后搜索时会自动清空的问题（#1472）\n- 修复某些tx源歌词因数据异常解析失败的问题\n- 修复windows平台下隐藏窗口后再显示时任务栏按钮丢失的问题\n- 修复首句歌词被提前播放的问题\n- 修复潜在导致列表数据不同步的问题\n- 修复kg无评论时的加载处理问题\n\n### 变更\n\n- 播放模式应该只适用于列表内的歌曲，所以单曲循环模式不应对“稍后播放”的歌曲有效，该行为现在与移动端一致\n- 随机模式下，通过点击与播放列表相同的列表切歌时，将不再清空已播放列表，即已播放的歌曲不再重新参与随机，若想恢复之前的行为可以去设置-播放设置启用清空已播放列表选项\n\n### 其他\n\n- 更新 electron 到 v22.3.23\n- 重构同步服务端功能部分代码，使其更易扩展新功能\n\n## [2.3.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.2...v2.3.0) - 2023-06-29\n\n### 新增\n\n- 新增音效设置（实验性功能），支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效（由于升降调需要实时处理音频数据，这会导致额外的CPU占用，已知问题：如果CPU资源不够时将处理导致任务堆积而出现声音异常，这时需要暂停播放一段时间等堆积的任务处理完毕再播放）\n- 播放速率设置面板新增是否音调补偿设置，在调整播放速率后，可以选择是否启用音调补偿，默认启用\n\n### 优化\n\n- Windows、MacOS平台下的字体列表改用原生方式获取，现在Windows平台下能显示当前已安装的更多类型字体了（注：MacOS平台未测，可用性未知）\n- 移除桌面歌词窗口透明边距，在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了\n- 过滤嵌入、下载的翻译、罗马音歌词时间标签，与主歌词时间不匹配的歌词将被丢弃，防止出现原歌词与翻译歌词顺序错乱的问题（#1358）\n\n### 修复\n\n- 修复列表名翻译显示\n- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题，同时也可能导致同步数据丢失的问题（要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本）\n- 修复下载时出现302错误的问题\n- 修复播放某些在线音频会没有声音的问题\n- 修复改变播放速率时会导致歌词报错的问题\n- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)\n- 修复wy源热搜词失效的问题（#1401, @Folltoshe）\n- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题（#1288）\n- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题\n- 修复mg歌单搜索歌单播放数量显示问题\n- 修复tx翻译歌词解析丢失的问题（更新版本后需手动清理一次歌词缓存）\n\n### 其他\n\n- 更新 electron 到 v22.3.15\n\n## [2.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.1...v2.2.2) - 2023-05-01\n\n### 修复\n\n- 修复在低版本Linux amd64系统上无法启动的问题（glibc版本要求过高导致的，采用内置预编译二进制文件的方式解决）\n- 修复添加歌曲弹窗默认列表名字显示问题\n\n## [2.2.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.0...v2.2.1) - 2023-05-01\n\n### 优化\n\n- 优化对系统Media Session的支持，现在切歌不会再会导致信息丢失的问题了\n- 启用桌面歌词时，取消对歌词窗口的聚焦\n- 增加kg歌单歌曲flac24bit显示（@helloplhm-qwq）\n- 增加tx源热门评论图片显示（@Folltoshe）\n- 优化更新弹窗弹出时机\n- 优化搜索框背景配色，使其适应高透明主题\n- 支持wy热门评论翻页\n\n### 修复\n\n- 修复启用全局快捷键时与Media Session注册冲突的问题，启用全局快捷键时，不再注册媒体控制快捷键\n- 修复mg搜索不显示时长的问题（@Folltoshe）\n- 修复mg评论加载失败的问题（@Folltoshe）\n- 修复对存在错误时间标签的歌词的解析\n\n### 其他\n\n- 自定义源API utils对象新增`zlib.inflate`与`zlib.deflate`方法，API版本更新到 v1.3.0\n- 更新kg、tx、wy等平台排行榜列表\n- 更新 electron 到 v22.3.7\n\n## [2.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.2...v2.2.0) - 2023-03-26\n\n从v2.2.0起，我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)，如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明\n\n### 不兼容性变更说明\n\n- 同步功能，从这个版本起，数据同步功能至少需要移动端v1.0.0的版本才能连接，连接的地址格式也略有改变，详情看[文档说明](https://lyswhut.github.io/lx-music-doc/desktop/faq/sync)\n\n### 新增\n\n- 重构数据同步功能，新增客户端模式\n- 新增全屏时自动关闭歌词设置，默认开启，可以去设置-桌面歌词设置更改\n- 新增设置-桌面歌词设置-重置窗口设置功能，点击时会重置桌面歌词窗口大小及位置\n- 新增设置-其他-列表数据清理功能，点击时会清空已创建的所有列表及所有收藏的歌曲\n\n### 优化\n\n- 支持wy源flac hires歌曲类型的显示\n- 快捷键调整音量时每次加减2%音量改为4%（#1220）\n- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容，保留点击切换显示/隐藏\n- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示（#1231, #1238 By @helloplhm-qwq, @Folltoshe）\n- 播放速率的粒度调整为0.01，范围0.6-2.0x\n\n### 修复\n\n- 修复同步连接的处理问题\n- 修复记住播放进度的情况下，使用Scheme URL打开应用播放的歌曲进度没有被重置的问题\n- 修复使用酷狗码无法打开某些类型的歌单的问题\n- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题\n- 修复连续选择时的初始选择歌曲位置被意外改变的问题\n\n### 其他\n\n- 更新 Electron 到v22.3.4\n\n## [2.1.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.1...v2.1.2) - 2023-02-18\n\n\n### 修复\n\n- 修复处于最新版本时更新弹窗日志内容显示异常的问题\n- 修复更新到最新版本后的首次启动时的更新日志未显示的问题\n\n## [2.1.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.0...v2.1.1) - 2023-02-18\n\n\n### 修复\n\n- 修复检查更新日志地址不正确的问题\n\n## [2.1.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.5...v2.1.0) - 2023-02-18\n\n由于软件内功能在设计时只考虑简单便捷性，是否对新手友好并不是我们考虑的重点，功能的新增、变更会在更新日志中注明，不会在软件内做指引提示，\n因此为了更愉快地使用本软件，我们建议在使用新版本时阅读一遍更新日志以了解软件的变更情况，同时若遇到问题可以去阅读常见问题找解决方案\n\n### 新增\n\n- 新增桌面歌词设置字体加粗设置，可以到设置-桌面歌词设置-加粗字体修改\n- 新增是否自动下载更新设置，默认开启，可以去设置-软件更新更改\n- 新增当前版本更新日志显示弹窗（建议大家阅读更新日志以了解当前版本的变化），在更新版本后将自动弹出\n- 新增是否在更新版本的首次启动时显示更新日志弹窗设置，默认开启，可以去设置-软件更新更改\n- 新增播放速率调整功能，可以去播放详情页的控制按钮调整，范围限制为x0.5至x2之间（#13）\n- 添加wy、tx源（感谢某位不愿透露姓名的大佬提供的C++算法源码，但由于作者不希望公开，所以将会以预构建二进制文件的形式加入代码仓库中）逐字歌词的支持\n- 新增设置-下载设置-是否嵌入翻译歌词、罗马音歌词设置，默认关闭\n- 添加启动时的数据库表及表结构完整性校验，若未通过校验，则会显示弹窗提示后将该数据库重命名添加`.bak`后缀后重建数据库启动。对于某些人遇到更新到v2.0.0后出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表的问题，可以通过此特性自动重建数据库并重新迁移数据，不再需要手动去数据目录删除数据库\n\n### 优化\n\n- 微调了桌面歌词逐行字体阴影，使其看起来更匀称\n- 调整了桌面歌词在启用滚动到顶部时的距离，现在滚动到顶部的歌词更靠边，不再受字体大小、歌词间距影响\n- 优化更新弹窗内容的显示，添加了自动更新失败时的更新指引\n- 为所有文本输入框添加右键快速粘贴的功能，右击输入框可以自动粘贴剪贴板的文字，若选中文字时将粘贴并替换选中文字\n- 防止桌面歌词窗口在屏幕分辨率变小时，窗口位置跟随分辨率变化的问题，现在若屏幕分辨率变小后窗口位置仍会在原始分辨率的位置（添加这个机制是为了解决屏幕分辨率被临时调整时的位置更新问题，如运行某些低分辨率的全屏游戏、高分辨率外接屏幕休眠时），但若你的分辨率调整不是临时的，因窗口在原始位置导致看不到窗口可以开关桌面歌词即可重新自动调节回屏幕内\n\n### 修复\n\n- 修复播放下载列表的歌曲时，调整歌词偏移时间功能异常的问题\n- 修复较旧Linux arm64系统下无法启动软件的问题（将预构建模块的所需glibc版本降级到2.28）（#1161）\n- 修改列表响应式更新机制，尝试修复偶现的删除歌曲列表未更新的问题\n- 修复某些kg歌单链接无法打开的问题\n- 修复将桌面歌词放到屏幕边缘时，偶现的开启桌面歌词后出现歌词窗口位置出现少许偏移的问题，以及将歌词窗口调整到全屏大小后，重开桌面歌词窗口被缩小出现边距的问题\n\n### 其他\n\n- 更新Electron到v22.3.0\n\n## [2.0.5](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.4...v2.0.5) - 2023-01-18\n\n这应该是LX今年的最后一个版本，提前祝大家新年快乐~😘\n\n### 修复\n\n- 修复声音输出设备更改时后的自动暂停播放设置无效的问题\n- 重写桌面歌词窗口坐标的计算逻辑，修复桌面歌词移动到最边缘时，某些情况下在启用歌词后会出现窗口偏移的问题（远古bug了）\n- 修复随机播放模式下使用稍后播放功能播放我的列表的歌曲时，切换下一曲永远是当前歌曲的问题（#1147）\n- 修复macOS下的软件系统菜单中的退出功能不会完全退出软件的问题（#1148）\n\n## [2.0.4](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.3...v2.0.4) - 2023-01-15\n\n\n### 修复\n\n- 修复备份文件导入指引无法识别v2配置的问题\n- 修复从搜索界面进入歌单详情后，若启用强迫症设置的清空功能会导致意外清空搜索框、搜索列表的问题\n- 修复桌面歌词在启用卡拉OK歌词后字体边缘可能被截断的问题（特别是纵向歌词某些字的边角被截断导致后面的阴影露出来或阴影不均匀的问题）\n- 修复桌面歌词启用歌词缩放后的阴影显示问题\n- 修复Linux armv7l系统（如树莓派）下无法启动的问题（与修复Linux arm64的方法一样采用内置预编译模块的方式修复）\n- 修复备份与恢复的列表导入列表信息设置逻辑问题与潜在导入问题\n\n## [2.0.3](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.2...v2.0.3) - 2023-01-08\n\n\n### 修复\n\n- 修复初始设置的桌面歌词窗口没有完全居右下角的问题\n- 修复Linux arm64系统下无法启动的问题（#1102）\n- 修复桌面歌词使用斜体出现截断的问题（#1106）\n- 修复某些情况下歌词的滚动问题\n- 修复禁用切歌时歌曲播放完毕后的歌曲信息显示问题\n- 修复修改播放设置-音频输出设置后，所做的更改没有被保存的问题\n\n### 优化\n\n- 点击打开歌单弹窗背景可以关闭弹窗（#1096）\n\n## [2.0.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.1...v2.0.2) - 2023-01-02\n\n若你更新v2.0.0后，出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题，可以按以下方式解决：\n\n1. 根据你的平台类型，进入软件数据目录\n   - Windows：`%APPDATA%/lx-music-desktop`\n   - Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\n   - macOS：`~/Library/Application Support/lx-music-desktop`\n\n2. 进入`LxDatas`目录，退出LX，删除`lx.data.db`文件，再启动软件即可\n\n若以上操作仍然不行，可以加交流群或者在GitHub开issue反馈\n\n### 修复\n\n- 修复无效的歌曲信息导致我的列表数据迁移失败的问题\n\n## [2.0.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.0...v2.0.1) - 2023-01-02\n\n若你更新v2.0.0后，出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题，可以按以下方式解决：\n\n1. 根据你的平台类型，进入软件数据目录\n   - Windows：`%APPDATA%/lx-music-desktop`\n   - Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\n   - macOS：`~/Library/Application Support/lx-music-desktop`\n\n2. 进入`LxDatas`目录，退出LX，删除`lx.data.db`文件，再启动软件即可\n\n若以上操作仍然不行，可以加交流群或者在GitHub开issue反馈\n\n### 优化\n\n- 单次执行所有sql语句，尝试解决某些情况下某些表没有成功创建的问题\n\n## [2.0.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.22.3...v2.0.0) - 2023-01-01\n\n\n### 不兼容性变更说明\n\n- 数据迁移，升级此版本时，会使用旧版本的我的列表、下载设置、快捷键设置、自定义源等数据会自动迁移到新的数据格式版本，旧的数据仍然会保留，但下载列表的数据不做迁移\n- 备份文件，v2.0.0及以后版本导出的列表、配置不支持导入v2.0.0之前版本，但v2.0.0之前版本导出的列表、配置支持导入v2.0.0以及以后版本（移动端需v0.15.0起才支持导入PC端v2生成的备份数据）\n- 同步功能，该功能不支持与移动端v1.0.0之前版本的使用，需等待后面的新版移动端，目前移动端v1的开发工作已在进行中\n\n### 新增\n\n- 新增自定义主题功能\n- 新增歌单搜索功能\n- 新增将本地歌曲添加到我的列表的支持，此功能可以在列表的右击菜单中使用（本地歌曲的歌词优先尝试读取相同路径下的同名歌词文件，若文件不存在则尝试读取歌曲文件内的歌词，若还是找不到歌词则尝试利用换源功能获取在线歌词，歌曲封面则是尝试读取歌曲文件内的封面，若不存在则利用换源功能获取在线封面）\n- 启动软件时自动回到上次的界面，例如上次退出软件时在我的列表，下次启动软件时会自动进入我的列表\n- 新增启动软件时自动播放音乐设置，默认关闭，可去设置-播放设置开启\n- 新增“蛋雅深藍”、“近墨者黑”皮肤\n- 新增下载歌词时是否同时下载歌词翻译、罗马音设置，默认关闭，可以去设置-下载设置开启（#344）\n- 新增下载时，若目录存在同名的文件时是否跳过下载此任务的设置（默认跳过，可以去设置-下载设置更改）\n- 新增界面字体大小设置\n- 桌面歌词新增竖排歌词显示功能（#971）\n- 桌面歌词新增歌词对齐方式、是否不允许歌词换行、歌词颜色、滚动对齐方式、歌词间距设置\n- 桌面歌词新增歌曲频谱显示（得益于主窗口与桌面歌词进程通信的改进，可以将此功能以CPU使用率“相对较低”的方式带到桌面歌词中）\n- 桌面歌词新增在任务栏显示歌词进程设置（此设置用于在录屏软件无法捕获歌词窗口时的变通解决方法）（#1063）\n- 添加kg源罗马音歌词的支持（感谢@helloplhm-qwq）\n- 支持打开波点音乐歌单（需在酷我源打开）\n- 新增设置-基本设置-播放栏进度条样式设置（此版本默认使用迷你进度条样式，对于某些不喜欢该样式的人可以将其换成其他样式）\n- 添加kg源评论图片展示（感谢@helloplhm-qwq）\n\n### 优化（界面/交互/功能）\n\n- 调整软件界面及配色，使其更加清爽\n- 处于单曲循环、顺序播放、禁用切歌模式时，手动切歌将会按列表循环模式的逻辑处理切歌（#864）\n- 歌单右键菜单的“重复歌曲”扫描功能现在会将歌曲名字内的括号内容移除再对比，这可以有效找出歌曲的变体，例如：`突然的自我`、`突然的自我(Live)`、`突然的自我（女生版）`、`突然的自我(DJ版)`等都会被找出来（#987）\n- 允许更小的桌面歌词窗口高度，可以取消“不允许拖动到主屏幕之外”设置后，再启用“不允许歌词换行”、“置顶歌词”与“自动刷新置顶”等设置，把它拖动到任务栏上，当做任务栏歌词使用（具体可以按你想要的显示方式使用这些设置组合去调）\n\n### 优化（程序）\n\n- 优化程序启动性能，优化与程序交互的流畅度\n- 重构整个程序，重新梳理了程序逻辑，使其更容易扩展及维护，将大部分代码从JavaScript迁移到TypeScript\n- 重写配置管理、列表管理功能，列表、歌词数据从json文件迁移到sqlite3存储，这应该能解决因为意外的字符编码导致的数据文件损坏问题\n\n### 变更\n\n- 列表右侧的操作按钮栏默认不再显示，歌曲的操作可以使用右键菜单代替，若想恢复它们的显示，可以去设置-列表设置-启用操作按钮栏开启\n- 窗口大小设置时不再自动调整字体大小，想要调整字体大小可以使用新增的字体大小设置调整\n\n### 修复\n\n- 修复Linux、macOS下若程序路径存在百分号时会导致软件无法启动的问题（#963）\n- 支持单行多时间标签歌词解析，修复某些歌词会出现时间标签的问题\n\n### 移除\n\n- 移除“信口雌黄”皮肤（由于该皮肤的配色有点刺眼），若你正在使用该皮肤，可以使用自定义主题功能恢复它\n- 移除Linux deb x86包构建，Electron/Chromium已不再支持 32-bit Linux（electron/electron#34787）\n- 移除桌面歌词主题设置，改用桌面歌词字体颜色设置功能代替\n\n### 其他\n\n- 更新Electron到v19.1.9\n\n## [1.22.3](https://github.com/lyswhut/lx-music-desktop/compare/v1.22.2...v1.22.3) - 2022-09-03\n\n### 修复\n\n- 修复因音源的域名到期导致的音源失效的问题\n\n## [1.22.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.22.1...v1.22.2) - 2022-08-18\n\n### 优化\n\n- 为tx、kw源添加 Flac 24bit 音质显示，注：由于之前没有记录此音质，所以之前收藏的歌曲信息中不包含它\n\n### 修复\n\n- 修复无法批量排序歌曲的问题\n- 修复某些缺失的繁体中文翻译\n- 修复企鹅音乐搜索失效的问题\n\n### 其他\n\n- 降级electron到v15.5.7\n\n## [1.22.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.22.0...v1.22.1) - 2022-07-09\n\n### 优化\n\n- 歌单列表添加歌单内歌曲数量显示，注：目前只有kw、mg、wy、tx（部分）源支持显示\n\n### 修复\n\n- 修复处于不支持的源时，歌单、排行榜的右键下载菜单没有禁用的问题\n- 修复若桌面歌词窗口与主窗口重叠时，鼠标划过重叠区域鼠标会闪烁的问题，注：此修复只对未启用“鼠标移入歌词区域时降低歌词透明度”时有效\n- 修复tx源搜索失效的问题\n\n### 其他\n\n- 升级Electron到 v17.4.10\n\n## [1.22.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.21.0...v1.22.0) - 2022-06-19\n\n### 新增\n\n- 新增设置-以全屏模式启动设置\n- 新增设置-桌面歌词设置-鼠标移入歌词区域时降低歌词透明度（#883），默认关闭，此设置不支持linux，注：此功能存在兼容性问题，若鼠标移出后无法恢复到正常透明度，可尝试再移入移出即可恢复\n\n### 优化\n\n- 添加歌曲到“我的列表”时，若按住`ctrl`键（Mac对应`Command`），则不会自动关闭添加窗口，这对想要将同一首（一批）歌曲添加到多个列表时会很有用\n- 支持mg源逐字歌词的播放，感谢 @mozbugbox 提供的帮助\n- 添加歌曲列表更新操作的二次确认\n- 添加导入文件错误时的指引提示\n\n### 修复\n\n- 修复若配置了`http_proxy`环境变量时，会意外使用此代理配置的问题\n- 修复多选后切换列表后不会清空多选内容的问题\n- 修复设置快捷键时的处理逻辑问题\n- 修复在新建歌单输入框、歌单内歌曲搜索输入框会意外触发设置的全局快捷键的问题（#879）\n\n### 文档\n\n桌面版文档已迁移到：<https://lyswhut.github.io/lx-music-doc/desktop>\n\n### 其他\n\n- 更新 Electron 到 v17.4.7\n\n## [1.21.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.20.0...v1.21.0) - 2022-05-22\n\n### 新增\n\n- 新增设置-播放设置-显示歌词罗马音，默认关闭，注：目前只有网易源能获取到罗马音歌词（得益于 Binaryify/NeteaseCloudMusicApi/pull/1523），如果你知道其他源的歌词罗马音获取方式，欢迎PR或开issue交流！\n\n### 优化\n\n- 同时删除一首歌以上时将需要二次确认删除\n- 禁用透明窗口时右侧不再偏移5px距离（在win7、Ubuntu等系统上测试发现不偏移也不影响滚动条的拖动了）\n- 删除未下载完成的任务时，只同时尝试删除已有下载进度的本地文件\n- 在全屏状态下使用`Esc`键可以退出全屏（#827）\n\n### 修复\n\n- 修复某些情况下歌曲播放出错时不会自动切歌的问题\n- 修复关闭“显示切换动画”设置后，在应用启动时该设置没有被应用的问题\n- 修复原始歌词存在偏移时，歌词偏移设置的重置未按预期工作的问题\n- 修复长度大于一行的歌词在使用歌词调整播放进度时的时间不准问题\n- 修复潜在歌单更新失败的问题\n\n### 文档\n\n- 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\n- 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\n\n## [1.20.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.19.0...v1.20.0) - 2022-04-17\n\n特别说明：Scheme URL其实是支持Linux系统的，但好像需要deb之类的安装包创建出`.desktop`文件才行。\n\n### 新增\n\n- 新增播放详情页歌词右键菜单，原来设置-播放详情页设置的字体重置已迁移到此菜单内\n- 新增歌词偏移设置，可以在播放详情页歌词右键菜单中使用\n- 新增设置-播放设置-播放错误时自动切换歌曲设置，默认开启（原来的行为），若你不想在遇到音频加载失败、url获取失败等错误时自动切歌可以关闭此设置\n- 新增设置-桌面歌词设置-自动刷新歌词置顶（当歌词置顶后仍被某些程序遮挡时可尝试启用此设置）\n- 新增列表更新管理，可以在鼠标移入“我的列表”标题时出现的按钮中进入，这可以用来设置启动软件时需要自动从原平台更新的列表\n\n### 优化\n\n- 优化播放详情页背景显示，现在有背景图片的主题可以在播放详情页显示它的图片了\n- 播放详情页在全屏状态下仍会显示退出播放详情页按钮，同时在其旁边添加退出全屏按钮\n- 播放详情页在全屏状态下鼠标在空白处静止不动3秒后自动将其隐藏\n\n### 修复\n\n- 修复Linux无法全屏的问题\n- 修复播放下载列表的歌曲时，使用Windows任务栏缩略图工具栏控制按钮的收藏按钮收藏歌曲时的异常问题\n- 修复启用搜索历史但不启用热门搜索时，搜索历史不显示的问题\n- 修复窗口尺寸设置对应的字体大小在启动后不生效的问题\n- 修复wy源搜索某些歌曲时第一页之后的歌曲无法加载的问题\n- 修复使用Scheme URL搜索歌曲时，不会自动关闭播放详情页（若处于打开状态）的问题\n- 修复换源失败时的处理问题\n- 修复启用代理时，https请求可能被挂起或被转为http的问题\n- 修复正在下载的歌曲暂停任务后，再开始会导致程序卡死的问题\n\n### 变更\n\n- 播放详情页的任意地方右键双击隐藏详情页的行为，“任意区域”改为在“非歌词区域”\n\n### 移除\n\n- 移除设置-播放详情页设置-歌词字体重置，此设置项已迁移到播放详情页的歌词菜单中\n- 移除播放详情页使用+-快捷键调整字体大小的功能，改用歌词右键菜单的字体大小调整功能\n\n## [1.19.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.18.0...v1.19.0) - 2022-03-20\n\n### 新增\n\n- 新增对播放详情页歌词大小、是否缩放、对齐方式的设置，可以去设置-播放详情页设置查看\n- 新增播放详情页通过歌词调整播放进度，默认关闭，需要到设置-播放详情页设置开启，开启后在播放详情页拖动歌词时将会出现跳转当前行歌词播放的按钮\n- 新增全屏状态，按F11可以进入、退出全屏状态，由于全屏时会隐藏控制栏按钮，所以需要使用鼠标右键双击（详情页的任意地方都可以）来关闭播放详情页\n- 新增动态主题“道法自然”，你可以预先设置一个亮色主题及暗色主题，此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。注：鼠标 右击 此主题项即可打开亮、暗色主题设置窗口。\n- 新增对kw源卡拉OK歌词的支持\n\n### 优化\n\n- 优化Windows任务栏缩略图工具栏控制按钮在浅色任务栏下的显示效果\n- 添加音频可视化与音频输出设备冲突的提示\n- 优化歌词的播放偏移\n- 优化托盘菜单操作（#686）\n- 优化播放下载列表时的切歌性能\n\n### 修复\n\n- 修复“当前的声音输出设备被改变时暂停播放歌曲”设置无效的问题\n- 修复桌面歌词没有处理停止播放状态的问题\n- 修复AppImage包无法运行的问题\n- 修复Windows任务栏缩略图工具栏控制按钮的歌曲收藏按钮状态更新问题\n- 修复使用链接导入的歌单无法在我的列表打开原歌单详情页的问题\n- 修复播放下载列表的歌曲时增删下载任务导致正在的歌曲序号改变时，不会更新到新增序号的问题\n\n### 文档\n\n添加LX中定义的快捷操作汇总说明到常见问题中，这是目前可用的鼠标、键盘快捷操作，它们都可以在更新日志中找到\n\n- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲\n- 鼠标右击播放栏进度条上的LRC按钮可以锁定/解锁桌面歌词\n- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内\n- 鼠标右击搜索界面中的单条搜索历史可以将其移除\n- 歌曲列表内的文字在选中后，鼠标右击可以复制已选中的文字，此功能只对搜索、歌单、排行榜、我的列表中的列表有效\n- 鼠标在播放详情页内右键双击可以关闭播放详情页\n- 鼠标左击播放栏上的歌曲名字可以将它复制\n- 鼠标右击“道法自然（英文Auto）”主题可以打开亮、暗主题设置窗口\n- 歌曲搜索框的候选内容可以用键盘上下方向键选择，按回车键搜索已选内容\n- 在歌单详情页按退格键可以返回歌单列表\n- 歌曲列表中可以使用Ctrl、Shift键进行多选，这类似Windows下的文件选择，详情看常见问题列表多选部分\n- 在我的列表内可以使用Ctrl+f键打开搜索框进行列表内歌曲搜索，搜索框按Esc键可以关闭搜索框，搜索框内按上下方向键可以选择歌曲，按回车键跳转到已选歌曲，按Ctrl+回车可以跳转并播放已选歌曲\n- 在我的列表按住Ctrl键可以进入列表拖动模式，此时可以用鼠标拖动列表调整列表的位置\n- 编辑列表名时按Esc键可以取消编辑\n- 按F11可以进入、退出全屏状态\n\n## [1.18.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.17.1...v1.18.0) - 2022-02-26\n\n### 新增\n\n- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置，此功能仅对歌单、排行榜有效，默认关闭\n- 新增打开收藏的在线列表的对应平台详情页功能，可以在我的列表-列表右键菜单中使用\n- 新增定时暂停播放功能，由于此功能大多数人可能不常用，所以将其放在设置-基本设置中\n- 新增任务栏缩略图工具栏控制按钮（此功能仅在Windows平台可用），按钮分别为收藏/取消收藏（将歌曲添加到“我的收藏”列表）、上一曲、播放/暂停、下一曲\n- 新增设置-基本设置-软件字体设置，此设置可用于设置主界面的字体（已知的问题：Windows 7 下可能会出现字体列表为空的情况，这是当前系统的 Powershell 版本小于5.1导致的，请自行尝试看常见解决）\n- 新增Scheme URL对音乐搜索的调用支持，详情看常见问题-Scheme URL支持\n- 新增Scheme URL以url传参的方式调用，详情看常见问题-Scheme URL支持\n- 自定义源新增更新弹窗方法，同时自定义源管理新增是否允许源显示更新弹窗设置（出于防止滥用考虑），当源作者想要通知用户源已更新时，可以调用此方法弹窗告诉用户，调用说明看常见问题-自定义源部分\n\n### 优化\n\n- 过滤tx源某些不支持播放的歌曲，解决播放此类内容会导致意外的问题\n- 把歌曲的热门评论与最新评论拆分成两个列表显示\n\n### 修复\n\n- 修复排行榜名字右击菜单的播放功能在播放非激活的列表时的列表获取问题\n- 修复修改列表名时无法使用`Ctrl`键的问题\n- 修复wy源某些歌曲获取歌词翻译的问题处理\n- 修复下载功能的歌词换源时会进入死循环的问题\n- 修复某些歌曲无法下载的问题\n- 修复windows平台下软件目录存在`portable`文件夹时，仍会创建`C:\\Users\\<user>\\AppData\\Roaming\\lx-music-desktop\\Dictionaries\\en-US-9-0.bdic`文件的问题，现在不会再创建文件，但仍会创建空目录（Electron的问题，目前暂无解决方法）\n- 修复播放器的停止逻辑问题\n\n### 其他\n\n- 更新electron到v13.6.9\n\n## [1.17.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.17.0...v1.17.1) - 2022-01-28\n\n### 优化\n\n- 优化kw源英文与翻译歌词的匹配\n\n### 修复\n\n- 修复快捷键与默认按键行为冲突的问题，现在若将某些有默认行为的按键（如在列表中上、下箭头、Home、End等键可以使列表滚动）设置为快捷键时，将禁用其默认行为\n- 修复列表的聚焦问题，现在在列表中使用上、下箭头、空格等键滚动列表时不会导致滚动到一定距离后丢失焦点的问题\n\n### 其他\n\n- 更新electron到v13.6.8\n\n## [1.17.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.16.0...v1.17.0) - 2022-01-22\n\n### 新增\n\n- 新增“便携”功能，在Windows平台下，若程序目录下存在 portable 目录，则自动使用此目录作为数据存储目录\n- 新增 Scheme URL 支持，同时发布lx-music-script项目配合使用（一个油猴脚本，可以在浏览器中的官方平台网页直接调用LX Music），Scheme URL的调用说明看Readme.md文档的Scheme URL支持部分\n- 新增启动参数`-proxy-server`与`-proxy-bypass-list`，详细介绍看Readme.md文档的启动参数部分\n- 新增桌面歌词是否延迟滚动设置，默认开启，若你不想要桌面歌词延迟滚动可以去设置-桌面歌词设置关掉\n\n### 优化\n\n- 为可视化音频的频谱整体添加频谱均值加成，使频谱显示更有节奏感\n- 优化程序初始化逻辑，修复无网络的情况下的初始化问题\n- 我的列表-列表名的右击菜单更新已收藏的在线列表时，将始终重新加载，不再使用缓存，解决在原平台更新歌单后，在LX点击更新可能看到的还是在原平台更新前的歌单的问题\n\n### 修复\n\n- 修复代理不生效的问题\n- 修复`openDevTools`选项无效的问题\n- 修复播放状态的提示问题\n- 修复tx源无搜索结果的问题\n\n### 其他\n\n- 更新 Electron 到 v13.6.7\n\n## [1.16.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.3...v1.16.0) - 2022-01-01\n\n这算是一个大版本，对主窗口部分的代码逻辑做了较大改动，但由于界面的改动不大，所以没有更新大版本号。\n虽然经过一个月的测试与问题修复，但可能仍然存在未发现的问题，若你发现某些界面异常、某些行为与旧版本存在差异等问题，欢迎反馈！\n另外祝大家元旦快乐~！\n\n### 新增\n\n- 播放详情页新增音量控制条\n- 播放详情页新增桌面歌词切换按钮\n- 新增将我的列表保存为TXT、CSV格式，可以去设置-备份与恢复中使用（注意：此类格式的备份目前不支持恢复到LX Music中）\n- 新增根据歌曲名、歌手名等字段对列表自动排序的功能，可以在我的列表右击列表名弹出的菜单中使用\n- 新增将播放与下载的歌词转换为繁体中文选项，默认关闭，可在设置-播放设置中开启\n- 现在已允许进入临时播放列表，即：使用歌单详情页、排行榜名称右键菜单的“播放”按钮播放歌曲时，可右击播放封面进入此临时列表\n- 播放详情页新增音频可视化功能（实验性）\n- 我的列表新增拖动调整位置功能，按住Ctrl键（Mac上对应Command键）的时候将进入“拖动模式”，此时可以拖动列表的位置来调整顺序\n\n### 优化\n\n- 优化列表性能，软件整体性能\n- 调整Mac平台下的图标大小\n- 同步功能添加对列表顺序调整的控制，确保手动调整位置后的列表与不同的电脑同步时，列表位置不会被还原\n- 优化歌单详情、排行榜名右键的播放按钮的播放机制，现在不用等待整个列表（多页时）加载完成才能播放了\n- 为播放详情页、桌面歌词添加延迟滚动，播放详情页略微减小已激活歌词的缩放大小及桌面歌词翻译大小\n- 修改右边控制按钮为windows风格\n- 更新了新年皮肤的背景与配色，欢迎体验~\n\n### 修复\n\n- 修复kw源某些歌曲的歌词提取异常的问题\n\n### 变更\n\n- 现在使用繁体中文语言时将不再自动转换歌词，转换行为将由上面新增的转换开关控制\n\n### 移除\n\n- 移除我的列表右键菜单的“上移、下移列表”功能，调整改用新增的拖动功能去调整位置\n\n### 其他\n\n- 升级vue到 3.x\n\n## [1.15.3](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.2...v1.15.3) - 2021-11-21\n\n### 修复\n\n- 修复设置-控制按钮位置选项与下载歌词编码格式选项命名冲突导致选项显示异常的问题\n- 修复播放下载列表时存在失效的歌曲会导致切歌不准确的问题\n- 修复潜在的音乐加载超时不会切歌的问题\n- 修复因kw源歌词接口停用导致该源歌词获取失败的问题\n\n## [1.15.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.3...v1.15.2) - 2021-11-09\n\n### 其他\n\n- 降级electron到v13.4.0（这修复了windows 7下播放歌曲时软件会崩溃的问题）\n\n## [1.15.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.15.0...v1.15.1) - 2021-11-09\n\n### 优化\n\n- 优化我的列表、下载列表等列表的滚动流畅度\n- 优化下载功能的批量添加、删除、暂停任务时的流畅度，现在进行这些操作应该不会再觉得卡顿了\n- 支持启动软件时恢复播放下载列表里的歌曲\n- 添加媒体播放进度条的信息设置\n\n### 修复\n\n- 修复某些情况下获取URL失败时会意外切歌的问题\n- 修复了某些情况下会列表同步失败，导致连接断开无限重连或一直卡在 `syncing...` 的问题\n- 修复列表数据过大导致同步失败的问题\n\n### 其他\n\n- 更新electron到v15.3.1（这修复了媒体控制失效的问题）\n\n## [1.15.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.14.1...v1.15.0) - 2021-10-29\n\n### 新增\n\n- 添加黑色托盘图标\n- 自定义源新增`version`字段，新增`utils.buffer.bufToString`方法\n\n### 优化\n\n- 大幅优化我的列表、下载、歌单、排行榜列表性能，现在即使同一列表内的歌曲很多时也不会卡顿了\n- 优化列表同步代码逻辑\n- 优化开关评论时的动画性能\n- 优化进入、离开播放详情页的性能\n- 兼容桌面歌词以触摸的方式移动、调整大小\n- 调整图标尺寸\n\n### 修复\n\n- 修复kg源的歌单链接无法打开的问题\n- 修复同一首歌的URL、歌词等同时需要换源时的处理问题\n\n### 其他\n\n- 更新 Electron 到 v15.3.0\n\n## [1.14.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.14.0...v1.14.1) - 2021-10-04\n\n### 修复\n\n- 修复我的列表搜索无法搜索小括号、中括号等字符的问题\n- 修复v1.14.0出现的备份与恢复功能备份的数据无法恢复的问题，同时兼容使用v1.14.0导出的存在问题的数据\n\n## [1.14.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.13.0...v1.14.0) - 2021-10-02\n\n### 新增\n\n- 新增歌词简体中文转繁体中文，当软件语言被设置为繁体中文后，播放歌曲的歌词也将自动转成繁体中文显示\n- 新增单个列表导入/导出功能，可以方便分享歌曲列表，可在右击“我的列表”里的列表名后弹出的菜单中使用\n- 新增删除列表前的确认弹窗，防止误删列表\n- 新增歌词文本选择复制功能，可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式，选择完成后可鼠标右击或者使用系统快捷键复制\n- 新增重复歌曲列表，可以方便移除我的列表中的重复歌曲，此列表会列出目标列表里歌曲名相同的歌曲，可在右击“我的列表”里的列表名后弹出的菜单中使用\n\n### 修复\n\n- 修复mg排行榜无法加载的问题\n- 修复点击播放详情页的进度条跳进度时会出现偏移的问题\n- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题\n- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题\n- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题\n\n## [1.13.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.2...v1.13.0) - 2021-09-05\n\n如果你喜欢并经常使用洛雪音乐，并想要第一时间尝鲜洛雪的新功能，可以加入测试企鹅群768786588，\n注意：测试版的功可能会不稳定，打算潜水的勿加。\n\n### 新增\n\n- 歌曲搜索框新增清理按钮，点击此按钮可以清理搜索框并返回初始搜索界面\n- 新增“下载的歌词文件编码格式”设置，默认下载的歌词编码仍是`UTF-8`，对于某些在设备(如车机)上出现歌词中文乱码的用户可以尝试选择以`GBK`编码格式保存歌词文件\n- 新增设置-桌面歌词-歌词字体设置，此设置可用于设置桌面歌词的字体（已知的问题：Windows 7 下可能会出现字体列表为空的情况，这是当前系统的 Powershell 版本小于5.1导致的，请自行**尝试**看常见解决）\n\n### 优化\n\n- 支持网易源“我喜欢”歌单以注入token的方式打开。由于网易源的“我喜欢”歌单需要登录才能打开（若你看不懂后半句就去阅读 常见问题-无法打开外部歌单），现若想要打开此类歌单，需要在歌单链接后面拼上 `###` 再加上有效的token，拼接格式：`[id|url]###token`，例子（最后面的xxxxxx替换成你的token）：`https://music.163.com/#/playlist?id=123456&userid=123456###xxxxxx`\n- 软件内快捷键的最小化触发时，如果已启用托盘，则隐藏程序，否则最小化程序\n\n### 修复\n\n- 修复某些情况下同步功能会导致切歌混乱的问题\n- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题\n\n## [1.12.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.1...v1.12.2) - 2021-08-11\n\n### 修复\n\n- 修复播放下载列表的歌曲时切歌的问题\n- 修复播放下载列表的歌曲时歌词无法显示的问题\n- 修复下载列表稍后播放功能无效的问题\n- 修复同步服务器启动失败时，关闭同步服务不会清空失败信息的问题\n\n## [1.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.0...v1.12.1) - 2021-08-08\n\n### 修复\n\n- 修复随机播放下无法切歌的问题\n\n## [1.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.11.0...v1.12.0) - 2021-08-08\n\n### 新增\n\n- 新增局域网同步功能（实验性，首次使用前建议先备份一次列表），此功能需要配合PC端使用，移动端与PC端处在同一个局域网（路由器的网络）下时，可以多端实时同步歌曲列表，使用问题请看\"常见问题\"。\n\n### 优化\n\n- 添加播放器对系统媒体控制与显示的兼容处理，现在在windows下的锁屏界面可以正确显示当前播放的音乐信息及切换歌曲了\n\n### 修复\n\n- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注：现在可以加载1000+首歌曲的歌单，但出于未知原因会导致部分歌曲无法加载（可能是无版权导致的），目前酷狗码仍然最多只能加载500首歌\n- 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题\n\n## [1.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.2...v1.11.0) - 2021-07-18\n\n### 新增\n\n- 添加 win arm64 架构的安装包构建\n- 新增“添加歌曲到列表时的位置”设置，可选项为列表的“顶部”与“底部”\n\n### 优化\n\n- 优化网络请求，尝试去解决无法连接服务器的问题\n- 优化mg源打开歌单的链接兼容\n\n### 修复\n\n- 修复mg源搜索失效的问题\n\n### 移除\n\n- 因wy源的歌单列表已没有“最新”排序的选项，所以现跟随移除wy源歌单列表按“最新”排序的按钮\n\n### 变更\n\n- 添加歌曲到列表时从原来的底部改为顶部，若你想要将你的列表歌曲顺序反转以适应这一变更，可先按住`shift`键的情况下点击列表的最后一首歌，然后再点击列表的第一首歌，完成倒序选中，最后随便右击列表的任意一首歌，在弹出的菜单中选择调整顺序，在弹出框输入1后确定即可反转列表。\n若你想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项。\n\n### 其他\n\n- 更新electron到v13.1.7\n\n## [1.10.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.1...v1.10.2) - 2021-05-25\n\n### 修复\n\n- 修复企鹅音乐搜索歌曲没有结果的问题\n\n## [1.10.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.0...v1.10.1) - 2021-05-25\n\n### 修复\n\n- 修复企鹅音乐搜索歌曲没有结果的问题\n- 修复播放在空的歌单列表点击播放全部时报错的问题\n\n## [1.10.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.9.0...v1.10.0) - 2021-05-19\n\nlx music移动端已经发布了，使用习惯仍跟桌面版一样，不过功能、界面仍比较简单，有兴趣的可以去体检一下，项目地址：\nhttps://github.com/lyswhut/lx-music-mobile#readme\n\n### 新增\n\n- 排行榜界面添加播放、收藏整个排行榜功能，可以右击排行榜名字后，在弹出的右键菜单中使用。注：收藏、播放存在分页的排行榜时需等待操作完成后才能切换排行榜，不然会导致操作中断。\n- 新增Mac arm64位dmg包的构建\n\n### 修复\n\n- 修复全局快捷键对桌面歌词无效的问题\n- 修复快捷键设置框内的提示问题\n- 修复在当前正常播放的列表中使用稍后播放功能时，播放完后稍后播放的歌曲后不会恢复原来播放位置播放的问题\n- 修复kw部分歌单无法打开的问题\n- 修复wy源的歌曲音质匹配问题\n- 修复mg源歌单标签、排行榜歌曲列表无法加载的问题\n- 修复了一个歌曲下载失败时不会跳过任务的问题\n\n### 其他\n\n- 更新 Electron 到 12.0.8\n\n## [1.9.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.8.2...v1.9.0) - 2021-04-24\n\n### 新增\n\n- 新增启动参数`-dhmkh`，此参数将禁用Chromium的Hardware Media Key Handling特性，用于解决漫步者部分型号耳机与本程序冲突导致耳机意外关机的问题\n- 新增Windows arm64位免安装版的构建\n- 新增黑色皮肤“黑灯瞎火”，有关于皮肤配色的建议欢迎反馈\n- 新增自动换源下载功能，默认关闭，当无法从歌曲的原始源下载时，将尝试切换到其他源下载，注：此功能不100%保证换源后的歌曲版本与原版一致\n\n### 优化\n\n- 程序启动时对数据文件做读取校验，数据出现损坏时自动备份损坏的数据，若出现数据读取错误的弹窗并出现我的列表丢失时可到GitHub或加群反馈\n- 当设置-代理启用，但主机地址为空的时，将不再使用代理配置进行网络连接，并且在离开设置界面时自动禁用代理\n- 优化歌曲自动换源匹配\n- 分离歌词与歌曲列表信息的保存，以减小列表列表文件损坏的几率\n- 兼容打开咪咕移动端分享的歌单链接，添加打开歌单的信息显示\n\n### 修复\n\n- 修复备份与恢复功能在恢复数据时某些设置不立即生效的问题\n- 修正设置页“搜索设置”部分内容的缩进显示问题\n- 修复正在播放“稍后播放”的歌曲时，对“稍后播放”前播放的列表进行添加、删除操作会导致切歌的问题\n\n## [1.8.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.8.1...v1.8.2) - 2021-03-09\n\n### 修复\n\n- 修复歌曲ID存储变更导致酷狗图片获取失败的问题\n- 修复收藏的在线列表id迁移保存出错的问题\n\n## [1.8.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.8.0...v1.8.1) - 2021-03-07\n\n### 修复\n\n- 修复歌词翻译的主题颜色适配问题\n\n## [1.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.1...v1.8.0) - 2021-03-07\n\n### 新增\n\n- 新增设置-其他-列表缓存信息清理功能，注：此功能一般情况下不要使用\n- 新增启动参数`-play`，可以在启动软件时播放指定歌单，使用方法看Readme.md的\"启动参数\"部分\n- 新增逐字歌词播放，默认开启，可到设置界面关闭，注：本功能目前仅对酷狗源的歌曲有效\n- 新增自定义源功能，源编写规则可以去常见问题查看\n\n### 优化\n\n- 允许播放除了搜索列表以外的所有歌曲，即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注：该功能的原理是尝试自动切换到其他源播放，所以不一定会播放成功，特别是对于那些独家的资源\n- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示；现在在收藏单首歌曲时，若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说，在添加单首歌曲时若列表名是灰色，则证明当前歌曲已在那个列表中\n- 将歌词翻译放到原文的下方，同时新增当前播放翻译的高亮功能\n\n### 移除\n\n- 移除虾米源。注：虽然已移除该源，但仍可尝试去播放之前添加的歌曲，虽然不一定会成功\n\n### 修复\n\n- 修复音乐搜索列表的稍后播放功能无效的问题\n- 修复搜索列表双击不支持播放的源时会导致切歌的问题\n- 修复歌单列表加载失败时无法进入歌单打开界面的问题\n- 修复mg源歌单列表无法加载的问题\n- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题\n- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题\n- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题\n\n### 变更\n\n- 歌词翻译显示功能修改为默认关闭，注：此变更仅影响首次安装软件的用户\n\n### 其他\n\n- 更新electron到v9.4.4\n\n## [1.7.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.7.0...v1.7.1) - 2021-01-30\n\n### 修复\n\n- 修复非透明模式下右侧滚动条无法拖动的问题\n- 修复MAC下xm音乐滑块验证问题\n\n## [1.7.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.1...v1.7.0) - 2021-01-30\n\n### 新增\n\n- 搜索界面新增搜索状态的提示\n- 新增“稍后播放”功能，可在歌曲列表右键菜单使用\n- 新增“记住播放进度”功能的控制，该功能默认不再开启，可到播放设置-记住播放进度开启\n\n### 优化\n\n- 优化播放歌曲换源匹配\n- 优化设置界面设置项的展示\n\n### 修复\n\n- 修复快速切换歌曲时, 会出现播放的歌曲和界面展示的歌曲不一致的问题\n- 修复了一个由版本更新日志显示导致的潜在远程代码执行攻击漏洞，该漏洞影响v1.6.1及之前的所有版本，请务必更新到最新版本\n- 修复xm搜索源验证问题\n\n### 其他\n\n- 更新electron到9.4.2\n\n## [1.6.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.6.0...v1.6.1) - 2021-01-13\n\n### 优化\n\n- 改进自动换源时的歌曲匹配\n\n### 修复\n\n- 修复某些情况下自动换源的时间过长时会终止换源自动切歌的问题\n- 修复自动换源导致的搜索列表每页变成10条数据的问题\n- 降级electron到9.3.3修复部分系统没有声音的问题\n\n## [1.6.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.5.0...v1.6.0) - 2021-01-10\n\n### 新增\n\n- 我的列表右键菜单新增列表排序功能，可调整单曲、多选后的歌曲的顺序。注意：多选排序还将会按照选中歌曲时的顺序排序\n- 添加鼠标提示的自动关闭功能，鼠标长时间（目前是10秒）不动时鼠标提示将会自动关闭\n- 添加鼠标指向歌曲封面的提示（对于进度条左边的歌曲封面，你可能不知道的操作->右击在“我的列表”定位当前播放的歌曲）\n- 隐藏播放详情页按钮添加快速隐藏详情页提示（你可能不知道的操作->在播放详情页内的任意非窗口可拖动区域右键双击可以快速隐藏详情页）\n- 添加桌面歌词字体、透明度调整按钮微调提示（你可能不知道的操作->对于字体、透明度可右击微调）\n- 我的列表右键菜单添加搜索当前歌曲功能\n- 新增`-dha`参数，添加此启动参数将禁用硬件加速启动（Disable Hardware Acceleration），窗口显示有问题时可以尝试添加此参数启动，Linux系统的界面显示有问题时可尝试添加此参数启动，若不行可尝试添加`-dt`参数启动\n- 新增播放自动换源功能~\n\n### 变更\n\n- `-nt`参数更名为`-dt`（Disable Transparent），目前原来的`-nt`参数仍然可用，但将在后续的版本中移除\n\n### 修复\n\n- 修复恢复上次播放的歌曲时在随机播放模式下不把恢复播放的歌曲放入已播放队列的问题（该问题会导致随机模式下会导致未播放完整个列表前就会再次随机到该歌曲，以及无法通过上一曲切回该歌曲）\n- 修复音乐嵌入的封面在 Mac 系统无法显示的问题\n- 修复`-dt`（原来的`-nt`）启动参数不真正生效的问题\n\n## [1.5.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.4.1...v1.5.0) - 2020-12-13\n\n### 新增\n\n- 直接从歌单详情收藏的列表新增同步功能。注意：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表\n\n### 优化\n\n- 优化软件启动时恢复上一次播放的歌曲进度功能\n\n### 修复\n\n- 修复MAC平台上下载歌曲封面嵌入无法显示的问题\n- 修复MAC平台首次运行软件最小化、关闭控制按钮默认在右边的问题\n- 修复酷狗源的某些歌曲没有专辑字段导致的列表加载失败问题\n- 修复某些酷狗源歌单链接无法打开的问题\n\n## [1.4.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.4.0...v1.4.1) - 2020-11-25\n\n\n### 修复\n\n- 修复有歌词翻译与无歌词的音乐间切换会导致歌词翻译残留显示的问题\n- 修复歌曲URL过期时，等待刷新URL的自动切换歌曲时间间隔太短的问题\n- 修复某些电脑上的某些歌曲没有声音的问题（升级Electron9.3.4导致的，现降级到9.3.3）\n\n## [1.4.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.3.0...v1.4.0) - 2020-11-21\n\n### 新增\n\n- 托盘菜单新增显示、隐藏主界面选项，为Linux、MAC版添加托盘菜单\n- 新增播放进度信息保存\n\n### 优化\n\n- 移除kg源的歌词文件开头的空白字符串\n\n### 修复\n\n- 修复专辑图片无法嵌入的问题\n- 修复播放状态栏切换“上一首”歌曲按钮提示错误的问题\n- 修复移动单首歌曲时，如果目标列表存在该歌曲，会导致将源列表与目标列表里的目标歌曲移除\n- 修复kg源歌曲信息带有单引号等特殊字符被转义的问题\n\n## [1.3.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.2.2...v1.3.0) - 2020-11-01\n\n### 新增\n\n- 播放详情页新增歌曲评论加载显示（某些平台暂不支持显示子评论）\n\n### 优化\n\n- 修改播放详情页的歌曲图片的显示效果\n\n### 修复\n\n- 修复小芸源音乐搜索结果最多只有20条搜索结果的问题\n\n## [1.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.2.1...v1.2.2) - 2020-10-18\n\n### 修复\n\n- 降级 Electron 到 9.x.x 版本修复 Linux 版桌面歌词窗口变白的问题\n\n## [1.2.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.2.0...v1.2.1) - 2020-10-18\n\n### 优化\n\n- Linux版的软件界面默认使用圆角与阴影，顺便修复了桌面歌词窗口变白的问题，已在Ubuntu 18.10测试正常，若显示异常可尝试添加`-nt`参数启动\n\n### 修复\n\n- 修复聚合搜索的分页问题\n- 修复代理输入框输入的内容不生效的问题\n\n## [1.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.1.1...v1.2.0) - 2020-09-30\n\n提前祝大家中秋&国庆快乐~\n\n### 新增\n\n- 播放控制栏开启/关闭桌面歌词按钮 新增右击按钮时锁定/解锁桌面歌词功能\n\n### 优化\n\n- 优化我的列表滚动条位置的保存逻辑\n- 更新设置-备份与恢复功能的描述\n- 优化软件内鼠标悬停的提示界面\n\n### 修复\n\n- 修复桌面歌词窗口不允许拖出桌面之外的位置计算偏移Bug\n- 修复网易云KTV嗨榜无法加载的问题\n- 修复初始化搜索历史列表功能\n- 修复重启软件后试听列表与收藏列表无法恢复上次的滚动位置的问题\n- 修复歌曲封面无法嵌入的Bug\n- 修复酷狗歌词格式问题\n- 修复关闭切换动画时从搜索候选列表点击内容无效的问题\n\n### 其他\n\n- 更新 Electron 到 v10.1.3\n\n## [1.1.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.1.0...v1.1.1) - 2020-09-19\n\n### 修复\n\n- 修复某些情况下桌面歌词不会播放的问题\n\n## [1.1.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.0.1...v1.1.0) - 2020-09-18\n\n### 新增\n\n- 在歌单详情界面新增播放当前歌单按钮、收藏歌单按钮，注：播放歌单不会将歌曲添加到试听列表\n- 新增`不允许将歌词窗口拖出主屏幕之外`的设置项，默认开启，在连接多个屏幕时想要拖动到其他屏幕时可关闭此设置\n- 新增大部分平台的歌词翻译，感谢 @InoriHimea 提供的[krc解码算法](https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784)\n- 新增`显示歌词翻译`设置，默认开启，仅支持某些平台，注：无论该设置是否开启，嵌入或下载歌词时都不会带上翻译\n- 新增`显示切换动画`设置，默认开启，关闭时将基本禁用软件内的所有切换动画\n- 播放状态栏新增桌面歌词的开关、播放模式的切换、歌曲的收藏按钮，Thanks to @andylow for the [icon](https://github.com/lyswhut/lx-music-desktop/pull/309)!\n\n### 修复\n\n- 修复使用全局快捷键还原窗口时，窗口没有获取焦点的问题\n- 修复我的列表搜索对最后一个字符的匹配问题\n- 修复窗口在`较小`模式下最小化/关闭按钮不居中的问题\n\n### 优化\n\n- 桌面歌词当前播放行改为上下居中\n- 为区分静音状态，静音时音量条会变淡，调整音量条时将会取消静音\n- 优化随机播放机制，现在通过`下一曲`切换歌曲时，直到播放完整个列表之前将不会再随机到之前播放过的歌曲，并且通过`上一曲`可以正确播放上一首歌曲\n- 当下载目录没有写入权限时将显示没有写入权限的提示\n\n### 移除\n\n- 移除默认的全局声音媒体快捷键接管\n- 移除对百度音乐的支持，因百度音乐原有的大部分API失效，而且该平台相对其他平台来说音乐太少了，可有可无，以后再看情况恢复\n\n### 其他\n\n- 更新electron到 10.1.2\n\n## [1.0.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.0.0...v1.0.1) - 2020-07-25\n\n### 优化\n\n- 对我的列表歌曲搜索结果进行相似度排序\n\n### 修复\n\n- 修复在 Windows 系统下缩放比非100%时，拖动桌面歌词会自动加大桌面歌词窗口的问题\n\n## [1.0.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.18.2...v1.0.0) - 2020-07-24\n\n### 新增\n\n- 新增`rpm`、`pacman`包的构建（未测试可用性）\n- 新增因系统音频设备列表改变导致的当前音频输出设备改变时是否暂停播放的设置，默认关闭\n- 新增歌曲列表右击菜单\n- 新增自定义列表，创建列表的按钮在表头`#`左侧，鼠标移上去才会显示；编辑列表名字时，按`ESC`键可快速取消编辑，按回车键或使输入框失去焦点即可保存列表名字，右击列表可编辑已创建的列表，“试听列表”与“我的收藏”两个列表固定不可编辑\n- 改变排行榜布局，新增更多排行榜\n- 新增我的列表右键菜单复制歌曲名选项\n- 新增桌面歌词，默认关闭，可到设置或者托盘菜单开启（建议使用全局快捷键控制）；调整字体大小、透明度时，鼠标左击按钮正常调整，右击微调；**Windows 7未开启Aero效果时桌面歌词会有问题**，详情看常见问题解决；Linux版桌面歌词有问题，以后再尝试优化；\n- 新增“清热板蓝”皮肤\n- 新增软件最小化、关闭按钮位置设置，MAC版默认为左边，非MAC为右边，不想用默认的可到设置修改\n- 新增快捷键设置，软件内快捷键默认开启，全局快捷键默认关闭（注：若想开启蓝牙耳机切歌需开启全局快捷键，当快捷键被中划线划掉时，表示当前快捷键被其他程序占用导致注册失败）\n- 新增首次运行时自动根据当前系统使用的语言设置软件显示的语言\n- 新增歌词区域的触摸板、鼠标滚轮等对歌词滚动的支持\n- 为了方便支持正版资源，歌曲列表右击菜单新增跳转到当前歌曲源官方详情页菜单（注意：在本版本之前添加的虾米源歌曲无法跳转详情页，需要移除后重新搜索添加）\n- 新增我的列表内歌曲搜索，在我的列表按`ctrl+f`将显示搜索框；鼠标滑过或键盘上下方向键选择搜索结果；鼠标点击或按回车键定位选中的歌曲；按`ctrl`键的情况下鼠标点击或按回车键确认定位歌曲时，将会在定位歌曲结束后播放该歌曲（搜索框激活的情况下按`esc`可快速清空搜索框/关闭搜索框）\n- 新增托盘图标样式设置，可到设置-其他切换\n- 新增开关下载功能控制，默认关闭，可到设置-下载设置开启\n- 新增将歌词嵌入音频文件中，默认关闭，可到设置-下载设置开启\n- 新增当列表文件损坏时对损坏文件的备份，若出现该情况可打开`%HOMEPATH%\\AppData\\Roaming\\lx-music-desktop`找到`playList.json.bak`尝试手动修复列表文件，列表文件以`JSON`格式存储\n- 新增在歌单详情列表按退格（Backspace）键可快速返回歌单列表\n\n### 优化\n\n- 改进歌曲切换时的歌词滚动效果\n- 优化批量添加、删除播放列表的歌曲操作逻辑，大幅提升批量添加、删除列表歌曲的流畅度\n- 改进歌单列表展示\n- **改进聚合搜索的搜索结果排序**，修复当某些源搜索失败时导致其他源无法显示结果的问题，现在聚合搜索已达到最初的理想效果，为了使排序更精确，**建议同时输入 歌曲名 歌手名 搜索**（歌曲名在前歌手名在后），欢迎体验~！\n- 压缩备份数据文件大小\n\n### 修复\n\n- 修复按住`Ctrl`等键触发多选机制时不松开按键的情况下切换到其他窗口后再松开按键，这时切回软件不按按键都处在多选模式的问题\n- 修复Linux版开启托盘无法退出的问题\n- 修复某些情况下可能导致的音源输出问题\n- 修复某些情况下无法开始下载任务的问题\n- 修复 tab 组件边框溢出问题\n- 修复错误更新试听列表外的歌曲时间的问题\n- 修复网易音乐源歌单、排行榜歌曲列表加载显示的数量与实际不对的问题，同时支持加载大于1000首歌的歌单（歌曲大于1000首会分页），注意：目前软件一下子显示太多歌曲时会卡顿，不建议在同一列表内添加太多歌曲\n- 修复歌曲图片链接没有扩展名的情况下无法嵌入图片的问题\n- 修复无法检测最新版本时弹窗提示的显示\n- 修复某些情况下从托盘还原窗口后无法操作的问题\n- 修复Linux下无法`ctrl+a`全选的问题\n- 修复主题背景图片覆盖不全的问题\n- 修复聚合搜索音源标签的皮肤配色问题\n\n### 更变\n\n- 修改设置-列表-是否显示歌曲源的默认设置为选中（该变更不影响之前的设置）\n- 移除浮动按钮，现在在多选完成后可鼠标右击随意一项在弹出的右键菜单中进行原来悬浮按钮的操作\n- 为了避免出现误会，现在下载弹窗中不可用的音质将直接隐藏\n- 更改初始设置的搜索设置为聚合搜索（该变更不影响之前的设置）\n\n### 其他\n\n- 更新 Electron 到 9.1.1\n\n## [0.18.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.18.1...v0.18.2) - 2020-05-02\n\n### 修复\n\n- 修复开启托盘时，可能导致无法自动更新的问题\n\n## [0.18.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.18.0...v0.18.1) - 2020-05-02\n\n### 优化\n\n- win下的托盘图标使用更大的图片\n- 加长软件协议的强制停留时间\n\n### 修复\n\n- 修复导入设置某些设置未立即生效的问题\n\n## [0.18.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.17.0...v0.18.0) - 2020-05-01\n\n### 新增\n\n- 新增FLAC格式音乐标签信息写入与封面嵌入（因128k以外的音质已失效，目前该功能用不上了）\n- 添加软件启动时是否自动聚焦搜索框的设置\n- 新增托盘设置，默认关闭，可到设置开启，感谢 @LasyIsLazy 提交的PR\n- 新增打开酷狗源用户歌单\n- 新增使用协议\n- 新增虾米音源\n- 新增新皮肤“粉妆玉琢”、“青出于黑”，可去体验下~\n- 新增“超大”、“巨大”窗口尺寸\n- 新增播放详情页（退出详情页可点击右上角退出按钮或者在播放详情页任意地方**鼠标快速右击两次**）\n\n### 优化\n\n- 略微加深音量条底色\n- 优化其他界面细节\n- 优化英语翻译，感谢 @CPCer\n- 优化程序的流畅度\n\n### 更变\n\n- 下载列表的歌曲下载、播放将随设置中的保存路径改变而改变，不再固定指向其初始位置\n- 移除列表多选框，现在多选需要键盘配合，想要多选前需按下`Shift`或`Ctrl`键然后再鼠标点击想要选中的内容即可触发多选机制，其中`Shift`键用于连续选择，`Ctrl`键用于不连续选择，`Ctrl+a`用于快速全选。例子一：想要选中1-5项，则先按下`Shift`键后，鼠标点击第一项，再点击第五项即可完成选择；例子二：想要选中1项与第3项，则先按下`Ctrl`键后，鼠标点击第一项，再点击第三项即可完成选择；例子三：想要选中当前列表的全部内容，键盘先按下`Ctrl`键不放，然后按`a`键，即可完成选择。用`Shift`或`Ctrl`选择时，鼠标点击未选中的内容会将其选中，点击已选择的内容会将其取消选择，若想全部取消选择，在不按`Shift`或`Alt`键的情况下，随意点击列表里的一项内容即可全部取消选择。(P.S：`Ctrl`键对应Mac OS上的`Command`键)\n- 现在进度条的封面图左击改为打开播放详情页，在列表定位歌曲改为右击\n\n### 修复\n\n- 修复网易源某些歌曲提示没有可播放的音质的问题\n- 修复下载管理刷新URL失败时不标记任务下载失败的问题\n- 修复列表导出的文字描述，感谢 @CPCer\n- 修复歌曲切换方式无法取消勾选的问题\n- 修复打开歌单详情的情况下切到其他界面再切回来报错的问题\n- 修正播放列表浮动按钮错误的文字提示\n\n### 移除\n\n- 因128k以外的音质失效，So 禁止所有128k外的音质下载\n\n### 其他\n\n更新 Electron 到 8.2.5\n\n\n## [0.17.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.16.0...v0.17.0) - 2020-03-15\n\n### 新增\n\n- 新增多语言设置，目前软件内置了简体中文、繁体中文、英语三种语言，欢迎提交PR翻译更多语言！\n- 新增无法打开外部歌单FAQ\n- 新增启动参数`search`，使用例子：`.\\lx-music-desktop.exe -search=\"突然的自我 - 伍佰\"`\n- 新增音频输出设置\n- 新增软件内的包括字体在内的界面内容大小调整，现在当窗口大小切换到“较小/大/较大”时，软件内的元素将会适当减小或加大，窗口大小的“小”与“中”内的元素将保持之前的大小暂不做改变\n- 新增音源别名，默认将显示别名，想要显示回原名可到设置切换（免责声明：别名仅是本软件用于描述各音源的标签，其名字归版权方所有）\n- 新增发现新版本更新失败弹窗的忽略提醒按钮，忽略提醒后，以后同一个版本再失败时将不会弹窗提醒，但仍可到设置-版本更新手动点开更新弹窗查看或恢复提醒\n- 新增热搜词，默认关闭，可到设置开启\n- 新增历史搜索记录，默认关闭，可到设置开启（右击单个历史记录标签可移除所点击的记录）\n\n### 优化\n\n- 优化月里嫦娥皮肤侧栏鼠标悬浮颜色\n- 优化播放进度条的动画效果\n- 现在添加下载任务时，后面添加的任务会在列表顶部插入\n- 优化歌单打开机制，现在歌单加载失败时会提示加载失败了，并且支持直接打开企鹅、酷我手机分享出来的歌单了\n- 优化右上角最小化/关闭按钮布局\n\n### 修复\n\n- 修复歌单详情处于加载状态时无法返回的问题\n- 修复鼠标右击复制列表内容时会复制音质标签的问题\n- 修复`0.6.2`及以前的版本导出的“所有数据”内的歌曲列表无法导入的问题\n- 修复下载列表在某些情况下无法取消全选的问题\n\n### 其他\n\n- 更新Electron到 8.1.1\n\n## [0.16.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.15.0...v0.16.0) - 2020-02-16\n\n### 新增\n\n- 允许选中列表内歌曲名、歌手名、专辑名内的文字，选中后可使用键盘快捷键进行复制\n- 新增在列表可选内容区域**鼠标右击**时自动复制列表已选文字的功能\n- 新增在搜索框**鼠标右击**时自动粘贴剪贴板的文本到搜索框中\n- 任务下载失败时将显示搜索按钮，方便在其他源搜索该歌曲\n\n### 优化\n\n- 优化木叶之村主题翻页器背景颜色\n- 优化各个主题音质标签颜色\n- 优化其他一些界面细节及用户交互效果\n\n### 修复\n\n- 修复启用透明窗口鼠标不穿透的bug\n- 修复大窗口时设置的音乐来源选项不换行的问题\n- 修复某些情况下暂停任务会自动开始任务的问题\n- 修复移除暂停、错误的任务时不删除未下载完成的文件的问题\n- 修复酷狗源歌单热门标签歌单列表无法加载问题\n- 修复QQ源歌单热门标签歌单列表无法加载问题\n\n### 其他\n\n- 更新electron到 8.0.1\n\n## [0.15.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.14.1...v0.15.0) - 2020-01-23\n\n洛雪提前祝大家新年快乐、身体健康、阖家幸福！\n\n### 修复\n\n- 修复歌曲下载列表无法加载的问题\n- 修复歌曲下载任务数大于最大下载任务数的问题\n- 修复某些情况下歌曲下载错误的问题\n- 修复下载列表数据没有被迁移直接被丢弃的问题\n\n## [0.14.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.14.0...v0.14.1) - 2020-01-22\n\n洛雪提前祝大家新年快乐、身体健康、阖家幸福！\n\n### 修复\n\n- 修复由于旧版配置文件迁移出错导致的软件界面无法显示的问题\n\n## [0.14.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.13.1...v0.14.0) - 2020-01-22\n\n洛雪提前祝大家新年快乐、身体健康、阖家幸福！\n\n### 新增\n\n- 新增各大平台歌单热门标签显示（显示在歌单界面的第一个下拉标签菜单中）\n- 恢复QQ音乐源128k音质试听\n- 新增不强制win7开启透明效果即可使用，但要配置运行参数`-nt`，例如：`.\\lx-music-desktop.exe -nt`，添加方法可自行百度“给快捷方式加参数”\n- 新增“新年快乐”主题，可自行切换体验\n\n### 优化\n\n- 减淡各个主题的歌曲列表分隔线颜色\n- 在线音乐列表音质标签优化，当歌曲有无损音质时隐藏高品质标签\n- 更新改进的歌词播放插件，现在歌词的播放显示将更准确\n\n### 修复\n\n- 修复咪咕源无法搜索的问题\n- 修复更新弹窗底部文字颜色没有适配当前主题颜色的问题\n- 修复导入设置窗口大小、代理设置不立即生效的问题\n- 修复在线音乐列表获取失败时无限循环请求的问题\n\n### 其他\n\n- 将软件设置与播放列表分离存储成两个文件\n- 更新 Electron 到 7.1.9\n\n## [0.13.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.13.0...v0.13.1) - 2019-12-16\n\n### 修复\n\n- 修复全局更新弹窗无法遮盖搜索框的问题\n\n### 其他\n\n- 由于electron 7.1.3 - 7.1.5 的自动更新功能存在Bug，现降级到7.1.2\n\n## [0.13.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.12.1...v0.13.0) - 2019-12-15\n\n### 新增\n\n- 新增搜索框搜索建议键盘上下方向键选择功能\n- 聚合搜索新增音源显示\n- 新增“离开搜索界面时清空搜索列表”设置选项，默认关闭，可到设置-强迫症设置开启\n\n### 优化\n\n- 优化“信口雌黄”皮肤配色\n\n### 修复\n\n- 修复存在弹出层时，搜索建议列表被弹出层覆盖的问题\n- 修复搜索、排行榜、歌单列表多选框从不定状态到选中的Bug\n\n### 移除\n\n- 因Q音接口失效，移除Q音源的试听与下载\n\n### 其他\n\n- 更新electron到7.1.5\n- 更新vue到2.6.11\n\n## [0.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.12.0...v0.12.1) - 2019-12-01\n\n### 优化\n\n- 优化定位歌曲时的列表滚动机制\n- 优化链接点击效果\n\n### 修复\n\n- 修复使用酷我源下载歌曲时，当歌曲无封面时下载报错的问题\n- 修复酷我源排行榜、歌单详情列表里的歌曲音质匹配问题（原来无论歌曲有无高品、无损都会显示有）\n- 禁止外部链接在软件内打开，将所有外部链接从默认浏览器打开\n\n### 其他\n\n- 更新electron到7.1.2\n\n## [0.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.11.0...v0.12.0) - 2019-11-17\n\n由于新下载库仍然没有完成，但下载功能已经可用，so 移除之前使用的第三方下载库，暂时把新下载库的下载模块直接加入本程序，若出现下载问题欢迎反馈！\n\n### 新增\n\n- 新增下载功能对代理设置的支持，现在若在软件设置了代理服务器，下载功能也将会走代理网络了\n\n### 优化\n\n- 新下载模块将对恢复下载的任务进行字节校验，用于解决下载进度超过100%后仍然下载的问题\n- 注意：目前仍然无法暂停处于**链接获取**状态中的任务\n\n### 修复\n\n- 修复Linux deb版本`.desktop`桌面文件缺少图标的问题，新增中文名称显示、软件分类，感谢@lowy的反馈！\n- 修复下载列表歌曲状态分类列表操作Bug\n- 修复歌曲封面下载失败时仍然执行嵌入封面操作导致报错的问题\n- 跳过重复添加**相同歌曲名与扩展名的歌曲**，例如你之前下载了A歌曲的128k音质，现在想要下载它的320k音质，但由于两者都是MP3格式，会因为重名导致之前的128k音质被覆盖但列表中仍然显示两种音质的问题（但实际上都是指向后面的320k音质）\n\n## [0.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.10.0...v0.11.0) - 2019-11-10\n\n### 新增\n\n- 新增歌曲缓冲定时器，尝试用于解决网络正常但是歌曲缓冲过久的问题\n- 新增下载管理的任务状态分类\n- 添加**杀毒软件提示有病毒或恶意行为**的说明，可到**常见问题**拉到最后查看（常见问题可在开源地址找到）\n\n### 优化\n\n- 优化更新弹窗机制及其内容描述，对于可以自动更新的版本，现在可以看到软件的下载进度了\n\n## [0.10.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.9.1...v0.10.0) - 2019-11-02\n\n#### 优化\n\n- 大幅减少程序**播放时**对CPU与GPU的使用，经测试CPU使用减少60%以上，GPU使用减少90%以上，这应该能解决MAC系统上的温度上涨的问题\n\n#### 修复\n\n- 修复酷我源**搜索提示**、**排行榜**无法获取的问题\n- 修复咪咕源无法播放的问题\n\n## [0.9.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.9.0...v0.9.1) - 2019-10-27\n\n#### 修复\n\n- 修复没有配置文件时程序启动出错的问题\n\n## [0.9.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.8.2...v0.9.0) - 2019-10-27\n\n#### 新增\n\n- 新增窗口大小设置，若觉得软件窗口小可以到设置页调大点\n- 新增定位当前播放歌曲，点击播放栏左侧的**歌曲图片**可在播放列表定位当前播放的歌曲（该功能对播放下载列表的歌曲无效）\n\n#### 修复\n\n- 修复搜索提示失效的问题\n- 修复从歌单或列表点击搜索按钮搜索目标歌曲时，搜索框未聚焦仍然弹出候选搜索列表的问题\n\n## [0.8.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.8.1...v0.8.2) - 2019-10-20\n\n#### 修复\n\n- 兼容旧版酷我源搜索列表过滤128k音质的bug（注：0.8.1版本仅修复了酷我源的歌曲过滤问题，该修复仅对以后添加的歌曲有效，如果是之前添加的歌曲仍会出现这个问题，现修复对之前旧列表数据的兼容处理）\n\n## [0.8.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.8.0...v0.8.1) - 2019-10-20\n\n#### 修复\n\n- 修复酷我源搜索歌曲结果未添加128k音质导致播放128k音质时显示“该歌曲没有可播放的音频”的问题\n\n## [0.8.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.7.0...v0.8.0) - 2019-10-19\n\n#### 新增\n\n- 新增网易云源歌曲搜索\n- 新增网易云源歌单\n- 新增各平台通过输入歌单链接或歌单ID打开歌单详情列表，目前只适配了**网页版歌单链接**，其他方式的歌单链接可能无法解析，但你可想办法获取歌单ID后输入打开。注：各平台歌单ID均为纯数字，若遇到链接里存在歌单ID但无法解析的歌单链接，可以到GitHub提交issue或发送邮件或加群830125506反馈！\n- 新增音量调整滑动功能，现在支持鼠标左右拖动调整音量了\n\n#### 优化\n\n- 优化搜索框搜索体验\n- 优化音量条交互视觉效果\n- 缓存歌单详情列表数据\n\n#### 修复\n\n- 修复QQ源歌单无法翻页Bug\n- 修复默认列表没有创建时无法显示收藏列表的Bug\n- 修复网易云128k直接试听\n- 修复歌曲音质不存在时仍然播放或下载的Bug\n- 修复调整音量时，调整的位置与鼠标点击的位置不一致的问题\n\n## [0.7.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.6.2...v0.7.0) - 2019-10-07\n\n#### 新增\n\n- 新增“我的收藏”本地播放列表\n- 新增缓存清理功能，可到**设置-其他**查看与清理软件缓存\n- 新增QQ音乐源搜索\n- 新增咪咕源搜索\n- 新增咪咕源歌单\n- 新增咪咕源排行榜\n- 新增我的音乐列表歌曲源显示，默认关闭，可到**设置-列表设置**开启\n\n#### 优化\n\n- 优化选择框动画效果\n- 尝试优化选我的音乐列表内容很多时多选的卡顿问题\n\n#### 修复\n\n- 修复列表延迟显示的Bug\n- 修复QQ音源128k音质试听\n\n## [0.6.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.6.1...v0.6.2) - 2019-10-01\n\n祝贺祖国成立70周年~！\n\n#### 新增\n\n- 新增QQ音乐源歌单\n\n#### 修复\n\n- 修正火影皮肤名字\n- 修复当试听列表为空时，无法切到其他界面的Bug\n- 修复百度源搜索结果为空时的接口处理Bug\n- 恢复**酷狗**其他音质播放\n\n## [0.6.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.6.0...v0.6.1) - 2019-09-28\n\n### 新增\n\n- 新增试听列表**滚动条位置恢复**设置（可自动恢复到上次离开时的列表滚动位置），本功能默认开启，若不需要可到设置-列表设置将其关闭\n- 新增 **《海贼王》** 皮肤，喜欢个性化的可以试试~\n\n### 优化\n\n- 新增DNS解析缓存，加快请求速度\n- 优化代码逻辑，减少软件对系统资源的占用\n- 优化新版本信息检测，尽量减少弹出版本获取失败弹窗弹出的概率\n- 优化下拉列表动画效果\n\n### 修复\n\n- 修复请求超时的逻辑处理Bug，尝试修复请求无法取消导致的正在播放的歌曲与界面显示的信息不一致的问题\n- 修复其他一些小Bug\n\n### 移除\n\n- 移除 `192k` 音质\n- 移除酷我音源 `ape` 音质，无损推荐 `flac` 格式\n\n## [0.6.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.5...v0.6.0) - 2019-09-21\n\n### 新增\n\n- 新增音乐**聚合搜索**，目前支持酷我、酷狗、百度源搜索\n- 新增代理功能\n\n### 优化\n\n- 优化从《梦里嫦娥》皮肤切换到其他皮肤时侧栏动画的切换效果\n\n### 修复\n\n- 修复试听列表没有歌曲时会显示列表加载中的Bug\n- 修复切换歌单列表详情时的UI Bug\n\n## [0.5.5](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.4...v0.5.5) - 2019-09-13\n\n### 新增\n\n- 月是故乡明，祝大家中秋快乐🥮~~新增个性皮肤 **《月里嫦娥》**，时间仓促，皮肤还不是很完善，可以试试喜不喜欢~😉\n- 新增 MAC 版本退出快捷键支持\n- 新增点击播放器中的歌曲标题可以复制标题的功能（遇到好听的歌曲方便分享）\n\n### 修复\n\n- 修复 MAC 系统下软件关闭时再次从 dock 打开时报错的Bug\n- 修复下载的歌曲文件名中包含命名规则不允许的符号时下载失败的问题（若歌曲名包含这些符号会自动将其移除）\n- 修复 MAC 版本不能复制粘贴的问题\n\n## [0.5.4](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.3...v0.5.4) - 2019-09-09\n\n### 移除\n\n- 下载的FLAC文件在修改歌曲信息后，软件无法播放，但使用本地播放器可以播放\n- 为了稳妥起见，暂时移除FLAC格式的meta信息修改\n- MP3格式无此问题\n\n## [0.5.3](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.2...v0.5.3) - 2019-09-09\n\n### 优化\n\n- 更新所有依赖包到最新\n\n### 修复\n\n- 修复试听酷狗源的音乐仍然获取320k音质导致获取失败的Bug\n\n## [0.5.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.1...v0.5.2) - 2019-09-09\n\n### 新增\n\n- 新增强迫症设置-离开搜索界面时是否清空搜索框\n- 设置-关于板块新增常见问题链接\n- 歌单左上角的分类按钮添加一个**向下图标**，方便识别该按钮为下拉框（该按钮可选择歌单类型，请自行尝试）\n\n### 优化\n\n- 略微优化最小化按钮字符\n- 优化试听列表的加载体验，当歌曲数过多时列表将延迟加载\n\n### 修复\n\n- 修复下载管理的一些Bug\n\n### 移除\n\n- 因接口失效，移除网易云音源，酷狗音源仅支持播放128k音质\n\n## [0.5.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.5.0...v0.5.1) - 2019-09-05\n\n### 新增\n\n- 新增右上角最小化/关闭按钮鼠标滑过符号\n- 新增下载列表定位文件按钮\n\n### 修复\n\n- 修复百度源歌单全部分类无法加载的问题\n- 修复更新弹窗无法弹出的问题\n\n## [0.5.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.4.0...v0.5.0) - 2019-09-05\n\n### 新增\n\n- 新增**封面嵌入**（默认开启，可到设置-下载设置关闭）\n- 新增**歌词下载**（默认关闭，可到设置-下载设置开启）\n- 新增单例应用功能（实现软件单开功能，禁止软件多开）\n\n### 优化\n\n- 优化歌单列表动画\n\n### 修复\n\n- 修复歌单无法翻页的问题\n- 修复在某些情况下，添加下载歌曲导致下载列表崩溃的问题\n- 修复版本更新弹窗Bug\n- 修复酷狗歌单推荐歌单出现在其他分类中的Bug\n\n## [0.4.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.5...v0.4.0) - 2019-09-04\n\n\n### 新增\n\n- 新增**歌单**功能，目前支持酷我、酷狗、百度源歌单\n- 在设置界面-关于洛雪音乐说明部分新增**最新版网盘下载地址**与**打赏地址**\n- 新增酷狗 电音热歌榜、DJ热歌榜\n- 新增版本更新超时功能，对于部分无法访问GitHub的用户做更新超时提醒\n\n### 移除\n\n- **注意**：0.4.0以前的版本即将失效，请更新到0.4.0版本\n\n## [0.3.5](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.4...v0.3.5) - 2019-08-30\n\n### 新增\n\n- 新增**测试接口**，该接口同样速度较慢，但软件的大部分功能可用，**请自行切换到该接口**，找接口辛苦，且用且珍惜！\n\n### 优化\n\n- 取消需要刷新URL时windows任务栏进度显示错误状态（现显示为暂停状态）\n\n### 修复\n\n- 修复使用临时接口时在试听列表双击灰色歌曲仍然会进行播放的Bug\n- 修复歌词加载Bug\n\n## [0.3.4](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.3...v0.3.4) - 2019-08-29\n\n### 优化\n\n- 减少接口不稳定带来的影响，适当增加请求等待时间\n\n### 修复\n\n- 修复播放过程中URL过期不会刷新URL的问题\n\n## [0.3.3](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.2...v0.3.3) - 2019-08-29\n\n### 修复\n\n- **messoer**的接口已经关闭，暂时切换到临时接口使用，部分功能受限。。。\n- 修复设置界面更新出错时仍然显示更新下载中的问题\n- 修复手动定位播放进度条时存在偏差的问题\n- 屏蔽播放器中没有歌曲时对进度条的点击\n\n\n## [0.3.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.1...v0.3.2) - 2019-08-24\n\n### 新增\n\n- 新增酷狗排行榜其他音质下载\n\n## [0.3.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.3.0...v0.3.1) - 2019-08-24\n\n### 修复\n\n- 修复音量条主题适配\n\n## [0.3.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.2.3...v0.3.0) - 2019-08-24\n\n### 新增\n\n- 新增**MAC**及**Linux**版本（需要的可自行下载）\n- 新增音量调整\n- 新增任务栏播放进度条控制选项（现在可在设置界面关闭在任务栏显示的播放进度）\n- 新增更新出错时的弹窗提示\n- 从该版本起，非安装版也会有更新弹窗提醒了，但仍然需要手动下载新版本更新，版本信息可到设置页面查看\n\n### 修复\n\n- 强制把临时接口设置回 `messoer` 接口\n\n## [0.2.3](https://github.com/lyswhut/lx-music-desktop/compare/v0.2.2...v0.2.3) - 2019-08-22\n\n### 新增\n\n- 新增任务栏程序标题改变功能（播放歌曲时任务栏标题将显示当前播放的歌曲）\n\n### 修复\n\n- 使用临时接口时，试听列表中的下载按钮仍然能点击的Bug\n- 修复某些情况下歌曲链接未能缓存的问题\n\n### 移除\n\n- 移除临时接口（因服务器被攻击，本接口已关闭）\n- 移除列表栏设置的隐藏专辑栏选项（感觉这个设置并没有什么luan用，并且还会打破布局）\n\n## [0.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.2.1...v0.2.2) - 2019-08-21\n\n### 修复\n\n- 修复下载过程中出错重试5次都失败后不会自动开始下一个任务的Bug\n- 修复播放到一半URL过期时不会刷新URL直接播放下一首的问题\n\n## [0.2.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.2.0...v0.2.1) - 2019-08-20\n\n### 优化\n\n- 新增歌曲URL存储，当URL无效时才重新获取，以减少接口不稳定的影响\n\n### 修复\n\n- 修复歌曲加载无法加载时自动切换混乱的Bug\n- 修复移除列表最后一首歌曲时播放器不停止播放的问题\n\n## [0.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.6...v0.2.0) - 2019-08-20\n\n### 新增\n\n- 新增**百度音乐**排行榜及其音乐直接试听与下载\n- 新增网易云排行榜音乐直接试听与下载（目前仅支持128k音质）\n- 新增酷狗排行榜音乐直接试听与下载（目前仅支持128k音质）\n\n### 修复\n\n- 修复更新弹窗历史版本描述多余的换行问题\n- 修复歌曲无法播放的情况下歌词仍会播放的问题\n\n## [0.1.6](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.5...v0.1.6) - 2019-08-19\n\n### 修复\n\n- 修复列表多选音源限制Bug\n\n## [0.1.5](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.4...v0.1.5) - 2019-08-19\n\n### 新增\n\n- 新增搜索列表批量试听与下载功能\n- 新增排行榜列表批量试听与下载功能\n- 新增试听列表批量移除与下载功能\n- 新增下载列表批量开始、暂停与移除功能\n\n### 优化\n\n- 优化歌曲切换机制\n\n## [0.1.4](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.3...v0.1.4) - 2019-08-18\n\n### 新增\n\n- 新增音乐来源切换，可到设置页面-基本设置 look look !\n- 为搜索结果列表添加多选功能。\nP.S：暂时没想好多选后的操作按钮放哪...\n\n### 优化\n\n- 重构与改进checkbox组件，使其支持不定选中状态\n- 完善上一个版本的http请求封装并切换部分请求到该方法上\n- 优化其他一些细节\n\n## [0.1.3](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.2...v0.1.3) - 2019-08-17\n\n### 新增\n\n- 新增win32应用构建\n\n### 修复\n\n- 修复安装包许可协议乱码问题\n- **messoer 提供的接口已挂**，暂时切换到临时接口！\n\n### 移除\n\n- 由于messoer接口无法使用，QQ音乐排行榜直接播放/下载功能暂时关闭\n\n## [0.1.2](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.1...v0.1.2) - 2019-08-17\n\n### 修复\n\n- 修复更新弹窗的内容显示问题\n\n## [0.1.1](https://github.com/lyswhut/lx-music-desktop/compare/v0.1.0...v0.1.1) - 2019-08-17\n\n### 新增\n\n- QQ音乐排行榜直接试听与下载（该接口貌似不太稳定，且用且珍惜！）\n\n### 优化\n\n- 优化http请求机制\n- 更新关于本软件说明\n\n### 修复\n\n- 修复当上一个歌曲链接正在获取时切换歌曲请求不会取消的问题\n- 修复切换歌曲时仍然播放上一首歌曲的问题\n\n## [0.1.0] - 2019-8-16\n\n* 0.1.0版本发布\n"
  },
  {
    "path": "FAQ.md",
    "content": "# lx-music-desktop 常见问题\n\n本文档已迁移至：<https://lyswhut.github.io/lx-music-doc/desktop/faq>\n\n<!--\n在阅读本常见问题后，仍然无法解决你的问题，请提交issue或者加企鹅群`830125506`反馈（无事勿加，入群先看群公告），反馈时请**注明**已阅读常见问题！\n\n## ~~软件为什么没有桌面歌词与自定义列表功能~~\n\n洛雪音乐的最初定位不是作为播放器开发的，它主要用于**查找歌曲**，软件的播放功能仅用于试听，不建议用作为常用播放器使用。\n\n## 音乐播放列表机制\n\n1. 默认情况下，播放搜索列表、歌单列表、排行榜列表的歌曲时会自动将该歌曲添加到“我的列表”的试听列表后再播放，这与手动将歌曲添加到试听列表，再去试听列表找到这首歌点播放是等价的\n2. 如果你想要播放多首歌曲，需要使用多选功能（若不知道如何多选请看常见问题）多选后，将这些歌曲添加到“我的列表”播放，或使用稍后播放功能播放\n3. 第2条适用于搜索列表、歌单列表、排行榜列表、我的列表中的歌曲\n4. 对于歌单详情列表，除了可以使用第2条的方式播放外，你可以点击详情页上面的播放按钮临时播放当前歌单，或点击收藏将当前歌单收藏到“我的列表”后再去播放\n5. 对于排行榜详情列表，除了可以使用第2条的方式播放外，你可以在右击排行榜名字后弹出的菜单中，播放或收藏整个排行榜，这与第四条的歌单中的播放、与收藏按钮功能一致\n6. v1.18.0及之后新增了“双击列表里的歌曲时自动切换到当前列表播放”设置，默认关闭，此功能仅对歌单、排行榜有效\n7. 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\n8. 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\n\n## 可用的鼠标、键盘快捷操作\n\n- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲\n- 鼠标右击播放栏进度条上的`LRC`按钮可以锁定/解锁桌面歌词\n- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内\n- 鼠标右击搜索界面中的单条搜索历史可以将其移除\n- 歌曲列表内的文字在选中后，鼠标右击可以复制已选中的文字，此功能只对搜索、歌单、排行榜、我的列表中的列表有效\n- 鼠标在播放详情页内右键双击可以关闭播放详情页\n- 鼠标左击播放栏上的歌曲名字可以将它复制\n- 鼠标右击设置-主题设置的“道法自然（英文Auto）”主题可以打开亮、暗主题设置窗口\n- 歌曲搜索框的候选内容可以用键盘上下方向键选择，按回车键搜索已选内容\n- 在歌单详情页按退格键可以返回歌单列表\n- 歌曲列表中可以使用`Ctrl`、`Shift`键进行多选，这类似Windows下的文件选择，详情看常见问题列表多选部分\n- 在我的列表内可以使用`Ctrl + f`键打开搜索框进行列表内歌曲搜索，搜索框按`Esc`键可以关闭搜索框，搜索框内按上下方向键可以选择歌曲，按`回车`键跳转到已选歌曲，按`Ctrl + 回车`可以跳转并播放已选歌曲\n- 在我的列表按住`Ctrl`键可以进入列表拖动模式，此时可以用鼠标拖动列表调整列表的位置\n- 编辑列表名时按`Esc`键可以取消编辑\n- 按`F11`可以进入、退出全屏状态（v1.19.0新增）\n- 在歌曲添加弹窗中，若按住`Ctrl`键后再点击列表名，将不会自动关闭添加窗口，这对想要将同一首（一批）歌曲添加到多个列表时会很有用（v1.22.0新增）\n\n注：在macOS上`Ctrl`键对应`Command`键\n\n## 歌曲无法试听与下载\n\n### 所有歌曲都提示 `请求异常😮，可以多试几次，若还是不行就换一首吧。。。`\n\n尝试更换网络，如切换到移动网络，若移动网络还是不行则尝试开关下手机的飞行模式后再试，<br>\n若使用家庭网络的话，可尝试将光猫断电5分钟左右再通电联网后播放。\n\n### 提示 `getaddrinfo EAI_AGAIN ...` 或 `无法连接到服务器`\n\n尝试在在浏览器打开这个地址`http://ts.tempmusics.tk`，浏览器显示404是正常的，如果不是404那就证明所在网络无法访问接口服务器。\n若网页无法打开或打开来不是404，则可能是DNS的问题，可以尝试以下办法：\n\n1. 将DNS改成自动获取试试（注：改完可能需要清理下系统DNS缓存才生效）\n2. 手动把DNS改一下，不要用360的DNS，可以把DNS改成`223.6.6.6`、`8.8.8.8`（注：改完可能需要清理下系统DNS缓存才生效）\n\n改完DNS后可能需要重启软件才生效\n\n### 通用解决方法\n\n尝试按以下顺序解决：\n\n1. 尝试更新到最新版本\n2. 尝试切换其他歌曲（或直接搜索该歌曲），若全部歌曲都无法试听与下载则进行下一步\n3. 尝试到 设置-音乐来源 切换到其他接口\n4. 尝试切换网络，比如用手机开热点（所有歌曲都提示请求异常时可通过此方法解决，或等一两天后再试）\n5. 若还不行请到这个链接查看详情：<https://github.com/lyswhut/lx-music-desktop/issues/5>\n6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明，则应该是你网络无法访问接口服务器的问题，如果接口有问题我会在那里说明。\n\n想要知道是不是自己网络的问题可以看看`http://ts.tempmusics.tk`能不能在浏览器打开，浏览器显示404是正常的，如果不是404那就证明所在网络无法访问接口服务器。\n若网页无法打开或打来不是404，则应该是DNS的问题，可以尝试以下办法：\n\n1. 将DNS改成自动获取试试\n2. 手动把DNS改一下，不要用360的DNS，可以把DNS改成`223.6.6.6`、`8.8.8.8`\n\n### Windows版所有歌曲都提示 `音频加载错误，5秒后切换下一首`\n\n尝试关闭 Internet选项 的代理设置。\n\n如果你不知道怎么做，可以尝试按以下步骤去做：\n\n按<kbd>windows</kbd>+<kbd>r</kbd>键打开“运行”窗口，输入`inetcpl.cpl`后回车，在打开的 Internet选项 对话框中，切换到 连接 -> 局域网设置，在弹出的新窗口中把代理服务器下的勾去掉，如果自动配置下的勾也有被勾选，那么建议也去掉，最后按确定关闭所有弹窗。\n\n> 来源：<https://github.com/lyswhut/lx-music-desktop/issues/873#issuecomment-1146945724>\n\n## 列表多选\n\n从v0.18.0起，列表多选需要键盘配合，想要多选前需按下`Shift`或`Ctrl`键然后再鼠标点击想要选中的内容即可触发多选机制，其中`Shift`键用于连续选择，`Ctrl`键用于不连续选择，`Ctrl+a`用于快速全选。\n\n- 例子一：想要选中1-5项，则先按下`Shift`键后，鼠标点击第一项，再点击第五项即可完成选择；\n- 例子二：想要选中1项与第3项，则先按下`Ctrl`键后，鼠标点击第一项，再点击第三项即可完成选择；\n- 例子三：想要选中当前列表的全部内容，键盘先按下`Ctrl`键不放，然后按`a`键，即可完成选择。\n\n用`Shift`或`Ctrl`选择时，鼠标点击未选中的内容会将其选中，点击已选择的内容会将其取消选择，若想全部取消选择，在不按`Shift`或`Alt`键的情况下，随意点击列表里的一项内容即可全部取消选择。(P.S：`Ctrl`键对应Mac OS上的`Command`键)\n\n注：选完后可用鼠标右击弹出右键菜单操作已选的内容\n\n## 播放整个歌单或排行榜\n\n播放在线列表内的歌曲需要将它们都添加到我的列表才能播放，你可以全选列表内的歌曲然后添加到现有列表或者新创建的列表，然后去播放该列表内的歌曲。\n\n从v1.10.0起，你可以右击排行榜名字的弹出菜单中直接播放或收藏整个排行榜的歌曲。\n\n## 无法打开外部歌单\n\n不支持跨源打开歌单，请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应（不一样的话请通过右上角切换歌单源）；<br>\n对于分享出来的歌单，若打开失败，可尝试先在浏览器中打开后，再从浏览器地址栏复制URL地址到软件打开；<br>\n或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>\n\n注：网易源的“我喜欢”歌单无法在未登录的情况下打开，所以你需要手动创建一个歌单后将“我喜欢”里的歌曲移动到该歌单打开\n\n### 打开网易源“我喜欢”歌单\n\n由于网易源的“我喜欢”歌单需要登录才能打开，从v1.13.0起提供了可以以注入token的方式打开网易源“我喜欢”歌单的功能，现若想要打开此类歌单，需要在歌单链接或id后面拼上 `###` 再加上有效的token，拼接格式：`[id|url]###token`，例子（最后面的xxxxxx替换成你的token）：\n- `https://music.163.com/#/playlist?id=11332&userid=123456###xxxxxx`\n- `11332###xxxxxx`\n\n即：将 `歌单链接或者歌单ID`、`###`、`token` 这三者拼到一起。\n\n#### `token`的获取方法\n\n**注：`token`是你账号的临时身份令牌，不要随便泄露给他人**<br>\n在浏览器打开登录网易云音乐并**登录**后，按`F12`，此时将会打开开发者窗口，然后按你使用的浏览器操作：\n\n##### Chrome、360、QQ等浏览器\n\n这些浏览器打开此窗口时界面可能是中文也可能是英文，英文的话按括号里的来\n\n1. 点击窗口顶部`应用程序(application)`（若找不到此选项，则可能是被折叠起来了，看看顶部菜单的`>>`）\n2. 展开左侧 `Cookies`\n3. 点击 `https://music.163.com`\n4. 在右侧窗口找到 `名称(Name)` 为 `MUSIC_U` 的这行，这行的第二列（`值(Value)`）内的那串内容就是`token`，双击它进入编辑状态，然后按`ctrl + c`键就可以将它复制\n\n##### 火狐浏览器\n\n1. 点击窗口顶部`存储`\n2. 展开左侧 `Cookie`\n3. 点击 `https://music.163.com`\n4. 在右侧窗口找到 `名称` 为 `MUSIC_U` 的这行，这行的最后一列（`值`）内的那串内容就是`token`，双击它进入编辑状态，然后按`ctrl + c`键就可以将它复制\n\n## 更新已收藏的在线歌单\n\n该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效，可右击已收藏的列表名从弹出的菜单中选择“更新”使用该功能，\n\n需要注意的是：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表。\n\n## 调整我的列表的列表顺序\n\n按住Ctrl键（Mac上对应Command键）的时候将进入“拖动模式”，此时可以拖动列表的位置来调整顺序。\n\n## 同步功能的使用（实验性，首次使用前建议先备份一次列表）\n\n**注意：由于同步传输时的数据是明文传输，请在受信任的网络下使用此功能！**<br>\n此功能需要配合移动端使用，PC端与移动端处在同一个局域网（路由器的网络）下时，可以多端实时同步歌曲列表，使用方法：\n\n1. 在PC端的设置-数据同步开启同步功能（这时如果出现安全软件、防火墙等提示网络连接弹窗时需要点击允许）\n2. 在移动端的设置-同步-同步服务器地址输入PC端显示的同步服务器地址（如果显示可以多个，则输入与**移动端上显示的本机地址**最相似的那个），端口号与PC端的同步端口一致（**输入完毕后需要按一下键盘上的回车键使输入的内容生效**）\n3. 输入完这两项后点击“启动同步”\n4. 若连接成功，对于首次同步时，若两边的设备的列表不为空，则PC端会弹出选择列表同步方式的弹窗，同步方式的说明弹窗下面有介绍\n\n#### 关于同步弹窗的说明\n\n对于首次同步时，若两边的设备的列表不为空，则PC端会弹出选择列表同步方式的弹窗，此弹窗内的同步方式仅针对**首次同步**，<br>\n第一次同步成功后，以后再同步时将会自动根据两边设备的列表内容合并同步，不信你可以在同步完成后断开两边的连接，然后在两边增删一些歌曲或列表后再同步试试看~😉\n\n#### 连接同步服务失败的可能原因\n\n- 此功能需要PC端与移动端都连接在同一个路由器下的网络才能使用\n- 检查防火墙是否拦截了PC端的服务端口\n- 路由器若开启了AP隔离，则此功能无法使用\n\n#### 连接同步服务失败的检查\n\n1. 确保PC端的同步服务已启用成功（若连接码、同步服务地址没有内容，则证明服务启动失败，此时看启用同步功能复选框后面的错误信息自行解决，另外若你不知道端口号是什么意思就不要乱改，或不要改得太大与太小）\n2. 在手机浏览器地址栏输入`http://x.x.x.x:23332/hello` **（注：将`x.x.x.x`换成PC端显示的同步服务地址，`23332`为PC端的端口号）** 后回车，若此地址可以打开并显示 `Hello~::^-^::`则证明移动端与PC端网络已互通，\n3. 若移动端无法打开第2步的地址，则在PC端的浏览器地址栏输入并打开该地址，若可以打开，则要么是被LX Music PC端被电脑防火墙拦截，要么PC端与移动端不在同一个网络下，或者路由器开启了AP隔离（一般在公共网络下会出现这种情况）\n\n## 界面异常（界面显示不完整）\n\n### Windows 10、11界面异常、界面无法显示\n\n尝试添加运行参数 `--disable-gpu-sandbox` 启动，例如：`.\\lx-music-desktop.exe --disable-gpu-sandbox`，添加方法可自行百度“给快捷方式加参数”。\n\n若以上方法无效，则尝试将 `--disable-gpu-sandbox` 逐个换成以下参数启动，直到恢复正常为止：\n\n- `--no-sandbox`\n- `-dha`\n- `--disable-gpu`\n\n:::caution\n这些参数会禁用程序的某些安全特性或降低程序性能，没有遇到问题不要使用它们！\n:::\n\n对于界面无法显示，任务栏里也没看到图标，但是任务管理器里面看到进程的问题，还可尝试更换软件安装目录（对于安装版需要先卸载再换目录安装，绿色版直接剪切移动即可，只要目录换了就行），<br />\n此方法的相关讨论看：<https://github.com/lyswhut/lx-music-desktop/issues/943#issuecomment-1217832186>\n\n### Windows 7 下界面异常\n\n由于软件默认使用了透明窗口，根据Electron官方文档的[说明](https://www.electronjs.org/docs/latest/tutorial/window-customization#limitations)：\n> 在 windows 操作系统上, 当 DWM 被禁用时, 透明窗口将无法工作。\n\n因此，当 win7 没有使用**Aero**主题时界面将会显示异常，开启AERO的方法请自行百度：`win7开启Aero效果`（开启后可看到任务栏变透明）。<br>\n从`0.14.0`版本起不再强制要求开启透明效果，若你实在不想开启（若非电脑配置太低，墙裂建议开启！），可通过添加运行参数`-dt`来运行程序即可，例如：`.\\lx-music-desktop.exe -dt`，添加方法可自行百度“给快捷方式加参数”，该参数的作用是用来控制程序是否使用非透明窗口运行。\n\n注：启用**Aero**主题后，若软件出现黑边框，则重启软件即可恢复正常。\n\n对于一些完全无法正常显示界面、开启了AERO后问题仍未解决的情况，请阅读下面的 **Window 7 下软件启动后，界面无法显示** 解决。\n\n### Linux 下界面异常\n\n根据Electron里issue的[解决方案](https://github.com/electron/electron/issues/2170#issuecomment-736223269)，<br>\n若你遇到透明问题可尝试添加启动参数 `-dha` 来禁用硬件加速，例如：`.\\lx-music-desktop.exe -dha`。\n\n注：v1.6.0及之后的版本才支持`-dha`参数\n\n## Windows 7 下软件启动后，界面无法显示\n\n对于软件启动后，可以在任务栏看到软件，但软件界面在桌面上无任何显示，或者整个界面偶尔闪烁的情况。<br>\n原始问题看：<https://github.com/electron/electron/issues/19569#issuecomment-522231083><br>\n解决办法：下载`.NET Framework 4.7.1`或**更高**版本安装即可(建议安装最新版，若安装过程中遇到问题可尝试自行百度解决)。<br>\n微软官方下载地址：<https://dotnet.microsoft.com/download/dotnet-framework><br>\n下载`Runtime(运行时)`版即可，安装完成后可能需要重启才生效，**若出现闪烁的情况**，可阅读下面的**Windows 7 下整个界面闪烁**解决。\n\n## Windows 7 下整个界面闪烁（消失又出现）\n\n可尝试在关掉软件后，在桌面空白处鼠标右击，在弹出的菜单中选择**个性化**，在弹出的窗口中**切换到系统内置的Aero主题**，然后再启动软件看是否解决。\n\n## Windows 7 下桌面歌词字体列表为空\n\nWindows 7 系统系统需要安装 Powershell 5.1及以上版本才可正常获取系统字体列表。\n\n想要查看当前 Powershell 版本可以在 Powershell 窗口输入命令：`Get-Host`\n\n最新 Powershell 安装包可以去官方 [Github releases](https://github.com/PowerShell/PowerShell/releases) 页下载，安装过程中若出现错误，请自行按照提示或者百度/Google解决。\n\n## 安装版安装失败，提示安装程序并未成功地运行完成\n\n对于部分电脑出现安装失败的问题，可以做出以下尝试：\n\n- 若你之前可以安装成功，但现在安装失败，就去**控制面板-程序和功能**或用第三方卸载工具看下有没有之前的版本残留，若同时在不同路径下安装了多个版本就可能会出现该问题，这种情况卸载掉所有版本重新安装即可\n- 清理安装路径下的残留文件\n- 清理注册表（建议用清理工具清理）\n\n## 软件无法联网\n\n软件的排行榜、歌单、搜索列表**都**无法加载：\n\n- 检查是否在设置界面开启了代理（当代理乱设置时软件将无法联网）\n- 检查软件是否被第三方软件/防火墙阻止联网\n\n## 桌面歌词显示异常\n\n### Windows 7 系统桌面歌词显示异常\n\nWindows 7 未开启 Aero 效果时桌面歌词会有问题，详情看上面的 **Windows 7 下界面异常** 方法解决。\n\n### MAC OS 系统、桌面歌词有残留阴影\n\n此问题似乎是Electron的Bug，翻阅electron的issue列表发现该Bug以存在很久了，遗憾的是没有一直都没有修复，由于我没有装MAC平台的电脑，没法重现，就没再去electron提issue，更多信息看：\n\n- <https://github.com/electron/electron/issues/21173>\n- <https://github.com/electron/electron/issues/14304>\n\n### Linux 系统下桌面歌词窗口异常\n\n`v1.2.1`以前的版本在 Ubuntu 18.10 下第一次开启桌面歌词时歌词窗口会变白，需要关闭后再开启，\n`v1.2.1`及之后的版本已修复该问题。\n\n其他 Linux 系统未测试，如有异常也是意料之中，目前不打算去处理 Linux 平台的桌面歌词问题，但你可以尝试按照`Linux 下界面异常`的解决方案去解决。\n\n## 歌曲下载失败，提示 `ENOENT: no such file or directory, mkdir`\n\n更换下载歌曲目录即可解决（一般是设置的歌曲下载目录没有读写权限导致的）。\n\n## 使用软件时导致耳机意外关机\n\n据反馈，漫步者部分型号的耳机与本软件一起使用时将会导致耳机意外关机，\n详情看：<https://github.com/lyswhut/lx-music-desktop/issues/457>，\n若出现该问题可尝试添加`-dhmkh`启动参数解决，启动参数添加方法请自行百度“windows给快捷方式添加启动参数”。\n\n## 软件安装包说明\n\n软件发布页及网盘中有多个类型的安装文件，以下是对这些类型文件的说明：\n\n文件名带 `win_` 的是在Windows系统上运行的版本，<br>\n其中安装版（Setup）可自动更新软件，<br>\n绿色版（green）为免安装版，自动更新功能不可用；\n\n以 **`.dmg`** 结尾的文件为 MAC 版本；\n\n以 **`.AppImage`**、**`.deb`**、**`.rpm`**、**`.pacman`** 结尾的为 Linux 版本。\n\n带有`x64`的为64位的系统版本，带`x86`的为32位的系统版本；若两个都带有的则为集合版，安装时会自动根据系统位数选择对应的版本安装；带有`arm`的为arm架构系统的版本。\n\n## 软件更新\n\n软件启动时若发现新版本时会自动从本仓库下载安装包，下载完毕会弹窗提示更新。<br>\n若下载未完成时软件被关闭，下次启动软件会再次自动下载。<br>\n若还是**更新失败**，可能是无法访问GitHub导致的，这时需要手动更新，即下载最新安装包直接覆盖安装即可。<br>\n注意：**绿色版**的软件自动更新功能**不可用**，建议使用安装版！！<br>\n注意：**Mac版**、**Linux**版不支持自动更新！\n\n### Windows 安装版在升级后，卸载了旧版本，但没有安装新版本\n\n出现这个问题的原因一般是你当初在安装本软件的时候是以管理员身份安装的，运行软件的时候没有以管理员身份运行，所以卸载后无法再装上。\n\n安装本软件时建议选择 `为当前用户安装`，并安装在当前用户目录或者安装在不需要管理员权限的目录（即其他分区下），不要选`为所有用户安装`。\n\n## 缺少`xxx.dll`\n\n这个是电脑缺少某些dll导致的，正常的系统是没有这个问题的，可以尝试如下几个解决办法：\n\n- 以管理员权限打开`cmd`，输入`sfc /scannow`回车等待检查完成重启电脑\n- 若上面的方法**修复、重启**电脑后仍然不行，就自行百度弹出的**错误信息**看下别人是怎么解决的\n\n## MAC OS无法启动软件，提示 lx-music-desktop 已损坏\n\n这是因为软件没有签名，被系统阻止运行，<br>\n在终端里输入 `sudo xattr -rd com.apple.quarantine /Applications/lx-music-desktop.app`，然后输入你的电脑密码即可\n\n还可以参考：\n\n- <http://www.pc6.com/edu/168719.html>\n- <https://blog.csdn.net/for641/article/details/104811538>\n\n## 数据存储路径\n\n默认情况下，软件的数据存储在：\n\n- Windows：`%APPDATA%/lx-music-desktop`\n- Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\n- macOS：`~/Library/Application/lx-music-desktop`\n\n在Windows平台下，若程序目录下存在`portable`目录，则自动使用此目录作为数据存储目录（v1.17.0新增）。\n\n## 杀毒软件提示有病毒或恶意行为\n\n本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为，并且软件代码已开源，请自行查阅，软件安装包也是由CI拉取源代码构建，构建日志：[GitHub Actions](https://github.com/lyswhut/lx-music-desktop/actions)<br>\n尽管如此，但这不意味着软件是100%安全的，由于软件使用了第三方依赖，当这些依赖存在恶意行为时（[供应链攻击](https://docs.microsoft.com/zh-cn/windows/security/threat-protection/intelligence/supply-chain-malware)），软件也将会受到牵连，所以我只能尽量选择使用较多人用、信任度较高的依赖。<br>\n当然，以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的，或者有相关能力者还可以下载源代码自己构建安装包。\n\n从`v0.17.0`起，由于加入了音频输出设备切换功能，该功能调用了 [MediaDevices.enumerateDevices()](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/enumerateDevices)，可能导致安全软件提示洛雪要访问摄像头（目前发现卡巴斯基会提示），但实际上没有用到摄像头，并且摄像头的提示灯也不会亮，你可以选择阻止访问。\n\n最后，若出现杀毒软件报毒、存在恶意行为，请自行判断选择是否继续使用本软件！\n\n## 启动参数\n\n目前软件已支持的启动参数如下：\n\n- `-search`  启动软件时自动在搜索框搜索指定的内容，例如：`-search=\"突然的自我 - 伍佰\"`\n- `-dha`  禁用硬件加速启动（Disable Hardware Acceleration），窗口显示有问题时可以尝试添加此参数启动（v1.6.0起新增）\n- `-dt` 以非透明模式启动（Disable Transparent），对于未开启AERO效果的win7系统可加此参数启动以确保界面正常显示（注：该参数对桌面歌词无效），原来的`-nt`参数已重命名为`-dt`（v1.6.0起重命名）\n- `-dhmkh` 禁用硬件媒体密钥处理（Disable Hardware Media Key Handling），此选项将禁用Chromium的Hardware Media Key Handling特性（v1.9.0起新增）\n- `-proxy-server` 设置代理服务器，代理应用的所有流量，例：`-proxy-server=\"127.0.0.1:1081\"`（不支持设置账号密码，v1.17.0起新增）。注：应用内“设置-网络-代理设置”仅代理接口请求的流量，优先级更高\n- `-proxy-bypass-list` 以分号分隔的主机列表绕过代理服务器，例：`-proxy-bypass-list=\"<local>;*.google.com;*foo.com;1.2.3.4:5678\"`（与`-proxy-server`一起使用才有效，v1.17.0起新增）。注：此设置对应用内接口请求无效\n- `-play` 启动时播放指定列表的音乐，参数说明：\n  - `type`：播放类型，目前固定为`songList`\n  - `source`：播放源，可用值为`kw/kg/tx/wy/mg/myList`，其中`kw/kg/tx/wy/mg`对应各源的在线列表，`myList`为本地列表\n  - `link`：要播放的在线列表歌单链接、或ID，source为`kw/kg/tx/wy/mg`之一（在线列表）时必传，举例：`./lx-music-desktop -play=\"type=songList&source=kw&link=歌单URL or ID\"`，注意：如果传入URL时必须对URL进行编码后再传入\n  - `name`：要播放的本地列表歌单名字，source为`myList`时必传，举例：`./lx-music-desktop -play=\"type=songList&source=myList&name=默认列表\"`\n  - `index`：从列表的哪个位置开始播放，选传，若不传默认播放第一首歌曲，举例：`./lx-music-desktop -play=\"type=songList&source=myList&name=默认列表&index=2\"`\n\n## Scheme URL支持\n\n从v1.17.0起支持 Scheme URL，可以使用此功能从浏览器等场景下调用LX Music，我们开发了一个[油猴脚本](https://github.com/lyswhut/lx-music-script#readme)配套使用<br>\n脚本安装地址：<https://greasyfork.org/zh-CN/scripts/438148><br>\n以下是目前可用的Scheme URL调用方式：\n\n- URL统一以`lxmusic://`开头\n- 若无特别说明，源的可用值为：`kw/kg/tx/wy/mg`\n- 若无特别说明，音质的可用值为：`128k/320k/flac/flac24bit`\n\n目前支持两种传参方式：\n\n- 通过`data`传参，以经过URL编码的JSON数据传参，例：`lxmusic://music/play?data=xxxx`，其中`xxxx`为经过URL编码后的JSON数据，支持复杂的参数调用\n- 通过`URL`传参，适用于简单传参的调用，不需要转成JSON格式，例：`lxmusic://music/search/xxxx`，但仍然需要对数据进行URL编码，只适应于简单参数调用（v1.18.0新增）\n\n### `data`方式传参\n\n以经过URL编码的JSON数据传参，例：`lxmusic://music/play?data=xxxx`，其中`xxxx`为经过URL编码后的JSON数据，JSON数据内容取决于下表的参数部分\n\n| 描述 | URL | 参数\n| --- | --- | ---\n| 打开歌单 | `songlist/open` | `source<String>`（源，必须）<br>`id<String/Number>`（歌单ID，可选）<br>`url<String>`（歌单URL，可选）其中ID与URL必需传一个\n| 播放歌单 | `songlist/play` | `source<String>`（源，必须）<br>`id<String/Number>`（歌单ID，可选）<br>`url<String>`（歌单URL，可选）其中`id`与`url`必需传一个<br>`index<Number>`（播放第几首歌，可选，从0开始）\n| 搜索歌曲 | `music/search` | `keywords<String/Number>`（要搜索的内容，必须）<br>`source<String>`（源，可选）\n| 播放歌曲 | `music/play` | `name<String>`（歌曲名，必传）<br>`singer<String>`（艺术家名，必传）<br>`source<String>`（源，必传）<br>`songmid<String/Number>`（歌曲ID，必传）<br>`img<String>`（歌曲图片链接，选传）<br>`albumId<String/Number>`（歌曲专辑ID，选传）<br>`interval<String>`（格式化后的歌曲时长，选传，例：`03:55`）<br>`albumName<String>`（歌曲专辑名称，选传）<br>`types<Object>`（歌曲可用音质数组，必传，<br>数组格式：`[{\"type\": \"<音质>\", size: \"<格式化后的文件大小，选传>\", hash: \"<kg源必传>\"}]`，<br>例：`[{\"type\": \"128k\", size: \"3.56M\"}, {\"type\": \"320k\", size: null}]`）<br><br>以下为平台特定参数：<br>`hash<String>`（歌曲hash，kg源必传）<br>`strMediaMid<String>`（歌曲strMediaMid，tx源必传）<br>`albumMid<String>`（歌曲albumMid，tx源专用，选传）<br>`copyrightId<String>`（歌曲copyrightId，mg源必传）<br>`lrcUrl<String>`（歌曲lrcUrl，mg源专用，选传）\n\n### `URL`方式传参\n\n由于URL传参只适用于简单传参场景，所以目前只支持以下功能的调用：\n\n| 描述 | URL | 参数\n| --- | --- | ---\n| 搜索歌曲 | `music/search/{source}/{keywords}` | `source`（源，可选）<br>`keywords`（要搜索的内容，必须）<br>例：`music/search/kw/xxx`、`music/search/xxx`\n| 打开歌单 | `songlist/open/{source}/{id/url}` | `source`（源，必须）<br>`id/url`（歌单ID或歌单URL，必须）<br>例：`songlist/open/kw/123456`\n\n## 自定义源脚本编写说明\n\n文件请使用UTF-8编码格式编写，脚本所用编程语言为JavaScript，可以使用ES6+语法，脚本与应用的交互是使用类似事件收发的方式进行，这是一个基本的脚本例子：\n\n```js\n/**\n * @name 测试音乐源\n * @description 我只是一个测试音乐源哦\n * @version 1.0.0\n * @author xxx\n * @homepage http://xxx\n */\n\n\nconst { EVENT_NAMES, request, on, send } = window.lx\n\nconst qualitys = {\n  kw: {\n    '128k': '128',\n    '320k': '320',\n    flac: 'flac',\n  },\n}\nconst httpRequest = (url, options) => new Promise((resolve, reject) => {\n  request(url, options, (err, resp) => {\n    if (err) return reject(err)\n    resolve(resp.body)\n  })\n})\n\nconst apis = {\n  kw: {\n    musicUrl({ songmid }, quality) {\n      return httpRequest('http://xxx').then(data => {\n        return data.url\n      })\n    },\n  },\n}\n\n// 注册应用API请求事件\n// source 音乐源，可能的值取决于初始化时传入的sources对象的源key值\n// info 请求附加信息，内容根据action变化\n// action 请求操作类型，目前只有musicUrl，即获取音乐URL链接，\n//    当action为musicUrl时info的结构：{type, musicInfo}，\n//        info.type：音乐质量，可能的值有128k / 320k / flac（取决于初始化时对应源传入的qualitys值中的一个），\n//        info.musicInfo：音乐信息对象，里面有音乐ID、名字等信息\non(EVENT_NAMES.request, ({ source, action, info }) => {\n  // 回调必须返回 Promise 对象\n  switch (action) {\n    // action 为 musicUrl 时需要在 Promise 返回歌曲 url\n    case 'musicUrl':\n      return apis[source].musicUrl(info.musicInfo, qualitys[source][info.type]).catch(err => {\n        console.log(err)\n        return Promise.reject(err)\n      })\n  }\n})\n\n// 脚本初始化完成后需要发送inited事件告知应用\nsend(EVENT_NAMES.inited, {\n  status: true, // 初始化成功 or 失败\n  openDevTools: false, // 是否打开开发者工具，方便用于调试脚本\n  sources: { // 当前脚本支持的源\n    kw: { // 支持的源对象，可用key值：kw/kg/tx/wy/mg\n      name: '酷我音乐',\n      type: 'music',  // 目前固定为 music\n      actions: ['musicUrl'], // 目前固定为 ['musicUrl']\n      qualitys: ['128k', '320k', 'flac', 'flac24bit'], // 当前脚本的该源所支持获取的Url音质，有效的值有：['128k', '320k', 'flac', 'flac24bit']\n    },\n  },\n})\n\n```\n\n### 自定义源信息\n\n文件的开头必须包含以下注释：\n\n```js\n/**\n * @name 测试脚本\n * @description 我只是一个测试脚本\n * @version 1.0.0\n * @author xxx\n * @homepage http://xxx\n */\n\n```\n\n- `@name `：源的名字，建议不要过长，24个字符以内\n- `@description `：源的描述，建议不要过长，36个字符以内，可不填，不填时必须保留 @description\n- `@version`：源的版本号，可不填，不填时可以删除 @version\n- `@author `：脚本作者名字，可不填，不填时可以删除 @author\n- `@homepage `：脚本主页，可不填，不填时可以删除 @homepage\n\n### `window.lx`\n\n应用为脚本暴露的API对象。\n\n#### `window.lx.version`\n\n自定义源API版本，API变更时此版本号将会更改（新增于v1.14.0之后）\n\n#### `window.lx.EVENT_NAMES`\n\n常量事件名称对象，发送、注册事件时传入事件名时使用，可用值：\n\n| 事件名 | 描述\n| --- | ---\n| `inited` | 脚本初始化完成后发送给应用的事件名，发送该事件时需要传入以下信息：`{status, sources, openDevTools}`<br>`status`：初始化结果（`true`成功，`false`失败）<br>`openDevTools`：是否打开DevTools，此选项可用于开发脚本时的调试<br>`sources`：支持的源信息对象，<br>`sources[kw/kg/tx/wy/mg].name`：源的名字（目前非必须）<br>`sources[kw/kg/tx/wy/mg].type`：源类型，目前固定值需为`music`<br>`sources[kw/kg/tx/wy/mg].actions`：支持的actions，由于目前只支持`musicUrl`，所以固定传`['musicUrl']`即可<br>`sources[kw/kg/tx/wy/mg].qualitys`：该源支持的音质列表，有效的值为`['128k', '320k', 'flac', 'flac24bit']`，该字段用于控制应用可用的音质类型\n| `request` | 应用API请求事件名，回调入参：`handler({ source, action, info})`，回调必须返回`Promise`对象<br>`source`：音乐源，可能的值取决于初始化时传入的`sources`对象的源key值<br>`info`：请求附加信息，内容根据`action`变化<br>`action`：请求操作类型，目前只有`musicUrl`，即获取音乐URL链接，需要在 Promise 返回歌曲 url，`info`的结构：`{type, musicInfo}`，`info.type`：音乐质量，可能的值有`128k` / `320k` / `flac` / `flac24bit`（取决于初始化时对应源传入的`qualitys`值中的一个），`info.musicInfo`：音乐信息对象，里面有音乐ID、名字等信息\n| `updateAlert` | 显示源更新弹窗，发送该事件时的参数：`{log, updateUrl}`<br>`log`：更新日志，必传，字符串类型，内容可以使用`\\n`换行，最大长度1024，超过此长度后将被截取超出的部分<br>`updateUrl`：更新地址，用于引导用户去该地址更新源，选传，需为http协议的url地址，最大长度1024<br>此事件每次运行脚本只能调用一次（源版本v1.2.0新增）<br>例子：`lx.send(lx.EVENT_NAMES.updateAlert, { log: 'hello world', updateUrl: 'https://xxx.com' })`\n\n\n#### `window.lx.on`\n\n事件注册方法，应用主动与脚本通信时使用：\n\n```js\n/**\n * @param event_name 事件名\n * @param handler 事件处理回调 -- 注意：注册的回调必须返回 Promise 对象\n */\nwindow.lx.on(event_name, handler)\n```\n\n**注意：** 注册的回调必须返回 `Promise` 对象。\n\n#### `window.lx.send`\n\n事件发送方法，脚本主动与应用通信时使用：\n\n```js\n/**\n * @param event_name 事件名\n * @param datas 要传给应用的数据\n */\nwindow.lx.send(event_name, datas)\n```\n\n#### `window.lx.request`\n\nHTTP请求方法，用于发送HTTP请求，此HTTP请求方法不受跨域规则限制：\n\n```js\n/**\n * @param url 请求的URL\n * @param options 请求选项，可用选项有 method / headers / body / form / formData / timeout\n * @param callback 请求结果的回调 入参：err, resp, body\n * @return 返回一个方法，调用此方法可以终止HTTP请求\n */\nconst cancelHttp = window.lx.request(url, options, callback)\n```\n\n#### `window.lx.utils`\n\n应用提供给脚本的工具方法：\n\n- `window.lx.utils.buffer.from`：对应Node.js的 `Buffer.from`\n- `window.lx.utils.buffer.bufToString`：Buffer转字符串 `bufToString(buffer, format)`，`format`对应Node.js `Buffer.toString`的参数（v1.14.0之后新增）\n- `window.lx.utils.crypto.aesEncrypt`：AES加密 `aesEncrypt(buffer, mode, key, iv)`\n- `window.lx.utils.crypto.md5`：MD5加密 `md5(str)`\n- `window.lx.utils.crypto.randomBytes`：生成随机字符串 `randomBytes(size)`\n- `window.lx.utils.crypto.rsaEncrypt`：RSA加密 `rsaEncrypt(buffer, key)`\n\n目前仅提供以上工具方法，如果需要其他方法可以开issue讨论。 -->\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><a href=\"https://github.com/lyswhut/lx-music-desktop\"><img width=\"200\" src=\"https://github.com/lyswhut/lx-music-desktop/blob/master/doc/images/icon.png\" alt=\"lx-music logo\"></a></p>\n\n<h1 align=\"center\">LX Music 桌面版</h1>\n\n<p align=\"center\">\n  <a href=\"https://github.com/lyswhut/lx-music-desktop/releases\"><img src=\"https://img.shields.io/github/release/lyswhut/lx-music-desktop\" alt=\"Release version\"></a>\n  <a href=\"https://github.com/lyswhut/lx-music-desktop/actions/workflows/release.yml\"><img src=\"https://github.com/lyswhut/lx-music-desktop/workflows/Build/badge.svg\" alt=\"Build status\"></a>\n  <a href=\"https://github.com/lyswhut/lx-music-desktop/actions/workflows/beta-pack.yml\"><img src=\"https://github.com/lyswhut/lx-music-desktop/workflows/Build%20Beta/badge.svg\" alt=\"Build status\"></a>\n  <a href=\"https://electronjs.org/releases/stable\"><img src=\"https://img.shields.io/github/package-json/dependency-version/lyswhut/lx-music-desktop/dev/electron/master\" alt=\"Electron version\"></a>\n  <!-- <a href=\"https://github.com/lyswhut/lx-music-desktop/releases\"><img src=\"https://img.shields.io/github/downloads/lyswhut/lx-music-desktop/latest/total\" alt=\"Downloads\"></a> -->\n  <a href=\"https://github.com/lyswhut/lx-music-desktop/tree/dev\"><img src=\"https://img.shields.io/github/package-json/v/lyswhut/lx-music-desktop/dev\" alt=\"Dev branch version\"></a>\n  <!-- <a href=\"https://github.com/lyswhut/lx-music-desktop/blob/master/LICENSE\"><img src=\"https://img.shields.io/github/license/lyswhut/lx-music-desktop\" alt=\"License\"></a> -->\n</p>\n\n<!-- [![GitHub release][1]][2]\n[![Build status][3]][4]\n[![GitHub Releases Download][5]][6]\n[![dev branch][7]][8]\n[![GitHub license][9]][10] -->\n\n<!-- [1]: https://img.shields.io/github/release/lyswhut/lx-music-desktop\n[2]: https://github.com/lyswhut/lx-music-desktop/releases\n[3]: https://ci.appveyor.com/api/projects/status/flrsqd5ymp8fnte5?svg=true\n[4]: https://ci.appveyor.com/project/lyswhut/lx-music-desktop\n[5]: https://img.shields.io/github/downloads/lyswhut/lx-music-desktop/latest/total\n[5]: https://img.shields.io/github/downloads/lyswhut/lx-music-desktop/total\n[6]: https://github.com/lyswhut/lx-music-desktop/releases\n[7]: https://img.shields.io/github/package-json/v/lyswhut/lx-music-desktop/dev\n[8]: https://github.com/lyswhut/lx-music-desktop/tree/dev\n[9]: https://img.shields.io/github/license/lyswhut/lx-music-desktop\n[10]: https://github.com/lyswhut/lx-music-desktop/blob/master/LICENSE -->\n\n<p align=\"center\">一个基于 Electron & Vue 开发的音乐软件</p>\n\n## 说明\n\n所用技术栈：\n\n- Electron 30+\n- Vue 3\n\n已支持的平台：\n\n- Linux\n- macOS\n- Windows 7 及以上\n\n*移动版项目地址：https://github.com/lyswhut/lx-music-mobile*\n\n*LX Music 项目发展调整与新项目计划：https://github.com/lyswhut/lx-music-desktop/issues/1912*\n\n软件变化请查看[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)。\n\n软件下载请查看 [GitHub Releases](https://github.com/lyswhut/lx-music-desktop/releases)。\n\n使用常见问题请参阅[桌面版常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)。\n\n目前本项目的原始发布地址只有 [**GitHub**](https://github.com/lyswhut/lx-music-desktop/releases)，其他渠道均为第三方转载发布，与本项目无关！\n\n为了提高使用门槛，本软件内的默认设置、UI 操作不以新手友好为目标，所以使用前建议先根据你的喜好浏览调整一遍软件设置，阅读一遍[音乐播放列表机制](https://lyswhut.github.io/lx-music-doc/desktop/faq/playlist)及[可用的鼠标、键盘快捷操作](https://lyswhut.github.io/lx-music-doc/desktop/faq/hotkey)。\n\n### Scheme URL 支持\n\n从 v1.17.0 起支持 Scheme URL，可以使用此功能在浏览器等场景下调用 LX Music，我们开发了一个[油猴脚本](https://github.com/lyswhut/lx-music-script#readme)配套使用。\n\n脚本安装地址：[LX Music 辅助脚本](https://greasyfork.org/zh-CN/scripts/438148)。\n\n若你想自己调用 LX Music，可以参考文档「[Scheme URL 支持](https://lyswhut.github.io/lx-music-doc/desktop/scheme-url)」部分。\n\n### 数据同步服务\n\n从 v2.2.0 起，我们发布了一个独立的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)。如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明。\n\n### 开放 API 支持\n\n从 v2.7.0 起支持开放 API 服务。启用该功能后，将会在本地启动一个 HTTP 服务，提供播放器相关的接口供第三方软件调用，详情看文档「[开放 API 服务](https://lyswhut.github.io/lx-music-doc/desktop/open-api)」部分。\n\n### 数据存储目录\n\n默认情况下，软件的数据存储在：\n\n- Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\n- macOS：`~/Library/Application Support/lx-music-desktop`\n- Windows：`%APPDATA%/lx-music-desktop`\n\n在 Windows 平台上，若程序文件夹中存在 `portable` 文件夹，则自动使用此文件夹作为数据存储文件夹（适用于 v1.17.0 及以上版本）。\n\n## 用户界面\n\n<p><img width=\"100%\" src=\"./doc/images/app.png\" alt=\"lx-music desktop UI\"></p>\n\n## 贡献代码\n\n本项目欢迎 PR，但为了 PR 能顺利合并，需要注意以下几点：\n\n- 对于添加新功能的 PR，建议在提交 PR 前先创建 Issue 进行说明，以确认该功能是否确实需要。\n- 对于修复 bug 的 PR，请提供修复前后的说明及重现方式。\n- 对于其他类型的 PR，则适当附上说明。\n\n贡献代码步骤：\n\n1. 参照[源码使用方法](https://lyswhut.github.io/lx-music-doc/desktop/use-source-code)设置开发环境；\n2. 克隆本仓库代码并切换至 `dev` 分支进行开发；\n3. 提交 PR 至 `dev` 分支。\n\n## 源码使用方法\n\n请参阅：<https://lyswhut.github.io/lx-music-doc/desktop/use-source-code>\n\n## 项目协议\n\n本项目基于 [Apache License 2.0](https://github.com/lyswhut/lx-music-desktop/blob/master/LICENSE) 许可证发行，以下协议是对于 Apache License 2.0 的补充，如有冲突，以以下协议为准。\n\n---\n\n*词语约定：本协议中的“本项目”指 LX Music（洛雪音乐助手）桌面版项目；“使用者”指签署本协议的使用者；“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称；“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。*\n\n### 一、数据来源\n\n1.1 本项目的各官方平台在线数据来源原理是从其公开服务器中拉取数据（与未登录状态在官方平台 APP 获取的数据相同），经过对数据简单地筛选与合并后进行展示，因此本项目不对数据的合法性、准确性负责。\n\n1.2 本项目本身没有获取某个音频数据的能力，本项目使用的在线音频数据来源来自软件设置内“自定义源”设置所选择的“源”返回的在线链接。例如播放某首歌，本项目所做的只是将希望播放的歌曲名、艺术家等信息传递给“源”，若“源”返回了一个链接，则本项目将认为这就是该歌曲的音频数据而进行使用，至于这是不是正确的音频数据本项目无法校验其准确性，所以使用本项目的过程中可能会出现希望播放的音频与实际播放的音频不对应或者无法播放的问题。\n\n1.3 本项目的非官方平台数据（例如“我的列表”内列表）来自使用者本地系统或者使用者连接的同步服务，本项目不对这些数据的合法性、准确性负责。\n\n### 二、版权数据\n\n2.1 使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在 **24 小时内** 清除使用本项目的过程中所产生的版权数据。\n\n### 三、音乐平台别名\n\n3.1 本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼，不包含恶意。如果官方音乐平台觉得不妥，可联系本项目更改或移除。\n\n### 四、资源使用\n\n4.1 本项目内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系本项目移除。\n\n### 五、免责声明\n\n5.1 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。\n\n### 六、使用限制\n\n6.1 本项目完全免费，且开源发布于 GitHub 面向全世界人用作对技术的学习交流。本项目不对项目内的技术可能存在违反当地法律法规的行为作保证。\n\n6.2 **禁止在违反当地法律法规的情况下使用本项目。** 对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。\n\n### 七、版权保护\n\n7.1 音乐平台不易，请尊重版权，支持正版。\n\n### 八、非商业性质\n\n8.1 本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。\n\n### 九、接受协议\n\n9.1 若你使用了本项目，即代表你接受本协议。\n\n---\n\n若对此有疑问请 mail to: lyswhut+qq.com (请将 `+` 替换为 `@`)\n"
  },
  {
    "path": "build-config/build-after-pack.js",
    "content": "const fs = require('fs').promises\n\n// https://github.com/electron-userland/electron-builder/issues/4630\n// https://github.com/electron-userland/electron-builder/issues/4630#issuecomment-782020139\n\nmodule.exports = async(context) => {\n  const { electronPlatformName, appOutDir } = context\n  if (electronPlatformName !== 'darwin') return\n  const {\n    productFilename,\n    info: {\n      _metadata: { macLanguagesInfoPlistStrings },\n    },\n  } = context.packager.appInfo\n\n  const resPath = `${appOutDir}/${productFilename}.app/Contents/Resources`\n\n  // 创建APP语言包文件\n  return Promise.all(\n    Object.entries(macLanguagesInfoPlistStrings).map(([lang, config]) => {\n      let infos = Object.entries(config).map(([k, v]) => `\"${k}\" = \"${v}\";`).join('\\n')\n      return fs.writeFile(`${resPath}/${lang}.lproj/InfoPlist.strings`, infos)\n    }),\n  )\n}\n"
  },
  {
    "path": "build-config/build-before-pack.js",
    "content": "const fs = require('fs')\nconst fsPromises = require('fs').promises\nconst path = require('path')\nconst { Arch } = require('electron-builder')\nconst nodeAbi = require('node-abi')\n\nconst better_sqlite3_fileNameMap = {\n  [Arch.x64]: 'linux-x64',\n  [Arch.arm64]: 'linux-arm64',\n  [Arch.armv7l]: 'linux-arm',\n}\n\nconst qrc_decode_fileNameMap = {\n  win32: {\n    [Arch.x64]: 'win32-x64',\n    [Arch.ia32]: 'win32-ia32',\n    [Arch.arm64]: 'win32-arm64',\n  },\n  linux: {\n    [Arch.x64]: 'linux-x64',\n    [Arch.arm64]: 'linux-arm64',\n    [Arch.armv7l]: 'linux-arm',\n  },\n  darwin: {\n    [Arch.x64]: 'darwin-x64',\n    [Arch.arm64]: 'darwin-arm64',\n  },\n}\n\nconst replaceSqliteLib = async(electronNodeAbi, arch) => {\n  // console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))\n  // if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return\n  // https://github.com/lyswhut/lx-music-desktop/issues/1102\n  // https://github.com/lyswhut/lx-music-desktop/issues/1161\n  console.log('replace sqlite lib...')\n  const filePath = path.join(__dirname, `./lib/better_sqlite3_electron-v${electronNodeAbi}-${better_sqlite3_fileNameMap[arch]}.node`)\n  console.log(filePath)\n  const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')\n  await fsPromises.unlink(targetPath).catch(_ => _)\n  await fsPromises.copyFile(filePath, targetPath)\n}\n\nconst replaceQrcDecodeLib = async(electronNodeAbi, platform, arch) => {\n  console.log('replace qrc_decode lib...', platform, electronNodeAbi, qrc_decode_fileNameMap[platform][arch])\n  const filePath = path.join(__dirname, `./lib/qrc_decode_electron-v${electronNodeAbi}-${qrc_decode_fileNameMap[platform][arch]}.node`)\n  const targetPath = path.join(__dirname, '../build/Release/qrc_decode.node')\n  const targetDir = path.dirname(targetPath)\n  if (fs.existsSync(targetDir)) await fsPromises.unlink(targetPath).catch(_ => _)\n  else await fsPromises.mkdir(targetDir, { recursive: true })\n  await fsPromises.copyFile(filePath, targetPath)\n}\n\n\nmodule.exports = async(context) => {\n  const { electronPlatformName, arch } = context\n  const electronVersion = context.packager?.info?._framework?.version ?? require('../package.json').devDependencies.electron.replace(/^[^\\d]*?(\\d+)/, '$1')\n  const electronNodeAbi = nodeAbi.getAbi(electronVersion, 'electron')\n  await replaceQrcDecodeLib(electronNodeAbi, electronPlatformName, arch)\n  if (electronPlatformName !== 'linux' || process.env.FORCE) return\n  const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')\n  const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')\n  switch (arch) {\n    case Arch.x64:\n    case Arch.arm64:\n    case Arch.armv7l:\n      if (fs.existsSync(bindingFilePath)) {\n        // console.log('rename binding file...')\n        await fsPromises.rename(bindingFilePath, bindingBakFilePath)\n      }\n      await replaceSqliteLib(electronNodeAbi, arch)\n      break\n\n    default:\n      if (fs.existsSync(bindingFilePath)) return\n      // console.log('restore binding file...')\n      await fsPromises.rename(bindingBakFilePath, bindingFilePath)\n      break\n  }\n}\n"
  },
  {
    "path": "build-config/build-pack.js",
    "content": "/* eslint-disable no-template-curly-in-string */\n\nconst builder = require('electron-builder')\nconst beforePack = require('./build-before-pack')\nconst afterPack = require('./build-after-pack')\n\n/**\n* @type {import('electron-builder').Configuration}\n* @see https://www.electron.build/configuration/configuration\n*/\nconst options = {\n  appId: 'cn.toside.music.desktop',\n  productName: 'lx-music-desktop',\n  beforePack,\n  afterPack,\n  protocols: {\n    name: 'lx-music-protocol',\n    schemes: [\n      'lxmusic',\n    ],\n  },\n  directories: {\n    buildResources: './resources',\n    output: './build',\n  },\n  files: [\n    '!node_modules/**/*',\n    'node_modules/font-list',\n    'node_modules/better-sqlite3/lib',\n    'node_modules/better-sqlite3/package.json',\n    'node_modules/better-sqlite3/build/Release/better_sqlite3.node',\n    'node_modules/electron-font-manager/index.js',\n    'node_modules/electron-font-manager/package.json',\n    'node_modules/electron-font-manager/build/Release/font_manager.node',\n    'node_modules/node-gyp-build',\n    'node_modules/bufferutil',\n    'node_modules/utf-8-validate',\n    'build/Release/qrc_decode.node',\n    'dist/**/*',\n  ],\n  asar: {\n    smartUnpack: false,\n  },\n  extraResources: [\n    './licenses',\n  ],\n  publish: [\n    {\n      provider: 'github',\n      owner: 'lyswhut',\n      repo: 'lx-music-desktop',\n    },\n  ],\n}\n/**\n * @type {import('electron-builder').Configuration}\n * @see https://www.electron.build/configuration/configuration\n */\nconst winOptions = {\n  win: {\n    icon: './resources/icons/icon.ico',\n    legalTrademarks: 'lyswhut',\n    // artifactName: '${productName}-v${version}-${env.ARCH}-${env.TARGET}.${ext}',\n  },\n  nsis: {\n    oneClick: false,\n    language: '2052',\n    allowToChangeInstallationDirectory: true,\n    // differentialPackage: true,\n    license: './licenses/license.rtf',\n    shortcutName: 'LX Music',\n  },\n}\n/**\n * @type {import('electron-builder').Configuration}\n * @see https://www.electron.build/configuration/configuration\n */\nconst linuxOptions = {\n  linux: {\n    maintainer: 'lyswhut <lyswhut@qq.com>',\n    // artifactName: '${productName}-${version}.${env.ARCH}.${ext}',\n    icon: './resources/icons',\n    category: 'Utility;AudioVideo;Audio;Player;Music;',\n    desktop: {\n      // https://www.electron.build/app-builder-lib.interface.linuxdesktopfile\n      // https://www.electronjs.org/docs/latest/tutorial/linux-desktop-actions\n      // https://specifications.freedesktop.org/desktop-entry-spec/latest/example.html\n      // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html#desktop-files\n      entry: {\n        Name: 'LX Music',\n        'Name[zh_CN]': 'LX Music',\n        'Name[zh_TW]': 'LX Music',\n        Encoding: 'UTF-8',\n        MimeType: 'x-scheme-handler/lxmusic',\n        StartupNotify: 'false',\n      },\n    },\n  },\n  appImage: {\n    license: './licenses/license_zh.txt',\n    category: 'Utility;AudioVideo;Audio;Player;Music;',\n  },\n}\n/**\n * @type {import('electron-builder').Configuration}\n * @see https://www.electron.build/configuration/configuration\n */\nconst macOptions = {\n  mac: {\n    icon: './resources/icons/icon.icns',\n    category: 'public.app-category.music',\n    // artifactName: '${productName}-${version}.${ext}',\n  },\n  dmg: {\n    window: {\n      width: 530,\n      height: 380,\n    },\n    contents: [\n      {\n        x: 140,\n        y: 200,\n      },\n      {\n        x: 390,\n        y: 200,\n        type: 'link',\n        path: '/Applications',\n      },\n    ],\n    title: 'LX Music v${version}',\n  },\n}\n\n// win: {\n// tagret: {\n//   setup: ['nsis', '${productName}-v${version}-${env.ARCH}-Setup.${ext}'],\n//   green: ['7z', '${productName}-v${version}-${env.ARCH}-green.${ext}'],\n//   portable: ['portable', '${productName}-v${version}-${env.ARCH}-portable.${ext}'],\n// },\n// },\n// linux: {\n// platform: Platform.WINDOWS,\n// arch: {\n//   x64: builder.Arch.x64,\n//   arm64: builder.Arch.arm64,\n//   armv7l: builder.Arch.armv7l,\n// },\n// tagret: {\n//   deb: ['deb', '${productName}_${version}_${env.ARCH}.${ext}'],\n//   appImage: ['AppImage', '${productName}_${version}_${env.ARCH}.${ext}'],\n//   pacman: ['pacman', '${productName}_${version}_${env.ARCH}.${ext}'],\n//   rpm: ['rpm', '${productName}-${version}.${env.ARCH}.${ext}'],\n// },\n// },\n// mac: {\n// arch: {\n//   x64: builder.Arch.x64,\n//   x86: builder.Arch.ia32,\n//   arm64: builder.Arch.arm64,\n// },\n// tagret: {\n//   dmg: ['dmg', '${productName}-${version}-${env.ARCH}.${ext}'],\n// },\n// },\n\nconst createTarget = {\n  /**\n   *\n   * @param {*} arch\n   * @param {*} packageType\n   * @returns {{ buildOptions: import('electron-builder').CliOptions, options: import('electron-builder').Configuration }}\n   */\n  win(arch, packageType) {\n    switch (packageType) {\n      case 'setup':\n        winOptions.artifactName = `\\${productName}-v\\${version}-${arch}-Setup.\\${ext}`\n        return {\n          buildOptions: { win: ['nsis'] },\n          options: winOptions,\n        }\n      case 'green':\n        winOptions.artifactName = `\\${productName}-v\\${version}-win_${arch}-green.\\${ext}`\n        return {\n          buildOptions: { win: ['7z'] },\n          options: winOptions,\n        }\n      case 'win7_setup':\n        winOptions.artifactName = `\\${productName}-v\\${version}-win7_${arch}-Setup.\\${ext}`\n        return {\n          buildOptions: { win: ['nsis'] },\n          options: winOptions,\n        }\n      case 'win7_green':\n        winOptions.artifactName = `\\${productName}-v\\${version}-win7_${arch}-green.\\${ext}`\n        return {\n          buildOptions: { win: ['7z'] },\n          options: winOptions,\n        }\n      case 'portable':\n        winOptions.artifactName = `\\${productName}-v\\${version}-${arch}-portable.\\${ext}`\n        return {\n          buildOptions: { win: ['portable'] },\n          options: winOptions,\n        }\n      default: throw new Error('Unknown package type: ' + packageType)\n    }\n  },\n  /**\n   *\n   * @param {*} arch\n   * @param {*} packageType\n   * @returns {{ buildOptions: import('electron-builder').CliOptions, options: import('electron-builder').Configuration }}\n   */\n  linux(arch, packageType) {\n    switch (packageType) {\n      case 'deb':\n        linuxOptions.artifactName = `\\${productName}_\\${version}_${arch == 'x64' ? 'amd64' : arch}.\\${ext}`\n        return {\n          buildOptions: { linux: ['deb'] },\n          options: linuxOptions,\n        }\n      case 'appImage':\n        linuxOptions.artifactName = `\\${productName}_\\${version}_${arch}.\\${ext}`\n        return {\n          buildOptions: { linux: ['AppImage'] },\n          options: linuxOptions,\n        }\n      case 'pacman':\n        linuxOptions.artifactName = `\\${productName}_\\${version}_${arch}.\\${ext}`\n        return {\n          buildOptions: { linux: ['pacman'] },\n          options: linuxOptions,\n        }\n      case 'rpm':\n        linuxOptions.artifactName = `\\${productName}-\\${version}.${arch}.\\${ext}`\n        return {\n          buildOptions: { linux: ['rpm'] },\n          options: linuxOptions,\n        }\n      default: throw new Error('Unknown package type: ' + packageType)\n    }\n  },\n  /**\n   *\n   * @param {*} arch\n   * @param {*} packageType\n   * @returns {{ buildOptions: import('electron-builder').CliOptions, options: import('electron-builder').Configuration }}\n   */\n  mac(arch, packageType) {\n    switch (packageType) {\n      case 'dmg':\n        macOptions.artifactName = `\\${productName}-\\${version}-${arch}.\\${ext}`\n        return {\n          buildOptions: { mac: ['dmg'] },\n          options: macOptions,\n        }\n      default: throw new Error('Unknown package type: ' + packageType)\n    }\n  },\n}\n\n/**\n *\n * @param {'win' | 'mac' | 'linux' | 'dir'} target 构建目标平台\n * @param {'x86_64' | 'x64' | 'x86' | 'arm64' | 'armv7l'} arch 包架构\n * @param {*} packageType 包类型\n * @param {'onTagOrDraft' | 'always' | 'never'} publishType 发布类型\n */\nconst build = async(target, arch, packageType, publishType) => {\n  if (target == 'dir') {\n    await builder.build({\n      dir: true,\n      config: { ...options, ...winOptions, ...linuxOptions, ...macOptions },\n    })\n    return\n  }\n  const targetInfo = createTarget[target](arch, packageType)\n  // Promise is returned\n  await builder.build({\n    ...targetInfo.buildOptions,\n    publish: publishType ?? 'never',\n    x64: arch == 'x64' || arch == 'x86_64',\n    ia32: arch == 'x86' || arch == 'x86_64',\n    arm64: arch == 'arm64',\n    armv7l: arch == 'armv7l',\n    config: { ...options, ...targetInfo.options },\n  })\n  // .then((result) => {\n  //   console.log(JSON.stringify(result))\n  // })\n  // .catch((error) => {\n  //   console.error(error)\n  // })\n}\n\nconst params = {}\n\nfor (const param of process.argv.slice(2)) {\n  const [name, value] = param.split('=')\n  params[name] = value\n}\n\nif (params.target == null) throw new Error('Missing target')\nif (params.target != 'dir' && params.arch == null) throw new Error('Missing arch')\nif (params.target != 'dir' && params.type == null) throw new Error('Missing type')\n\nconsole.log(params.target, params.arch, params.type, params.publish ?? '')\nbuild(params.target, params.arch, params.type, params.publish)\n"
  },
  {
    "path": "build-config/css-loader.config.js",
    "content": "const isDev = process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  modules: {\n    localIdentName: isDev ? '[path][name]__[local]--[hash:base64:5]' : '[hash:base64:5]',\n    exportLocalsConvention: 'camelCase',\n    namedExport: false,\n  },\n  sourceMap: isDev,\n}\n"
  },
  {
    "path": "build-config/dependencies-patch.js",
    "content": "// 修补依赖源码以使vite构建的依赖恢复正常工作\n\nconst fs = require('node:fs')\nconst path = require('node:path')\n\nconst rootPath = path.join(__dirname, '../')\n\nconst patchs = [\n  [\n    path.join(rootPath, './node_modules/ws/package.json'),\n    '\\n      \"browser\": \"./browser.js\",',\n    '',\n  ],\n  [\n    path.join(rootPath, './node_modules/music-metadata/package.json'),\n    '\"default\": \"./lib/core.js\"',\n    '\"default\": \"./lib/index.js\"',\n  ],\n  [\n    path.join(rootPath, './node_modules/strtok3/package.json'),\n    '\"default\": \"./lib/core.js\"',\n    '\"default\": \"./lib/index.js\"',\n  ],\n]\n\n;(async() => {\n  for (const [filePath, fromStr, toStr] of patchs) {\n    console.log(`Patching ${filePath.replace(rootPath, '')}`)\n    try {\n      const file = (await fs.promises.readFile(filePath)).toString()\n      await fs.promises.writeFile(filePath, file.replace(fromStr, toStr))\n    } catch (err) {\n      console.error(`Patch ${filePath.replace(rootPath, '')} failed: ${err.message}`)\n    }\n  }\n  console.log('\\nDependencies patch finished.\\n')\n})()\n\n"
  },
  {
    "path": "build-config/lib-update.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst tar = require('tar')\n\nconst libDir = path.join(__dirname, 'lib')\n\nconst getGzipFiles = async() => {\n  const names = await fs.promises.readdir(libDir)\n  // for (const name of names) {\n  //   if (name.endsWith('.node')) await fs.promises.unlink(path.join(libDir, name))\n  // }\n  return names.filter(name => name.endsWith('.gz'))\n}\n\nconst unzip = async(filePath) => {\n  const targetDir = filePath.replace('.tar.gz', '')\n  if (fs.existsSync(targetDir)) await fs.promises.rm(targetDir, { recursive: true })\n  await fs.promises.mkdir(targetDir)\n  await tar.x({\n    file: filePath,\n    strip: 1,\n    C: targetDir,\n  })\n  return targetDir\n}\n\nconst files = [\n  'qrc_decode',\n  'better_sqlite3',\n]\nconst moveFile = async(filePath) => {\n  const name = 'electron-' + path.basename(filePath).split('-electron-')[1]\n  for (const fileName of files) {\n    if (fileName == 'better_sqlite3' && !name.includes('linux')) continue\n    const targetPath = path.join(libDir, `${fileName}_${name}.node`)\n    if (fs.existsSync(targetPath)) await fs.promises.unlink(targetPath)\n    await fs.promises.rename(path.join(filePath, 'Release', fileName + '.node'), targetPath)\n  }\n  await fs.promises.rm(filePath, { recursive: true })\n}\n\nconst run = async() => {\n  const files = await getGzipFiles()\n  for (const name of files) {\n    await moveFile(await unzip(path.join(libDir, name)))\n  }\n  for (const name of files) {\n    await fs.promises.unlink(path.join(libDir, name))\n  }\n}\n\nrun()\n\n"
  },
  {
    "path": "build-config/main/webpack.config.base.js",
    "content": "const path = require('path')\nconst ESLintPlugin = require('eslint-webpack-plugin')\n\nconst isDev = process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  target: 'electron-main',\n  output: {\n    filename: '[name].js',\n    library: {\n      type: 'commonjs2',\n    },\n    path: path.join(__dirname, '../../dist'),\n  },\n  externals: {\n    'font-list': 'font-list',\n    'better-sqlite3': 'better-sqlite3',\n    'electron-font-manager': 'electron-font-manager',\n    bufferutil: 'bufferutil',\n    'utf-8-validate': 'utf-8-validate',\n    'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),\n  },\n  resolve: {\n    alias: {\n      '@main': path.join(__dirname, '../../src/main'),\n      '@renderer': path.join(__dirname, '../../src/renderer'),\n      '@lyric': path.join(__dirname, '../../src/renderer-lyric'),\n      '@common': path.join(__dirname, '../../src/common'),\n    },\n    extensions: ['.tsx', '.ts', '.js', '.mjs', '.json', '.node'],\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.node$/,\n        use: 'node-loader',\n      },\n      {\n        test: /\\.tsx?$/,\n        use: 'ts-loader',\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  plugins: [\n    new ESLintPlugin(),\n  ],\n}\n"
  },
  {
    "path": "build-config/main/webpack.config.dev.js",
    "content": "const path = require('path')\nconst { merge } = require('webpack-merge')\nconst webpack = require('webpack')\n\nconst baseConfig = require('./webpack.config.base')\n\n\nmodule.exports = merge(baseConfig, {\n  mode: 'development',\n  entry: {\n    main: path.join(__dirname, '../../src/main/index-dev.ts'),\n    // 'dbService.worker': path.join(__dirname, '../../src/main/worker/dbService/index.ts'),\n  },\n  devtool: 'eval-source-map',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"development\"',\n      },\n      webpackStaticPath: `\"${path.join(__dirname, '../../src/static').replace(/\\\\/g, '\\\\\\\\')}\"`,\n      webpackUserApiPath: `\"${path.join(__dirname, '../../src/main/modules/userApi').replace(/\\\\/g, '\\\\\\\\')}\"`,\n    }),\n  ],\n  performance: {\n    maxEntrypointSize: 1024 * 1024 * 50,\n    maxAssetSize: 1024 * 1024 * 30,\n  },\n})\n"
  },
  {
    "path": "build-config/main/webpack.config.prod.js",
    "content": "const path = require('path')\nconst { merge } = require('webpack-merge')\nconst webpack = require('webpack')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\nconst baseConfig = require('./webpack.config.base')\n\n// const { dependencies } = require('../../package.json')\n\n// const buildConfig = require('../webpack-build-config')\n\n\nmodule.exports = merge(baseConfig, {\n  mode: 'production',\n  devtool: false,\n  entry: {\n    main: path.join(__dirname, '../../src/main/index.ts'),\n    // 'dbService.worker': path.join(__dirname, '../../src/main/worker/dbService/index.ts'),\n  },\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n  plugins: [\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: path.join(__dirname, '../../src/main/modules/userApi/renderer/user-api.html'),\n          to: path.join(__dirname, '../../dist/userApi/renderer/user-api.html'),\n        },\n        {\n          from: path.join(__dirname, '../../src/common/theme/images/*').replace(/\\\\/g, '/'),\n          to: path.join(__dirname, '../../dist/theme_images/[name][ext]'),\n        },\n      ],\n    }),\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"production\"',\n      },\n    }),\n  ],\n  performance: {\n    maxEntrypointSize: 1024 * 1024 * 10,\n    maxAssetSize: 1024 * 1024 * 20,\n  },\n  optimization: {\n    minimize: false,\n  },\n})\n"
  },
  {
    "path": "build-config/pack.js",
    "content": "process.env.NODE_ENV = 'production'\n\nconst chalk = require('chalk')\nconst del = require('del')\nconst webpack = require('webpack')\nconst Spinnies = require('spinnies')\n\nconst mainConfig = './main/webpack.config.prod'\nconst rendererConfig = './renderer/webpack.config.prod'\nconst rendererLyricConfig = './renderer-lyric/webpack.config.prod'\nconst rendererScriptConfig = './renderer-scripts/webpack.config.prod'\n\nconst errorLog = chalk.bgRed.white(' ERROR ') + ' '\nconst okayLog = chalk.bgGreen.white(' OKAY ') + ' '\n\nconst { Worker, isMainThread, parentPort } = require('worker_threads')\n\n\nfunction build() {\n  console.time('build')\n  del.sync(['dist/**', 'build/**'])\n\n  const spinners = new Spinnies({ color: 'blue' })\n  spinners.add('main', { text: 'main building' })\n  spinners.add('renderer', { text: 'renderer building' })\n  spinners.add('renderer-lyric', { text: 'renderer-lyric building' })\n  spinners.add('renderer-scripts', { text: 'renderer-scripts building' })\n  let results = ''\n\n  // m.on('success', () => {\n  //   process.stdout.write('\\x1B[2J\\x1B[0f')\n  //   console.log(`\\n\\n${results}`)\n  //   console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\\n`)\n  //   process.exit()\n  // })\n  function handleSuccess() {\n    process.stdout.write('\\x1B[2J\\x1B[0f')\n    console.log(`\\n\\n${results}`)\n    console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\\n`)\n    console.timeEnd('build')\n    process.exit()\n  }\n\n  Promise.all([\n    pack(mainConfig).then(result => {\n      results += result + '\\n\\n'\n      spinners.succeed('main', { text: 'main build success!' })\n    }).catch(err => {\n      spinners.fail('main', { text: 'main build fail :(' })\n      console.log(`\\n  ${errorLog}failed to build main process`)\n      console.error(`\\n${err}\\n`)\n      process.exit(1)\n    }),\n    pack(rendererConfig).then(result => {\n      results += result + '\\n\\n'\n      spinners.succeed('renderer', { text: 'renderer build success!' })\n    }).catch(err => {\n      spinners.fail('renderer', { text: 'renderer build fail :(' })\n      console.log(`\\n  ${errorLog}failed to build renderer process`)\n      console.error(`\\n${err}\\n`)\n      process.exit(1)\n    }),\n    pack(rendererLyricConfig).then(result => {\n      results += result + '\\n\\n'\n      spinners.succeed('renderer-lyric', { text: 'renderer-lyric build success!' })\n    }).catch(err => {\n      spinners.fail('renderer-lyric', { text: 'renderer-lyric build fail :(' })\n      console.log(`\\n  ${errorLog}failed to build renderer-lyric process`)\n      console.error(`\\n${err}\\n`)\n      process.exit(1)\n    }),\n    pack(rendererScriptConfig).then(result => {\n      results += result + '\\n\\n'\n      spinners.succeed('renderer-scripts', { text: 'renderer-scripts build success!' })\n    }).catch(err => {\n      spinners.fail('renderer-scripts', { text: 'renderer-scripts build fail :(' })\n      console.log(`\\n  ${errorLog}failed to build renderer-scripts process`)\n      console.error(`\\n${err}\\n`)\n      process.exit(1)\n    }),\n  ]).then(handleSuccess)\n}\n\nfunction pack(config) {\n  return new Promise((resolve, reject) => {\n    const worker = new Worker(__filename)\n    const subChannel = new MessageChannel()\n    worker.postMessage({ port: subChannel.port1, config }, [subChannel.port1])\n    subChannel.port2.on('message', ({ status, message }) => {\n      switch (status) {\n        case 'success': return resolve(message)\n        case 'error': return reject(message)\n      }\n    })\n  })\n}\n\nfunction runPack(config) {\n  return new Promise((resolve, reject) => {\n    config = require(config)\n    config.mode = 'production'\n    webpack(config, (err, stats) => {\n      if (err) reject(err.stack || err)\n      else if (stats.hasErrors()) {\n        let err = ''\n\n        stats.toString({\n          chunks: false,\n          modules: false,\n          colors: true,\n        })\n          .split(/\\r?\\n/)\n          .forEach(line => {\n            err += `    ${line}\\n`\n          })\n\n        reject(err)\n      } else {\n        resolve(stats.toString({\n          chunks: false,\n          colors: true,\n        }))\n      }\n    })\n  })\n}\n\nif (isMainThread) build()\nelse {\n  parentPort.once('message', ({ port, config }) => {\n    // assert(port instanceof MessagePort)\n    runPack(config).then((result) => {\n      port.postMessage({\n        status: 'success',\n        message: result,\n      })\n    }).catch((err) => {\n      port.postMessage({\n        status: 'error',\n        message: err,\n      })\n    }).finally(() => {\n      port.close()\n    })\n  })\n}\n"
  },
  {
    "path": "build-config/post-install.js",
    "content": "const { Arch } = require('electron-builder')\nrequire('./build-before-pack')({ electronPlatformName: process.platform, arch: Arch[process.arch] })\n"
  },
  {
    "path": "build-config/renderer/webpack.config.base.js",
    "content": "const path = require('path')\nconst { VueLoaderPlugin } = require('vue-loader')\nconst HTMLPlugin = require('html-webpack-plugin')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst ESLintPlugin = require('eslint-webpack-plugin')\n\nconst vueLoaderConfig = require('../vue-loader.config')\nconst { mergeCSSLoader } = require('../utils')\n\nconst isDev = process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  target: 'electron-renderer',\n  entry: {\n    renderer: path.join(__dirname, '../../src/renderer/main.ts'),\n  },\n  output: {\n    filename: '[name].js',\n    library: {\n      type: 'commonjs2',\n    },\n    path: path.join(__dirname, '../../dist'),\n    publicPath: '',\n  },\n  resolve: {\n    alias: {\n      '@root': path.join(__dirname, '../../src'),\n      '@main': path.join(__dirname, '../../src/main'),\n      '@renderer': path.join(__dirname, '../../src/renderer'),\n      '@lyric': path.join(__dirname, '../../src/renderer-lyric'),\n      '@static': path.join(__dirname, '../../src/static'),\n      '@common': path.join(__dirname, '../../src/common'),\n    },\n    extensions: ['.tsx', '.ts', '.js', '.json', '.node'],\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        exclude: /node_modules/,\n        use: {\n          loader: 'ts-loader',\n          options: {\n            appendTsSuffixTo: [/\\.vue$/],\n          },\n        },\n        parser: {\n          worker: [\n            '*audioContext.audioWorklet.addModule()',\n            '...',\n          ],\n        },\n      },\n      {\n        test: /\\.node$/,\n        use: 'node-loader',\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: vueLoaderConfig,\n      },\n      {\n        test: /\\.pug$/,\n        loader: 'pug-plain-loader',\n      },\n      {\n        test: /\\.css$/,\n        oneOf: mergeCSSLoader(),\n      },\n      {\n        test: /\\.less$/,\n        oneOf: mergeCSSLoader({\n          loader: 'less-loader',\n          options: {\n            sourceMap: true,\n          },\n        }),\n      },\n      {\n        test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n        exclude: path.join(__dirname, '../../src/renderer/assets/svgs'),\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'imgs/[name]-[contenthash:8][ext]',\n        },\n      },\n      {\n        test: /\\.svg$/,\n        include: path.join(__dirname, '../../src/renderer/assets/svgs'),\n        use: [\n          {\n            loader: 'svg-sprite-loader',\n            options: {\n              symbolId: 'icon-[name]',\n            },\n          },\n          'svg-transform-loader',\n          'svgo-loader',\n        ],\n      },\n      {\n        test: /\\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'media/[name]-[contenthash:8][ext]',\n        },\n      },\n      {\n        test: /\\.(woff2?|eot|ttf|otf)(\\?.*)?$/,\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'fonts/[name]-[contenthash:8][ext]',\n        },\n      },\n    ],\n  },\n  plugins: [\n    new HTMLPlugin({\n      filename: 'index.html',\n      template: path.join(__dirname, '../../src/renderer/index.html'),\n      isProd: process.env.NODE_ENV == 'production',\n      browser: process.browser,\n      __dirname,\n    }),\n    new VueLoaderPlugin(),\n    new MiniCssExtractPlugin({\n      // Options similar to the same options in webpackOptions.output\n      // both options are optional\n      filename: isDev ? '[name].css' : '[name].[contenthash:8].css',\n      chunkFilename: isDev ? '[id].css' : '[id].[contenthash:8].css',\n    }),\n    new ESLintPlugin({\n      extensions: ['js', 'vue'],\n      formatter: require('eslint-formatter-friendly'),\n    }),\n  ],\n}\n"
  },
  {
    "path": "build-config/renderer/webpack.config.dev.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\n\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\n\nconst gitInfo = {\n  commit_id: '',\n  commit_date: '',\n}\n\nmodule.exports = merge(baseConfig, {\n  mode: 'development',\n  devtool: 'eval-source-map',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"development\"',\n        ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',\n      },\n      // ENVIRONMENT: 'process.env',\n      __VUE_OPTIONS_API__: 'true',\n      __VUE_PROD_DEVTOOLS__: 'false',\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',\n      COMMIT_ID: `\"${gitInfo.commit_id}\"`,\n      COMMIT_DATE: `\"${gitInfo.commit_date}\"`,\n      staticPath: `\"${path.join(__dirname, '../../src/static').replace(/\\\\/g, '\\\\\\\\')}\"`,\n    }),\n  ],\n  performance: {\n    hints: false,\n  },\n})\n\n"
  },
  {
    "path": "build-config/renderer/webpack.config.prod.js",
    "content": "const path = require('path')\nconst { execSync } = require('child_process')\nconst webpack = require('webpack')\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin')\nconst TerserPlugin = require('terser-webpack-plugin')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\nconst buildConfig = require('../webpack-build-config')\n\n// const { dependencies } = require('../../package.json')\n\n// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']\n\nconst gitInfo = {\n  commit_id: '',\n  commit_date: '',\n}\n\ntry {\n  let isClean = !execSync('git status --porcelain').toString().trim()\n  if (process.env.BUILD_WIN7) {\n    console.warn('BUILD_WIN7 is set, skipping git status check.')\n    console.log('Workspace status:', execSync('git status --porcelain').toString().trim())\n    isClean = true\n  }\n  if (isClean) {\n    gitInfo.commit_id = execSync('git log -1 --pretty=format:\"%H\"').toString().trim()\n    gitInfo.commit_date = execSync('git log -1 --pretty=format:\"%ad\" --date=iso-strict').toString().trim()\n  } else if (process.env.IS_CI) {\n    throw new Error('Working directory is not clean')\n  }\n} catch {}\n\nmodule.exports = merge(baseConfig, {\n  mode: 'production',\n  devtool: 'source-map',\n  externals: [\n    // ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  plugins: [\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: path.join(__dirname, '../../src/static'),\n          to: path.join(__dirname, '../../dist/static'),\n        },\n      ],\n    }),\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"production\"',\n      },\n      // ENVIRONMENT: 'process.env',\n      __VUE_OPTIONS_API__: 'true',\n      __VUE_PROD_DEVTOOLS__: 'false',\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',\n      COMMIT_ID: `\"${gitInfo.commit_id}\"`,\n      COMMIT_DATE: `\"${gitInfo.commit_date}\"`,\n    }),\n  ],\n  optimization: {\n    minimize: buildConfig.minimize,\n    minimizer: [\n      new TerserPlugin(),\n      new CssMinimizerPlugin(),\n    ],\n    splitChunks: {\n      chunks: 'initial',\n      minChunks: 2,\n    },\n  },\n  performance: {\n    maxEntrypointSize: 1024 * 1024 * 10,\n    maxAssetSize: 1024 * 1024 * 20,\n    hints: 'warning',\n  },\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n})\n\n\n"
  },
  {
    "path": "build-config/renderer-lyric/webpack.config.base.js",
    "content": "const path = require('path')\nconst { VueLoaderPlugin } = require('vue-loader')\nconst HTMLPlugin = require('html-webpack-plugin')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst ESLintPlugin = require('eslint-webpack-plugin')\n\nconst vueLoaderConfig = require('../vue-loader.config')\nconst { mergeCSSLoader } = require('../utils')\n\nconst isDev = process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  target: 'electron-renderer',\n  entry: {\n    'renderer-lyric': path.join(__dirname, '../../src/renderer-lyric/main.ts'),\n  },\n  output: {\n    filename: '[name].js',\n    library: {\n      type: 'commonjs2',\n    },\n    path: path.join(__dirname, '../../dist'),\n    publicPath: '',\n  },\n  resolve: {\n    alias: {\n      '@root': path.join(__dirname, '../../src'),\n      '@main': path.join(__dirname, '../../src/main'),\n      '@renderer': path.join(__dirname, '../../src/renderer'),\n      '@lyric': path.join(__dirname, '../../src/renderer-lyric'),\n      '@static': path.join(__dirname, '../../src/static'),\n      '@common': path.join(__dirname, '../../src/common'),\n    },\n    extensions: ['.tsx', '.ts', '.js', '.json', '.node'],\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        exclude: /node_modules/,\n        use: {\n          loader: 'ts-loader',\n          options: {\n            appendTsSuffixTo: [/\\.vue$/],\n          },\n        },\n      },\n      {\n        test: /\\.node$/,\n        use: 'node-loader',\n      },\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: vueLoaderConfig,\n      },\n      {\n        test: /\\.pug$/,\n        loader: 'pug-plain-loader',\n      },\n      {\n        test: /\\.css$/,\n        oneOf: mergeCSSLoader(),\n      },\n      {\n        test: /\\.less$/,\n        oneOf: mergeCSSLoader({\n          loader: 'less-loader',\n          options: {\n            sourceMap: true,\n          },\n        }),\n      },\n      {\n        test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n        exclude: path.join(__dirname, '../../src/renderer/assets/svgs'),\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'imgs/[name]-[contenthash:8][ext]',\n        },\n      },\n      {\n        test: /\\.svg$/,\n        include: path.join(__dirname, '../../src/renderer/assets/svgs'),\n        use: [\n          {\n            loader: 'svg-sprite-loader',\n            options: {\n              symbolId: 'icon-[name]',\n            },\n          },\n          'svg-transform-loader',\n          'svgo-loader',\n        ],\n      },\n      {\n        test: /\\.(mp4|webm|ogg|mp3|wav|flac|aac)(\\?.*)?$/,\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'media/[name]-[contenthash:8][ext]',\n        },\n      },\n      {\n        test: /\\.(woff2?|eot|ttf|otf)(\\?.*)?$/,\n        type: 'asset',\n        parser: {\n          dataUrlCondition: {\n            maxSize: 10000,\n          },\n        },\n        generator: {\n          filename: 'fonts/[name]-[contenthash:8][ext]',\n        },\n      },\n    ],\n  },\n  plugins: [\n    new HTMLPlugin({\n      filename: 'lyric.html',\n      template: path.join(__dirname, '../../src/renderer-lyric/index.html'),\n      isProd: process.env.NODE_ENV == 'production',\n      browser: process.browser,\n      __dirname,\n    }),\n    new VueLoaderPlugin(),\n    new MiniCssExtractPlugin({\n      // Options similar to the same options in webpackOptions.output\n      // both options are optional\n      filename: isDev ? '[name].css' : '[name].[contenthash:8].css',\n      chunkFilename: isDev ? '[id].css' : '[id].[contenthash:8].css',\n    }),\n    new ESLintPlugin({\n      extensions: ['js', 'vue'],\n      formatter: require('eslint-formatter-friendly'),\n    }),\n  ],\n}\n"
  },
  {
    "path": "build-config/renderer-lyric/webpack.config.dev.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\n\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\n\nmodule.exports = merge(baseConfig, {\n  mode: 'development',\n  devtool: 'eval-source-map',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"development\"',\n        ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',\n      },\n      __VUE_OPTIONS_API__: 'true',\n      __VUE_PROD_DEVTOOLS__: 'false',\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',\n      staticPath: `\"${path.join(__dirname, '../../src/static').replace(/\\\\/g, '\\\\\\\\')}\"`,\n    }),\n  ],\n  performance: {\n    hints: false,\n  },\n})\n\n"
  },
  {
    "path": "build-config/renderer-lyric/webpack.config.prod.js",
    "content": "// const path = require('path')\nconst webpack = require('webpack')\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin')\nconst TerserPlugin = require('terser-webpack-plugin')\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\nconst buildConfig = require('../webpack-build-config')\n\n// const { dependencies } = require('../../package.json')\n\n// let whiteListedModules = ['vue']\n// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']\n\n\nmodule.exports = merge(baseConfig, {\n  mode: 'production',\n  devtool: 'source-map',\n  externals: [\n    // ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"production\"',\n      },\n      __VUE_OPTIONS_API__: 'true',\n      __VUE_PROD_DEVTOOLS__: 'false',\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',\n    }),\n  ],\n  optimization: {\n    minimize: buildConfig.minimize,\n    minimizer: [\n      new TerserPlugin(),\n      new CssMinimizerPlugin(),\n    ],\n  },\n  performance: {\n    maxEntrypointSize: 1024 * 1024 * 10,\n    maxAssetSize: 1024 * 1024 * 20,\n    hints: 'warning',\n  },\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n})\n\n\n"
  },
  {
    "path": "build-config/renderer-scripts/webpack.config.base.js",
    "content": "const path = require('path')\nconst ESLintPlugin = require('eslint-webpack-plugin')\n\nmodule.exports = {\n  target: 'electron-renderer',\n  entry: {\n    'user-api-preload': path.join(__dirname, '../../src/main/modules/userApi/renderer/preload.js'),\n  },\n  output: {\n    filename: '[name].js',\n    library: {\n      type: 'commonjs2',\n    },\n    path: path.join(__dirname, '../../dist'),\n    publicPath: '',\n  },\n  resolve: {\n    alias: {\n      '@root': path.join(__dirname, '../../src'),\n      '@main': path.join(__dirname, '../../src/main'),\n      '@renderer': path.join(__dirname, '../../src/renderer'),\n      '@lyric': path.join(__dirname, '../../src/renderer-lyric'),\n      '@static': path.join(__dirname, '../../src/static'),\n      '@common': path.join(__dirname, '../../src/common'),\n    },\n    extensions: ['.tsx', '.ts', '.js', '.json', '.node'],\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        exclude: /node_modules/,\n        use: {\n          loader: 'ts-loader',\n          options: {\n            appendTsSuffixTo: [/\\.vue$/],\n          },\n        },\n      },\n      {\n        test: /\\.node$/,\n        use: 'node-loader',\n      },\n    ],\n  },\n  plugins: [\n    new ESLintPlugin({\n      extensions: ['js'],\n      formatter: require('eslint-formatter-friendly'),\n    }),\n  ],\n}\n"
  },
  {
    "path": "build-config/renderer-scripts/webpack.config.dev.js",
    "content": "const path = require('path')\nconst webpack = require('webpack')\n\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\n\nmodule.exports = merge(baseConfig, {\n  mode: 'development',\n  devtool: 'eval-source-map',\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"development\"',\n        ELECTRON_DISABLE_SECURITY_WARNINGS: 'true',\n      },\n      staticPath: `\"${path.join(__dirname, '../../src/static').replace(/\\\\/g, '\\\\\\\\')}\"`,\n    }),\n  ],\n  performance: {\n    hints: false,\n  },\n})\n\n"
  },
  {
    "path": "build-config/renderer-scripts/webpack.config.prod.js",
    "content": "// const path = require('path')\nconst webpack = require('webpack')\nconst TerserPlugin = require('terser-webpack-plugin')\nconst { merge } = require('webpack-merge')\n\nconst baseConfig = require('./webpack.config.base')\nconst buildConfig = require('../webpack-build-config')\n\n// const { dependencies } = require('../../package.json')\n\n// let whiteListedModules = ['vue']\n// let whiteListedModules = ['vue', 'vue-router', 'vuex', 'vue-i18n']\n\n\nmodule.exports = merge(baseConfig, {\n  mode: 'production',\n  devtool: 'source-map',\n  externals: [\n    // ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),\n  ],\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: '\"production\"',\n      },\n    }),\n  ],\n  optimization: {\n    minimize: buildConfig.minimize,\n    minimizer: [\n      new TerserPlugin(),\n    ],\n  },\n  performance: {\n    maxEntrypointSize: 1024 * 1024 * 10,\n    maxAssetSize: 1024 * 1024 * 20,\n    hints: 'warning',\n  },\n  node: {\n    __dirname: false,\n    __filename: false,\n  },\n})\n\n\n"
  },
  {
    "path": "build-config/runner-dev.js",
    "content": "process.env.NODE_ENV = 'development'\n\nconst chalk = require('chalk')\nconst electron = require('electron')\nconst path = require('path')\n// const { say } = require('cfonts')\nconst { spawn } = require('child_process')\nconst webpack = require('webpack')\nconst WebpackDevServer = require('webpack-dev-server')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst webpackHotMiddleware = require('webpack-hot-middleware')\n\nconst mainConfig = require('./main/webpack.config.dev')\nconst rendererConfig = require('./renderer/webpack.config.dev')\nconst rendererLyricConfig = require('./renderer-lyric/webpack.config.dev')\nconst rendererScriptConfig = require('./renderer-scripts/webpack.config.dev')\nconst { Arch } = require('electron-builder')\nconst replaceLib = require('./build-before-pack')\nconst treeKill = require('tree-kill')\nconst { debounce } = require('./utils')\n\nlet electronProcess = null\nlet hotMiddlewareRenderer\nlet hotMiddlewareRendererLyric\n\n\nfunction startRenderer() {\n  return new Promise((resolve, reject) => {\n    // rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)\n    // rendererConfig.mode = 'development'\n    const compiler = webpack(rendererConfig)\n    hotMiddlewareRenderer = webpackHotMiddleware(compiler, {\n      log: false,\n      heartbeat: 2500,\n    })\n\n    compiler.hooks.compilation.tap('compilation', compilation => {\n      // console.log(Object.keys(compilation.hooks))\n      HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {\n        hotMiddlewareRenderer.publish({ action: 'reload' })\n        cb()\n      })\n    })\n\n    // compiler.hooks.done.tap('done', stats => {\n    //   // logStats('Renderer', 'Compile done')\n    //   // logStats('Renderer', stats)\n    // })\n\n    const server = new WebpackDevServer({\n      port: 9080,\n      hot: true,\n      historyApiFallback: true,\n      static: {\n        directory: path.join(__dirname, '../src/common/theme/images'),\n        publicPath: '/theme_images',\n      },\n      client: {\n        logging: 'warn',\n        overlay: true,\n      },\n      setupMiddlewares(middlewares, devServer) {\n        devServer.app.use(hotMiddlewareRenderer)\n        setImmediate(() => {\n          devServer.middleware.waitUntilValid(resolve)\n        })\n\n        return middlewares\n      },\n    }, compiler)\n\n    server.start()\n  })\n}\n\nfunction startRendererLyric() {\n  return new Promise((resolve, reject) => {\n    // rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)\n    // rendererConfig.mode = 'development'\n    const compiler = webpack(rendererLyricConfig)\n    hotMiddlewareRendererLyric = webpackHotMiddleware(compiler, {\n      log: false,\n      heartbeat: 2500,\n    })\n\n    compiler.hooks.compilation.tap('compilation', compilation => {\n      // console.log(Object.keys(compilation.hooks))\n      HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {\n        hotMiddlewareRendererLyric.publish({ action: 'reload' })\n        cb()\n      })\n    })\n\n    // compiler.hooks.done.tap('done', stats => {\n    //   // logStats('Renderer', 'Compile done')\n    //   // logStats('Renderer', stats)\n    // })\n\n    const server = new WebpackDevServer({\n      port: 9081,\n      hot: true,\n      historyApiFallback: true,\n      // static: {\n      //   directory: path.join(__dirname, '../'),\n      // },\n      client: {\n        logging: 'warn',\n        overlay: true,\n      },\n      setupMiddlewares(middlewares, devServer) {\n        devServer.app.use(hotMiddlewareRenderer)\n        setImmediate(() => {\n          devServer.middleware.waitUntilValid(resolve)\n        })\n        return middlewares\n      },\n    }, compiler)\n\n    server.start()\n  })\n}\n\nfunction startRendererScripts() {\n  return new Promise((resolve, reject) => {\n    // mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)\n    // mainConfig.mode = 'development'\n    const compiler = webpack(rendererScriptConfig)\n\n    compiler.watch({}, (err, stats) => {\n      if (err) {\n        console.log(err)\n        return\n      }\n      resolve()\n    })\n  })\n}\n\nfunction startMain() {\n  let firstRun = true\n  return new Promise((resolve, reject) => {\n    // mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)\n    // mainConfig.mode = 'development'\n    const runElectronDelay = debounce(startElectron, 200)\n    const compiler = webpack(mainConfig)\n\n    compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {\n      hotMiddlewareRenderer.publish({ action: 'compiling' })\n      hotMiddlewareRendererLyric.publish({ action: 'compiling' })\n      done()\n    })\n\n    compiler.watch({}, (err, stats) => {\n      if (err) {\n        console.log(err)\n        reject(err)\n        return\n      }\n\n      // logStats('Main', stats)\n      if (electronProcess) {\n        electronProcess.removeAllListeners()\n        treeKill(electronProcess.pid)\n      }\n      if (firstRun) {\n        firstRun = false\n        resolve()\n      } else runElectronDelay()\n    })\n  })\n}\n\nfunction startElectron() {\n  let args = [\n    '--inspect=5858',\n    // 'NODE_ENV=development',\n    path.join(__dirname, '../dist/main.js'),\n  ]\n\n  // detect yarn or npm and process commandline args accordingly\n  if (process.env.npm_execpath.endsWith('yarn.js')) {\n    args = args.concat(process.argv.slice(3))\n  } else if (process.env.npm_execpath.endsWith('npm-cli.js')) {\n    args = args.concat(process.argv.slice(2))\n  }\n\n  electronProcess = spawn(electron, args)\n\n  electronProcess.stdout.on('data', data => {\n    electronLog(data, 'blue')\n  })\n  electronProcess.stderr.on('data', data => {\n    electronLog(data, 'red')\n  })\n\n  electronProcess.on('close', () => {\n    process.exit()\n  })\n}\n\nconst logs = [\n  'Manifest version 2 is deprecated, and support will be removed in 2023',\n  '\"Extension server error: Operation failed: Permission denied\", source: devtools://devtools/bundled',\n\n  // https://github.com/electron/electron/issues/32133\n  '\"Electron sandbox_bundle.js script failed to run\"',\n  '\"TypeError: object null is not iterable (cannot read property Symbol(Symbol.iterator))\",',\n]\nfunction electronLog(data, color) {\n  let log = data.toString()\n  if (/[0-9A-z]+/.test(log)) {\n    // 抑制某些无关的报错日志\n    if (color == 'red' && typeof log === 'string' && logs.some(l => log.includes(l))) return\n\n    console.log(chalk[color](log))\n  }\n}\n\nfunction init() {\n  const Spinnies = require('spinnies')\n  const spinners = new Spinnies({ color: 'blue' })\n  spinners.add('main', { text: 'main compiling' })\n  spinners.add('renderer', { text: 'renderer compiling' })\n  spinners.add('renderer-lyric', { text: 'renderer-lyric compiling' })\n  spinners.add('renderer-scripts', { text: 'renderer-scripts compiling' })\n  function handleSuccess(name) {\n    spinners.succeed(name, { text: name + ' compile success!' })\n  }\n  function handleFail(name) {\n    spinners.fail(name, { text: name + ' compile fail!' })\n  }\n  replaceLib({ electronPlatformName: process.platform, arch: Arch[process.arch] })\n\n  Promise.all([\n    startRenderer().then(() => handleSuccess('renderer')).catch((err) => {\n      console.error(err.message)\n      return handleFail('renderer')\n    }),\n    startRendererLyric().then(() => handleSuccess('renderer-lyric')).catch((err) => {\n      console.error(err.message)\n      return handleFail('renderer-lyric')\n    }),\n    startRendererScripts().then(() => handleSuccess('renderer-scripts')).catch((err) => {\n      console.error(err.message)\n      return handleFail('renderer-scripts')\n    }),\n    startMain().then(() => handleSuccess('main')).catch(() => handleFail('main')),\n  ]).then(startElectron).catch(err => {\n    console.error(err)\n  })\n}\n\ninit()\n"
  },
  {
    "path": "build-config/utils.js",
    "content": "const MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst cssLoaderConfig = require('./css-loader.config')\nconst chalk = require('chalk')\n\n// merge css-loader\nexports.mergeCSSLoader = beforeLoader => {\n  const loader = [\n    // 这里匹配 `<style module>`\n    {\n      resourceQuery: /module/,\n      use: [\n        {\n          loader: MiniCssExtractPlugin.loader,\n          options: {\n            esModule: false,\n          },\n        },\n        {\n          loader: 'css-loader',\n          options: cssLoaderConfig,\n        },\n        'postcss-loader',\n      ],\n    },\n    // 这里匹配普通的 `<style>` 或 `<style scoped>`\n    {\n      use: [\n        {\n          loader: MiniCssExtractPlugin.loader,\n          options: {\n            esModule: false,\n          },\n        },\n        {\n          loader: 'css-loader',\n          options: {\n            esModule: false,\n          },\n        },\n        'postcss-loader',\n      ],\n    },\n  ]\n  if (beforeLoader) {\n    loader[0].use.push(beforeLoader)\n    loader[1].use.push(beforeLoader)\n  }\n  return loader\n}\n\nexports.logStats = (proc, data) => {\n  let log = ''\n\n  log += chalk.yellow.bold(`${proc} Process：`)\n  log += '\\n'\n\n  if (typeof data === 'object') {\n    data.toString({\n      colors: true,\n      chunks: false,\n    }).split(/\\r?\\n/).forEach(line => {\n      log += '  ' + line + '\\n'\n    })\n  } else {\n    log += `  ${data}\\n`\n  }\n\n  console.log(log)\n}\n\nexports.debounce = (fn, delay = 100) => {\n  let timer = null\n  let _args\n  return (...args) => {\n    _args = args\n    if (timer) clearTimeout(timer)\n    timer = setTimeout(() => {\n      timer = null\n      fn(..._args)\n    }, delay)\n  }\n}\n"
  },
  {
    "path": "build-config/vue-loader.config.js",
    "content": "const isDev = process.env.NODE_ENV === 'development'\n\nmodule.exports = {\n  // preserveWhitepace: true,\n  compilerOptions: {\n    whitespace: 'preserve',\n  },\n  extractCSS: !isDev,\n  // cssModules: {\n  //   localIndetName: '',\n  // },\n}\n"
  },
  {
    "path": "build-config/webpack-build-config.js",
    "content": "module.exports = {\n  minimize: true,\n}\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@main/*\": [\"src/main/*\"],\n      \"@renderer/*\": [\"src/renderer/*\"],\n      \"@lyric/*\": [\"src/renderer-lyric/*\"],\n      \"@static/*\": [\"src/static/*\"],\n      \"@common/*\": [\"src/common/*\"],\n    }\n  },\n  \"vueCompilerOptions\": {\n    \"plugins\": [\n      \"@vue/language-plugin-pug\"\n    ]\n  },\n  \"exclude\": [\"node_modules\", \"build\", \"dist\"]\n}\n"
  },
  {
    "path": "licenses/license.rtf",
    "content": "{\\rtf1\\adeflang1025\\ansi\\ansicpg936\\uc2\\adeff0\\deff0\\stshfdbch31505\\stshfloch31506\\stshfhich31506\\stshfbi0\\deflang1033\\deflangfe2052\\themelang1033\\themelangfe2052\\themelangcs0{\\fonttbl{\\f0\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}{\\f2\\fbidi \\fmodern\\fcharset0\\fprq1{\\*\\panose 02070309020205020404}Courier New;}\n{\\f13\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'cb\\'ce\\'cc\\'e5{\\*\\falt SimSun};}{\\f34\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02040503050406030204}Cambria Math;}\n{\\f36\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'b5\\'c8\\'cf\\'df{\\*\\falt DengXian};}{\\f44\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 00000000000000000000}@\\'cb\\'ce\\'cc\\'e5;}\n{\\f45\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 00000000000000000000}@\\'b5\\'c8\\'cf\\'df;}{\\flomajor\\f31500\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}\n{\\fdbmajor\\f31501\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'b5\\'c8\\'cf\\'df Light;}{\\fhimajor\\f31502\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'b5\\'c8\\'cf\\'df Light;}\n{\\fbimajor\\f31503\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}{\\flominor\\f31504\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}\n{\\fdbminor\\f31505\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'b5\\'c8\\'cf\\'df{\\*\\falt DengXian};}{\\fhiminor\\f31506\\fbidi \\fnil\\fcharset134\\fprq2{\\*\\panose 02010600030101010101}\\'b5\\'c8\\'cf\\'df{\\*\\falt DengXian};}\n{\\fbiminor\\f31507\\fbidi \\froman\\fcharset0\\fprq2{\\*\\panose 02020603050405020304}Times New Roman;}{\\f46\\fbidi \\froman\\fcharset238\\fprq2 Times New Roman CE;}{\\f47\\fbidi \\froman\\fcharset204\\fprq2 Times New Roman Cyr;}\n{\\f49\\fbidi \\froman\\fcharset161\\fprq2 Times New Roman Greek;}{\\f50\\fbidi \\froman\\fcharset162\\fprq2 Times New Roman Tur;}{\\f51\\fbidi \\froman\\fcharset177\\fprq2 Times New Roman (Hebrew);}{\\f52\\fbidi \\froman\\fcharset178\\fprq2 Times New Roman (Arabic);}\n{\\f53\\fbidi \\froman\\fcharset186\\fprq2 Times New Roman Baltic;}{\\f54\\fbidi \\froman\\fcharset163\\fprq2 Times New Roman (Vietnamese);}{\\f66\\fbidi \\fmodern\\fcharset238\\fprq1 Courier New CE;}{\\f67\\fbidi \\fmodern\\fcharset204\\fprq1 Courier New Cyr;}\n{\\f69\\fbidi \\fmodern\\fcharset161\\fprq1 Courier New Greek;}{\\f70\\fbidi \\fmodern\\fcharset162\\fprq1 Courier New Tur;}{\\f71\\fbidi \\fmodern\\fcharset177\\fprq1 Courier New (Hebrew);}{\\f72\\fbidi \\fmodern\\fcharset178\\fprq1 Courier New (Arabic);}\n{\\f73\\fbidi \\fmodern\\fcharset186\\fprq1 Courier New Baltic;}{\\f74\\fbidi \\fmodern\\fcharset163\\fprq1 Courier New (Vietnamese);}{\\f178\\fbidi \\fnil\\fcharset0\\fprq2 SimSun Western{\\*\\falt SimSun};}{\\f386\\fbidi \\froman\\fcharset238\\fprq2 Cambria Math CE;}\n{\\f387\\fbidi \\froman\\fcharset204\\fprq2 Cambria Math Cyr;}{\\f389\\fbidi \\froman\\fcharset161\\fprq2 Cambria Math Greek;}{\\f390\\fbidi \\froman\\fcharset162\\fprq2 Cambria Math Tur;}{\\f393\\fbidi \\froman\\fcharset186\\fprq2 Cambria Math Baltic;}\n{\\f394\\fbidi \\froman\\fcharset163\\fprq2 Cambria Math (Vietnamese);}{\\f408\\fbidi \\fnil\\fcharset0\\fprq2 DengXian Western{\\*\\falt DengXian};}{\\f406\\fbidi \\fnil\\fcharset238\\fprq2 DengXian CE{\\*\\falt DengXian};}\n{\\f407\\fbidi \\fnil\\fcharset204\\fprq2 DengXian Cyr{\\*\\falt DengXian};}{\\f409\\fbidi \\fnil\\fcharset161\\fprq2 DengXian Greek{\\*\\falt DengXian};}{\\f488\\fbidi \\fnil\\fcharset0\\fprq2 @SimSun Western;}{\\f498\\fbidi \\fnil\\fcharset0\\fprq2 @DengXian Western;}\n{\\f496\\fbidi \\fnil\\fcharset238\\fprq2 @DengXian CE;}{\\f497\\fbidi \\fnil\\fcharset204\\fprq2 @DengXian Cyr;}{\\f499\\fbidi \\fnil\\fcharset161\\fprq2 @DengXian Greek;}{\\flomajor\\f31508\\fbidi \\froman\\fcharset238\\fprq2 Times New Roman CE;}\n{\\flomajor\\f31509\\fbidi \\froman\\fcharset204\\fprq2 Times New Roman Cyr;}{\\flomajor\\f31511\\fbidi \\froman\\fcharset161\\fprq2 Times New Roman Greek;}{\\flomajor\\f31512\\fbidi \\froman\\fcharset162\\fprq2 Times New Roman Tur;}\n{\\flomajor\\f31513\\fbidi \\froman\\fcharset177\\fprq2 Times New Roman (Hebrew);}{\\flomajor\\f31514\\fbidi \\froman\\fcharset178\\fprq2 Times New Roman (Arabic);}{\\flomajor\\f31515\\fbidi \\froman\\fcharset186\\fprq2 Times New Roman Baltic;}\n{\\flomajor\\f31516\\fbidi \\froman\\fcharset163\\fprq2 Times New Roman (Vietnamese);}{\\fdbmajor\\f31520\\fbidi \\fnil\\fcharset0\\fprq2 DengXian Light Western;}{\\fdbmajor\\f31518\\fbidi \\fnil\\fcharset238\\fprq2 DengXian Light CE;}\n{\\fdbmajor\\f31519\\fbidi \\fnil\\fcharset204\\fprq2 DengXian Light Cyr;}{\\fdbmajor\\f31521\\fbidi \\fnil\\fcharset161\\fprq2 DengXian Light Greek;}{\\fhimajor\\f31530\\fbidi \\fnil\\fcharset0\\fprq2 DengXian Light Western;}\n{\\fhimajor\\f31528\\fbidi \\fnil\\fcharset238\\fprq2 DengXian Light CE;}{\\fhimajor\\f31529\\fbidi \\fnil\\fcharset204\\fprq2 DengXian Light Cyr;}{\\fhimajor\\f31531\\fbidi \\fnil\\fcharset161\\fprq2 DengXian Light Greek;}\n{\\fbimajor\\f31538\\fbidi \\froman\\fcharset238\\fprq2 Times New Roman CE;}{\\fbimajor\\f31539\\fbidi \\froman\\fcharset204\\fprq2 Times New Roman Cyr;}{\\fbimajor\\f31541\\fbidi \\froman\\fcharset161\\fprq2 Times New Roman Greek;}\n{\\fbimajor\\f31542\\fbidi \\froman\\fcharset162\\fprq2 Times New Roman Tur;}{\\fbimajor\\f31543\\fbidi \\froman\\fcharset177\\fprq2 Times New Roman (Hebrew);}{\\fbimajor\\f31544\\fbidi \\froman\\fcharset178\\fprq2 Times New Roman (Arabic);}\n{\\fbimajor\\f31545\\fbidi \\froman\\fcharset186\\fprq2 Times New Roman Baltic;}{\\fbimajor\\f31546\\fbidi \\froman\\fcharset163\\fprq2 Times New Roman (Vietnamese);}{\\flominor\\f31548\\fbidi \\froman\\fcharset238\\fprq2 Times New Roman CE;}\n{\\flominor\\f31549\\fbidi \\froman\\fcharset204\\fprq2 Times New Roman Cyr;}{\\flominor\\f31551\\fbidi \\froman\\fcharset161\\fprq2 Times New Roman Greek;}{\\flominor\\f31552\\fbidi \\froman\\fcharset162\\fprq2 Times New Roman Tur;}\n{\\flominor\\f31553\\fbidi \\froman\\fcharset177\\fprq2 Times New Roman (Hebrew);}{\\flominor\\f31554\\fbidi \\froman\\fcharset178\\fprq2 Times New Roman (Arabic);}{\\flominor\\f31555\\fbidi \\froman\\fcharset186\\fprq2 Times New Roman Baltic;}\n{\\flominor\\f31556\\fbidi \\froman\\fcharset163\\fprq2 Times New Roman (Vietnamese);}{\\fdbminor\\f31560\\fbidi \\fnil\\fcharset0\\fprq2 DengXian Western{\\*\\falt DengXian};}{\\fdbminor\\f31558\\fbidi \\fnil\\fcharset238\\fprq2 DengXian CE{\\*\\falt DengXian};}\n{\\fdbminor\\f31559\\fbidi \\fnil\\fcharset204\\fprq2 DengXian Cyr{\\*\\falt DengXian};}{\\fdbminor\\f31561\\fbidi \\fnil\\fcharset161\\fprq2 DengXian Greek{\\*\\falt DengXian};}{\\fhiminor\\f31570\\fbidi \\fnil\\fcharset0\\fprq2 DengXian Western{\\*\\falt DengXian};}\n{\\fhiminor\\f31568\\fbidi \\fnil\\fcharset238\\fprq2 DengXian CE{\\*\\falt DengXian};}{\\fhiminor\\f31569\\fbidi \\fnil\\fcharset204\\fprq2 DengXian Cyr{\\*\\falt DengXian};}{\\fhiminor\\f31571\\fbidi \\fnil\\fcharset161\\fprq2 DengXian Greek{\\*\\falt DengXian};}\n{\\fbiminor\\f31578\\fbidi \\froman\\fcharset238\\fprq2 Times New Roman CE;}{\\fbiminor\\f31579\\fbidi \\froman\\fcharset204\\fprq2 Times New Roman Cyr;}{\\fbiminor\\f31581\\fbidi \\froman\\fcharset161\\fprq2 Times New Roman Greek;}\n{\\fbiminor\\f31582\\fbidi \\froman\\fcharset162\\fprq2 Times New Roman Tur;}{\\fbiminor\\f31583\\fbidi \\froman\\fcharset177\\fprq2 Times New Roman (Hebrew);}{\\fbiminor\\f31584\\fbidi \\froman\\fcharset178\\fprq2 Times New Roman (Arabic);}\n{\\fbiminor\\f31585\\fbidi \\froman\\fcharset186\\fprq2 Times New Roman Baltic;}{\\fbiminor\\f31586\\fbidi \\froman\\fcharset163\\fprq2 Times New Roman (Vietnamese);}}{\\colortbl;\\red0\\green0\\blue0;\\red0\\green0\\blue255;\\red0\\green255\\blue255;\\red0\\green255\\blue0;\n\\red255\\green0\\blue255;\\red255\\green0\\blue0;\\red255\\green255\\blue0;\\red255\\green255\\blue255;\\red0\\green0\\blue128;\\red0\\green128\\blue128;\\red0\\green128\\blue0;\\red128\\green0\\blue128;\\red128\\green0\\blue0;\\red128\\green128\\blue0;\\red128\\green128\\blue128;\n\\red192\\green192\\blue192;\\red0\\green0\\blue0;\\red0\\green0\\blue0;\\chyperlink\\ctint255\\cshade255\\red5\\green99\\blue193;\\red96\\green94\\blue92;\\red225\\green223\\blue221;\\cfollowedhyperlink\\ctint255\\cshade255\\red149\\green79\\blue114;}{\\*\\defchp \n\\fs21\\kerning2\\loch\\af31506\\hich\\af31506\\dbch\\af31505 }{\\*\\defpap \\ql \\li0\\ri0\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0 }\\noqfpromote {\\stylesheet{\n\\qj \\li0\\ri0\\nowidctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \\fs21\\lang1033\\langfe2052\\kerning2\\loch\\f31506\\hich\\af31506\\dbch\\af31505\\cgrid\\langnp1033\\langfenp2052 \n\\snext0 \\sqformat \\spriority0 Normal;}{\\*\\cs10 \\additive \\ssemihidden \\sunhideused \\spriority1 Default Paragraph Font;}{\\*\n\\ts11\\tsrowd\\trftsWidthB3\\trpaddl108\\trpaddr108\\trpaddfl3\\trpaddft3\\trpaddfb3\\trpaddfr3\\trcbpat1\\trcfpat1\\tblind0\\tblindtype3\\tsvertalt\\tsbrdrt\\tsbrdrl\\tsbrdrb\\tsbrdrr\\tsbrdrdgl\\tsbrdrdgr\\tsbrdrh\\tsbrdrv \n\\ql \\li0\\ri0\\widctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0 \\rtlch\\fcs1 \\af0\\afs21\\alang1025 \\ltrch\\fcs0 \\fs21\\lang1033\\langfe2052\\kerning2\\loch\\f31506\\hich\\af31506\\dbch\\af31505\\cgrid\\langnp1033\\langfenp2052 \n\\snext11 \\ssemihidden \\sunhideused Normal Table;}{\\s15\\qj \\li0\\ri0\\nowidctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0 \\rtlch\\fcs1 \\af2\\afs22\\alang1025 \\ltrch\\fcs0 \n\\fs21\\lang1033\\langfe2052\\kerning2\\loch\\f31505\\hich\\af2\\dbch\\af31505\\cgrid\\langnp1033\\langfenp2052 \\sbasedon0 \\snext15 \\slink16 \\sunhideused Plain Text;}{\\*\\cs16 \\additive \\rtlch\\fcs1 \\af2 \\ltrch\\fcs0 \\loch\\f31505\\hich\\af2 \\sbasedon10 \\slink15 \\slocked \n\\'b4\\'bf\\'ce\\'c4\\'b1\\'be \\'d7\\'d6\\'b7\\'fb;}{\\*\\cs17 \\additive \\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\ul\\cf19 \\sbasedon10 \\sunhideused \\styrsid9533173 Hyperlink;}{\\*\\cs18 \\additive \\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\cf20\\chshdng0\\chcfpat0\\chcbpat21 \n\\sbasedon10 \\ssemihidden \\sunhideused \\styrsid9533173 Unresolved Mention;}{\\*\\cs19 \\additive \\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\ul\\cf22 \\sbasedon10 \\ssemihidden \\sunhideused \\styrsid9533173 FollowedHyperlink;}}{\\*\\pgptbl {\\pgp\\ipgp0\\itap0\\li0\\ri0\\sb0\\sa0}}\n{\\*\\rsidtbl \\rsid927107\\rsid1398824\\rsid2109456\\rsid2766548\\rsid3758332\\rsid3950508\\rsid4133944\\rsid4355753\\rsid9533173\\rsid10447395\\rsid11081282\\rsid12910709\\rsid13643782\\rsid14384001\\rsid14511311\\rsid15225067\\rsid15226681}{\\mmathPr\\mmathFont34\\mbrkBin0\n\\mbrkBinSub0\\msmallFrac0\\mdispDef1\\mlMargin0\\mrMargin0\\mdefJc1\\mwrapIndent1440\\mintLim0\\mnaryLim1}{\\info{\\author lysyw}{\\operator lysyw}{\\creatim\\yr2019\\mo8\\dy17\\hr10\\min22}{\\revtim\\yr2025\\mo2\\dy22\\hr15\\min34}{\\version12}{\\edmins5}{\\nofpages2}\n{\\nofwords195}{\\nofchars1117}{\\nofcharsws1310}{\\vern77}}{\\*\\xmlnstbl {\\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\\paperw11906\\paperh16838\\margl2253\\margr2253\\margt1440\\margb1440\\gutter0\\ltrsect \n\\deftab420\\ftnbj\\aenddoc\\trackmoves0\\trackformatting1\\donotembedsysfont1\\relyonvml0\\donotembedlingdata0\\grfdocevents0\\validatexml1\\showplaceholdtext0\\ignoremixedcontent0\\saveinvalidxml0\\showxmlerrors1\\formshade\\horzdoc\\dgmargin\\dghspace180\\dgvspace156\n\\dghorigin2253\\dgvorigin1440\\dghshow0\\dgvshow2\\jcompress\\lnongrid\n\\viewkind1\\viewscale100\\splytwnine\\ftnlytwnine\\htmautsp\\useltbaln\\alntblind\\lytcalctblwd\\lyttblrtgr\\lnbrkrule\\nobrkwrptbl\\snaptogridincell\\allowfieldendsel\\wrppunct\\asianbrkrule\\rsidroot3950508\\newtblstyruls\n\\nogrowautofit\\usenormstyforlist\\noindnmbrts\\felnbrelev\\nocxsptable\\indrlsweleven\\noafcnsttbl\\afelev\\utinl\\hwelev\\spltpgpar\\notcvasp\\notbrkcnstfrctbl\\notvatxbx\\krnprsnet\\cachedcolbal \\nouicompat {\\upr{\\*\\fchars \n!%),.:\\'3b>?]\\'7d\\'a1\\'e9\\'a1\\'a7\\'a1\\'e3\\'a1\\'a4\\'a1\\'a6\\'a1\\'a5\\'a8\\'44\\'a1\\'ac\\'a1\\'af\\'a1\\'b1\\'a1\\'ad\\'a1\\'eb\\'a1\\'e4\\'a1\\'e5?\\'a1\\'e6\\'a1\\'c3\\'a1\\'a2\\'a1\\'a3\\'a1\\'a8\\'a1\\'b5\\'a1\\'b7\\'a1\\'b9\\'a1\\'bb\\'a1\\'bf\\'a1\\'b3\\'a1\\'bd\\'a8\\'95\\'a6\\'e1\\'a6\\'e3\\'a6\\'e7\\'a6\\'e5\\'a6\\'eb\\'a9\\'77\\'a9\\'79\\'a9\\'7b\\'a3\\'a1\\'a3\\'a2\\'a3\\'a5\\'a3\\'a7\\'a3\\'a9\\'a3\\'ac\\'a3\\'ae\\'a3\\'ba\\'a3\\'bb\\'a3\\'bf\\'a3\\'dd\\'a3\\'e0\\'a3\\'fc\\'a3\\'fd\\'a1\\'ab\\'a1\\'e9\n}{\\*\\ud\\uc0{\\*\\fchars \n!%),.:\\'3b>?]\\'7d{\\uc2\\u162 \\'a1\\'e9\\'a1\\'a7\\'a1\\'e3\\'a1\\'a4\\'a1\\'a6\\'a1\\'a5\\'a8D\\'a1\\'ac\\'a1\\'af\\'a1\\'b1\\'a1\\'ad\\'a1\\'eb\\'a1\\'e4\\'a1\\'e5}{\\uc1\\u8250 ?\\'a1\\'e6\\'a1\\'c3\\'a1\\'a2\\'a1\\'a3\\'a1\\'a8\\'a1\\'b5\\'a1\\'b7\\'a1\\'b9\\'a1\\'bb\\'a1\\'bf\\'a1\\'b3\\'a1\\'bd\\'a8\\'95\\'a6\\'e1\\'a6\\'e3\\'a6\\'e7\\'a6\\'e5\\'a6\\'eb\\'a9w\\'a9y\\'a9\\'7b\\'a3\\'a1\\'a3\\'a2\\'a3\\'a5\\'a3\\'a7\\'a3\\'a9\\'a3\\'ac\\'a3\\'ae\\'a3\\'ba\\'a3\\'bb\\'a3\\'bf\\'a3\\'dd\\'a3\\'e0\\'a3\\'fc\\'a3\\'fd\\'a1\\'ab\\'a1\\'e9}\n}}}{\\upr{\\*\\lchars $([\\'7b\\'a1\\'ea\\'a3\\'a4\\'a1\\'a4\\'a1\\'ae\\'a1\\'b0\\'a1\\'b4\\'a1\\'b6\\'a1\\'b8\\'a1\\'ba\\'a1\\'be\\'a1\\'b2\\'a1\\'bc\\'a8\\'94\\'a9\\'76\\'a9\\'78\\'a9\\'7a\\'a1\\'e7\\'a3\\'a8\\'a3\\'ae\\'a3\\'db\\'a3\\'fb\\'a1\\'ea\\'a3\\'a4}{\\*\\ud\\uc0{\\*\\lchars \n$([\\'7b{\\uc2\\u163 \\'a1\\'ea\\u165 \\'a3\\'a4\\'a1\\'a4\\'a1\\'ae\\'a1\\'b0\\'a1\\'b4\\'a1\\'b6\\'a1\\'b8\\'a1\\'ba\\'a1\\'be\\'a1\\'b2\\'a1\\'bc\\'a8\\'94\\'a9v\\'a9x\\'a9z\\'a1\\'e7\\'a3\\'a8\\'a3\\'ae\\'a3\\'db\\'a3\\'fb\\'a1\\'ea\\'a3\\'a4}}}}\\fet0{\\*\\wgrffmtfilter 2450}\\nofeaturethrottle1\n\\ilfomacatclnup0\\ltrpar \\sectd \\ltrsect\\linex0\\headery851\\footery992\\colsx425\\endnhere\\sectlinegrid312\\sectspecifyl\\sectrsid2109456\\sftnbj {\\*\\pnseclvl1\\pnucrm\\pnstart1\\pnindent720\\pnhang {\\pntxta \\dbch .}}{\\*\\pnseclvl2\\pnucltr\\pnstart1\\pnindent720\\pnhang\n{\\pntxta \\dbch .}}{\\*\\pnseclvl3\\pndec\\pnstart1\\pnindent720\\pnhang {\\pntxta \\dbch .}}{\\*\\pnseclvl4\\pnlcltr\\pnstart1\\pnindent720\\pnhang {\\pntxta \\dbch )}}{\\*\\pnseclvl5\\pndec\\pnstart1\\pnindent720\\pnhang {\\pntxtb \\dbch (}{\\pntxta \\dbch )}}{\\*\\pnseclvl6\n\\pnlcltr\\pnstart1\\pnindent720\\pnhang {\\pntxtb \\dbch (}{\\pntxta \\dbch )}}{\\*\\pnseclvl7\\pnlcrm\\pnstart1\\pnindent720\\pnhang {\\pntxtb \\dbch (}{\\pntxta \\dbch )}}{\\*\\pnseclvl8\\pnlcltr\\pnstart1\\pnindent720\\pnhang {\\pntxtb \\dbch (}{\\pntxta \\dbch )}}{\\*\\pnseclvl9\n\\pnlcrm\\pnstart1\\pnindent720\\pnhang {\\pntxtb \\dbch (}{\\pntxta \\dbch )}}\\pard\\plain \\ltrpar\\qj \\li0\\ri0\\nowidctlpar\\wrapdefault\\aspalpha\\aspnum\\faauto\\adjustright\\rin0\\lin0\\itap0\\pararsid2766548 \\rtlch\\fcs1 \\af0\\afs22\\alang1025 \\ltrch\\fcs0 \n\\fs21\\lang1033\\langfe2052\\kerning2\\loch\\af31506\\hich\\af31506\\dbch\\af31505\\cgrid\\langnp1033\\langfenp2052 {\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\n\\'bb\\'f9\\'d3\\'da}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13  Apache License 2.0 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'d0\\'ed\\'bf\\'c9\\'d6\\'a4\\'b7\\'a2\\'d0\\'d0\\'a3\\'ac\\'d2\\'d4\\'cf\\'c2\\'d0\\'ad\\'d2\\'e9\\'ca\\'c7\\'b6\\'d4\\'d3\\'da}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\hich\\af13\\dbch\\af13\\loch\\f13  Apache License 2.0 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b5\\'c4\\'b2\\'b9\\'b3\\'e4\\'a3\\'ac\\'c8\\'e7\\'d3\\'d0\\'b3\\'e5\\'cd\\'bb\\'a3\\'ac\\'d2\\'d4\n\\'d2\\'d4\\'cf\\'c2\\'d0\\'ad\\'d2\\'e9\\'ce\\'aa\\'d7\\'bc\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b4\\'ca\\'d3\\'ef\\'d4\\'bc\\'b6\\'a8\\'a3\\'ba\\'b1\\'be\\'d0\\'ad\\'d2\\'e9\\'d6\\'d0\\'b5\\'c4\\'a1\\'b0\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'a1\\'b1\\'d6\\'b8}{\n\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13  LX Music }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'d7\\'c0\\'c3\\'e6\\'b0\\'e6\\'cf\\'ee\\'c4\\'bf\\'a3\\'bb}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'ca\\'b9\\'d3\\'c3\\'d5\\'df}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \n\\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d6\\'b8\\'c7\\'a9\\'ca\\'f0\\'b1\\'be\\'d0\\'ad\\'d2\\'e9\\'b5\\'c4\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'a3\\'bb}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b9\\'d9\\'b7\\'bd\\'d2\\'f4\\'c0\\'d6\\'c6\\'bd\\'cc\\'a8}{\n\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d6\\'b8\\'b6\\'d4\\'b1\\'be\n\\'cf\\'ee\\'c4\\'bf\\'c4\\'da\\'d6\\'c3\\'b5\\'c4\\'b0\\'fc\\'c0\\'a8\\'bf\\'e1\\'ce\\'d2\\'a1\\'a2\\'bf\\'e1\\'b9\\'b7\\'a1\\'a2\\'df\\'e4\\'b9\\'be\\'b5\\'c8\\'d2\\'f4\\'c0\\'d6\\'d4\\'b4\\'b5\\'c4\\'b9\\'d9\\'b7\\'bd\\'c6\\'bd\\'cc\\'a8\\'cd\\'b3\\'b3\\'c6\\'a3\\'bb}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b0\\'e6\\'c8\\'a8\\'ca\\'fd\\'be\\'dd}{\\rtlch\\fcs1 \\af13 \n\\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d6\\'b8\\'b0\\'fc\\'c0\\'a8\\'b5\\'ab\\'b2\\'bb\n\\'cf\\'de\\'d3\\'da\\'cd\\'bc\\'cf\\'f1\\'a1\\'a2\\'d2\\'f4\\'c6\\'b5\\'a1\\'a2\\'c3\\'fb\\'d7\\'d6\\'b5\\'c8\\'d4\\'da\\'c4\\'da\\'b5\\'c4\\'cb\\'fb\\'c8\\'cb\\'d3\\'b5\\'d3\\'d0\\'cb\\'f9\\'ca\\'f4\\'b0\\'e6\\'c8\\'a8\\'b5\\'c4\\'ca\\'fd\\'be\\'dd\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d2\\'bb\\'a1\\'a2\\'ca\\'fd\\'be\\'dd\\'c0\\'b4\\'d4\\'b4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 1.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b5\\'c4\\'b8\\'f7\\'b9\\'d9\\'b7\\'bd\\'c6\\'bd\\'cc\\'a8\\'d4\\'da\\'cf\\'df\\'ca\\'fd\n\\'be\\'dd\\'c0\\'b4\\'d4\\'b4\\'d4\\'ad\\'c0\\'ed\\'ca\\'c7\\'b4\\'d3\\'c6\\'e4\\'b9\\'ab\\'bf\\'aa\\'b7\\'fe\\'ce\\'f1\\'c6\\'f7\\'d6\\'d0\\'c0\\'ad\\'c8\\'a1\\'ca\\'fd\\'be\\'dd\\'a3\\'a8\\'d3\\'eb\\'ce\\'b4\\'b5\\'c7\\'c2\\'bc\\'d7\\'b4\\'cc\\'ac\\'d4\\'da\\'b9\\'d9\\'b7\\'bd\\'c6\\'bd\\'cc\\'a8}{\\rtlch\\fcs1 \n\\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13  APP }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'bb\\'f1\\'c8\\'a1\n\\'b5\\'c4\\'ca\\'fd\\'be\\'dd\\'cf\\'e0\\'cd\\'ac\\'a3\\'a9\\'a3\\'ac\\'be\\'ad\\'b9\\'fd\\'b6\\'d4\\'ca\\'fd\\'be\\'dd\\'bc\\'f2\\'b5\\'a5\\'b5\\'d8\\'c9\\'b8\\'d1\\'a1\\'d3\\'eb\\'ba\\'cf\\'b2\\'a2\\'ba\\'f3\\'bd\\'f8\\'d0\\'d0\\'d5\\'b9\\'ca\\'be\\'a3\\'ac\\'d2\\'f2\\'b4\\'cb\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\n\\'b2\\'bb\\'b6\\'d4\\'ca\\'fd\\'be\\'dd\\'b5\\'c4\\'ba\\'cf\\'b7\\'a8\\'d0\\'d4\\'a1\\'a2\\'d7\\'bc\\'c8\\'b7\\'d0\\'d4\\'b8\\'ba\\'d4\\'f0\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 1.2 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b1\\'be\\'c9\\'ed\\'c3\\'bb\\'d3\\'d0\\'bb\\'f1\\'c8\\'a1\\'c4\\'b3\\'b8\\'f6\\'d2\\'f4\n\\'c6\\'b5\\'ca\\'fd\\'be\\'dd\\'b5\\'c4\\'c4\\'dc\\'c1\\'a6\\'a3\\'ac\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'ca\\'b9\\'d3\\'c3\\'b5\\'c4\\'d4\\'da\\'cf\\'df\\'d2\\'f4\\'c6\\'b5\\'ca\\'fd\\'be\\'dd\\'c0\\'b4\\'d4\\'b4\\'c0\\'b4\\'d7\\'d4\\'c8\\'ed\\'bc\\'fe\\'c9\\'e8\\'d6\\'c3\\'c4\\'da}{\\rtlch\\fcs1 \\af13 \n\\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d7\\'d4\\'b6\\'a8\\'d2\\'e5\\'d4\\'b4}{\n\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c9\\'e8\\'d6\\'c3\\'cb\\'f9\n\\'d1\\'a1\\'d4\\'f1\\'b5\\'c4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'d4\\'b4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'b7\\'b5\\'bb\\'d8\\'b5\\'c4\\'d4\\'da\\'cf\\'df\\'c1\\'b4\\'bd\\'d3\\'a1\\'a3\\'c0\\'fd\\'c8\\'e7\\'b2\\'a5\\'b7\\'c5\\'c4\\'b3\\'ca\\'d7\\'b8\\'e8\\'a3\\'ac\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'cb\\'f9\\'d7\\'f6\\'b5\\'c4\\'d6\\'bb\\'ca\\'c7\\'bd\\'ab\\'cf\\'a3\\'cd\\'fb\\'b2\\'a5\n\\'b7\\'c5\\'b5\\'c4\\'b8\\'e8\\'c7\\'fa\\'c3\\'fb\\'a1\\'a2\\'d2\\'d5\\'ca\\'f5\\'bc\\'d2\\'b5\\'c8\\'d0\\'c5\\'cf\\'a2\\'b4\\'ab\\'b5\\'dd\\'b8\\'f8}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \n\\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d4\\'b4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'a3\\'ac\\'c8\\'f4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d4\\'b4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b7\\'b5\\'bb\\'d8\\'c1\\'cb\\'d2\\'bb\\'b8\\'f6\\'c1\\'b4\\'bd\\'d3\\'a3\\'ac\\'d4\\'f2\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'bd\\'ab\\'c8\\'cf\\'ce\\'aa\\'d5\\'e2\\'be\\'cd\\'ca\\'c7\\'b8\\'c3\\'b8\\'e8\n\\'c7\\'fa\\'b5\\'c4\\'d2\\'f4\\'c6\\'b5\\'ca\\'fd\\'be\\'dd\\'b6\\'f8\\'bd\\'f8\\'d0\\'d0\\'ca\\'b9\\'d3\\'c3\\'a3\\'ac\\'d6\\'c1\\'d3\\'da\\'d5\\'e2\\'ca\\'c7\\'b2\\'bb\\'ca\\'c7\\'d5\\'fd\\'c8\\'b7\\'b5\\'c4\\'d2\\'f4\\'c6\\'b5\\'ca\\'fd\\'be\\'dd\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'ce\\'de\\'b7\\'a8\\'d0\\'a3\n\\'d1\\'e9\\'c6\\'e4\\'d7\\'bc\\'c8\\'b7\\'d0\\'d4\\'a3\\'ac\\'cb\\'f9\\'d2\\'d4\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b5\\'c4\\'b9\\'fd\\'b3\\'cc\\'d6\\'d0\\'bf\\'c9\\'c4\\'dc\\'bb\\'e1\\'b3\\'f6\\'cf\\'d6\\'cf\\'a3\\'cd\\'fb\\'b2\\'a5\\'b7\\'c5\\'b5\\'c4\\'d2\\'f4\\'c6\\'b5\\'d3\\'eb\\'ca\\'b5\n\\'bc\\'ca\\'b2\\'a5\\'b7\\'c5\\'b5\\'c4\\'d2\\'f4\\'c6\\'b5\\'b2\\'bb\\'b6\\'d4\\'d3\\'a6\\'bb\\'f2\\'d5\\'df\\'ce\\'de\\'b7\\'a8\\'b2\\'a5\\'b7\\'c5\\'b5\\'c4\\'ce\\'ca\\'cc\\'e2\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 1.3 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b5\\'c4\\'b7\\'c7\\'b9\\'d9\\'b7\\'bd\\'c6\\'bd\\'cc\\'a8\\'ca\\'fd\\'be\\'dd\\'a3\\'a8\n\\'c0\\'fd\\'c8\\'e7}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8220\\'a1\\'b0}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \n\\'ce\\'d2\\'b5\\'c4\\'c1\\'d0\\'b1\\'ed}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\u8221\\'a1\\'b1}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'c4\\'da\\'c1\\'d0\\'b1\\'ed\\'a3\\'a9\\'c0\\'b4\\'d7\\'d4\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'b1\\'be\\'b5\\'d8\\'cf\\'b5\\'cd\\'b3\\'bb\\'f2\\'d5\\'df\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'c1\\'ac\\'bd\\'d3\\'b5\\'c4\\'cd\\'ac\\'b2\\'bd\\'b7\\'fe\\'ce\\'f1\\'a3\\'ac\\'b1\\'be\\'cf\\'ee\n\\'c4\\'bf\\'b2\\'bb\\'b6\\'d4\\'d5\\'e2\\'d0\\'a9\\'ca\\'fd\\'be\\'dd\\'b5\\'c4\\'ba\\'cf\\'b7\\'a8\\'d0\\'d4\\'a1\\'a2\\'d7\\'bc\\'c8\\'b7\\'d0\\'d4\\'b8\\'ba\\'d4\\'f0\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b6\\'fe\\'a1\\'a2\\'b0\\'e6\\'c8\\'a8\\'ca\\'fd\\'be\\'dd}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 2\\hich\\af13\\dbch\\af13\\loch\\f13 .1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b5\\'c4\\'b9\\'fd\\'b3\\'cc\n\\'d6\\'d0\\'bf\\'c9\\'c4\\'dc\\'bb\\'e1\\'b2\\'fa\\'c9\\'fa\\'b0\\'e6\\'c8\\'a8\\'ca\\'fd\\'be\\'dd\\'a1\\'a3\\'b6\\'d4\\'d3\\'da\\'d5\\'e2\\'d0\\'a9\\'b0\\'e6\\'c8\\'a8\\'ca\\'fd\\'be\\'dd\\'a3\\'ac\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b2\\'bb\\'d3\\'b5\\'d3\\'d0\\'cb\\'fc\\'c3\\'c7\\'b5\\'c4\\'cb\\'f9\\'d3\\'d0\n\\'c8\\'a8\\'a1\\'a3\\'ce\\'aa\\'c1\\'cb\\'b1\\'dc\\'c3\\'e2\\'c7\\'d6\\'c8\\'a8\\'a3\\'ac\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'ce\\'f1\\'b1\\'d8\\'d4\\'da}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13  **24 }{\n\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d0\\'a1\\'ca\\'b1\\'c4\\'da}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\hich\\af13\\dbch\\af13\\loch\\f13 ** }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c7\\'e5\\'b3\\'fd\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b5\\'c4\\'b9\\'fd\\'b3\\'cc\\'d6\\'d0\\'cb\\'f9\n\\'b2\\'fa\\'c9\\'fa\\'b5\\'c4\\'b0\\'e6\\'c8\\'a8\\'ca\\'fd\\'be\\'dd\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c8\\'fd\\'a1\\'a2\\'d2\\'f4\\'c0\\'d6\\'c6\\'bd\\'cc\\'a8\\'b1\\'f0\\'c3\\'fb}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 3.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'c4\\'da\\'b5\\'c4\\'b9\\'d9\\'b7\\'bd\\'d2\\'f4\\'c0\\'d6\\'c6\\'bd\\'cc\\'a8\\'b1\\'f0\n\\'c3\\'fb\\'ce\\'aa\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'c4\\'da\\'b6\\'d4\\'b9\\'d9\\'b7\\'bd\\'d2\\'f4\\'c0\\'d6\\'c6\\'bd\\'cc\\'a8\\'b5\\'c4\\'d2\\'bb\\'b8\\'f6\\'b3\\'c6\\'ba\\'f4\\'a3\\'ac\\'b2\\'bb\\'b0\\'fc\\'ba\\'ac\\'b6\\'f1\\'d2\\'e2\\'a1\\'a3\\'c8\\'e7\\'b9\\'fb\\'b9\\'d9\\'b7\\'bd\\'d2\\'f4\\'c0\\'d6\n\\'c6\\'bd\\'cc\\'a8\\'be\\'f5\\'b5\\'c3\\'b2\\'bb\\'cd\\'d7\\'a3\\'ac\\'bf\\'c9\\'c1\\'aa\\'cf\\'b5\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b8\\'fc\\'b8\\'c4\\'bb\\'f2\\'d2\\'c6\\'b3\\'fd\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'cb\\'c4\\'a1\\'a2\\'d7\\'ca\\'d4\\'b4\\'ca\\'b9\\'d3\\'c3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 4.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'c4\\'da\\'ca\\'b9\\'d3\\'c3\\'b5\\'c4\\'b2\\'bf\\'b7\\'d6\\'b0\\'fc\\'c0\\'a8\\'b5\\'ab\n\\'b2\\'bb\\'cf\\'de\\'d3\\'da\\'d7\\'d6\\'cc\\'e5\\'a1\\'a2\\'cd\\'bc\\'c6\\'ac\\'b5\\'c8\\'d7\\'ca\\'d4\\'b4\\'c0\\'b4\\'d4\\'b4\\'d3\\'da\\'bb\\'a5\\'c1\\'aa\\'cd\\'f8\\'a1\\'a3\\'c8\\'e7\\'b9\\'fb\\'b3\\'f6\\'cf\\'d6\\'c7\\'d6\\'c8\\'a8\\'bf\\'c9\\'c1\\'aa\\'cf\\'b5\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'d2\\'c6\n\\'b3\\'fd\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'ce\\'e5\\'a1\\'a2\\'c3\\'e2\\'d4\\'f0\\'c9\\'f9\\'c3\\'f7}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 5.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d3\\'c9\\'d3\\'da\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b2\\'fa\\'c9\\'fa\\'b5\\'c4\\'b0\\'fc\\'c0\\'a8\n\\'d3\\'c9\\'d3\\'da\\'b1\\'be\\'d0\\'ad\\'d2\\'e9\\'bb\\'f2\\'d3\\'c9\\'d3\\'da\\'ca\\'b9\\'d3\\'c3\\'bb\\'f2\\'ce\\'de\\'b7\\'a8\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b6\\'f8\\'d2\\'fd\\'c6\\'f0\\'b5\\'c4\\'c8\\'ce\\'ba\\'ce\\'d0\\'d4\\'d6\\'ca\\'b5\\'c4\\'c8\\'ce\\'ba\\'ce\\'d6\\'b1\\'bd\\'d3\n\\'a1\\'a2\\'bc\\'e4\\'bd\\'d3\\'a1\\'a2\\'cc\\'d8\\'ca\\'e2\\'a1\\'a2\\'c5\\'bc\\'c8\\'bb\\'bb\\'f2\\'bd\\'e1\\'b9\\'fb\\'d0\\'d4\\'cb\\'f0\\'ba\\'a6\\'a3\\'a8\\'b0\\'fc\\'c0\\'a8\\'b5\\'ab\\'b2\\'bb\\'cf\\'de\\'d3\\'da\\'d2\\'f2\\'c9\\'cc\\'d3\\'fe\\'cb\\'f0\\'ca\\'a7\\'a1\\'a2\\'cd\\'a3\\'b9\\'a4\\'a1\\'a2\n\\'bc\\'c6\\'cb\\'e3\\'bb\\'fa\\'b9\\'ca\\'d5\\'cf\\'bb\\'f2\\'b9\\'ca\\'d5\\'cf\\'d2\\'fd\\'c6\\'f0\\'b5\\'c4\\'cb\\'f0\\'ba\\'a6\\'c5\\'e2\\'b3\\'a5\\'a3\\'ac\\'bb\\'f2\\'c8\\'ce\\'ba\\'ce\\'bc\\'b0\\'cb\\'f9\\'d3\\'d0\\'c6\\'e4\\'cb\\'fb\\'c9\\'cc\\'d2\\'b5\\'cb\\'f0\\'ba\\'a6\\'bb\\'f2\\'cb\\'f0\\'ca\\'a7\n\\'a3\\'a9\\'d3\\'c9\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'b8\\'ba\\'d4\\'f0\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c1\\'f9\\'a1\\'a2\\'ca\\'b9\\'d3\\'c3\\'cf\\'de\\'d6\\'c6}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 6.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'cd\\'ea\\'c8\\'ab\\'c3\\'e2\\'b7\\'d1\\'a3\\'ac\\'c7\\'d2\\'bf\\'aa\\'d4\\'b4\\'b7\\'a2\n\\'b2\\'bc\\'d3\\'da}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13  GitHub }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\loch\\af13\\hich\\af13\\dbch\\f13 \\'c3\\'e6\\'cf\\'f2\\'c8\\'ab\\'ca\\'c0\\'bd\\'e7\\'c8\\'cb\\'d3\\'c3\\'d7\\'f7\\'b6\\'d4\\'bc\\'bc\\'ca\\'f5\\'b5\\'c4\\'d1\\'a7\\'cf\\'b0\\'bd\\'bb\\'c1\\'f7\\'a1\\'a3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'b2\\'bb\\'b6\\'d4\\'cf\\'ee\\'c4\\'bf\\'c4\\'da\\'b5\\'c4\\'bc\\'bc\\'ca\\'f5\n\\'bf\\'c9\\'c4\\'dc\\'b4\\'e6\\'d4\\'da\\'ce\\'a5\\'b7\\'b4\\'b5\\'b1\\'b5\\'d8\\'b7\\'a8\\'c2\\'c9\\'b7\\'a8\\'b9\\'e6\\'b5\\'c4\\'d0\\'d0\\'ce\\'aa\\'d7\\'f7\\'b1\\'a3\\'d6\\'a4\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 6.2 **}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'bd\\'fb\\'d6\\'b9\\'d4\\'da\\'ce\\'a5\\'b7\\'b4\\'b5\\'b1\\'b5\\'d8\\'b7\\'a8\\'c2\\'c9\\'b7\\'a8\\'b9\\'e6\n\\'b5\\'c4\\'c7\\'e9\\'bf\\'f6\\'cf\\'c2\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\hich\\af13\\dbch\\af13\\loch\\f13 ** }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b6\\'d4\\'d3\\'da\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'d4\\'da\\'c3\\'f7\\'d6\\'aa\\'bb\\'f2\\'b2\\'bb\\'d6\\'aa\\'b5\\'b1\\'b5\\'d8\\'b7\\'a8\\'c2\\'c9\\'b7\\'a8\\'b9\\'e6\\'b2\\'bb\\'d4\\'ca\\'d0\\'ed\n\\'b5\\'c4\\'c7\\'e9\\'bf\\'f6\\'cf\\'c2\\'ca\\'b9\\'d3\\'c3\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'cb\\'f9\\'d4\\'ec\\'b3\\'c9\\'b5\\'c4\\'c8\\'ce\\'ba\\'ce\\'ce\\'a5\\'b7\\'a8\\'ce\\'a5\\'b9\\'e6\\'d0\\'d0\\'ce\\'aa\\'d3\\'c9\\'ca\\'b9\\'d3\\'c3\\'d5\\'df\\'b3\\'d0\\'b5\\'a3\\'a3\\'ac\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\n\\'b2\\'bb\\'b3\\'d0\\'b5\\'a3\\'d3\\'c9\\'b4\\'cb\\'d4\\'ec\\'b3\\'c9\\'b5\\'c4\\'c8\\'ce\\'ba\\'ce\\'d6\\'b1\\'bd\\'d3\\'a1\\'a2\\'bc\\'e4\\'bd\\'d3\\'a1\\'a2\\'cc\\'d8\\'ca\\'e2\\'a1\\'a2\\'c5\\'bc\\'c8\\'bb\\'bb\\'f2\\'bd\\'e1\\'b9\\'fb\\'d0\\'d4\\'d4\\'f0\\'c8\\'ce\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \n\\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c6\\'df\\'a1\\'a2\\'b0\\'e6\\'c8\\'a8\\'b1\\'a3\\'bb\\'a4}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 7.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'d2\\'f4\\'c0\\'d6\\'c6\\'bd\\'cc\\'a8\\'b2\\'bb\\'d2\\'d7\\'a3\\'ac\\'c7\\'eb\\'d7\\'f0\\'d6\\'d8\\'b0\\'e6\\'c8\\'a8\n\\'a3\\'ac\\'d6\\'a7\\'b3\\'d6\\'d5\\'fd\\'b0\\'e6\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b0\\'cb\\'a1\\'a2\\'b7\\'c7\\'c9\\'cc\\'d2\\'b5\\'d0\\'d4\\'d6\\'ca}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 8.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'bd\\'f6\\'d3\\'c3\\'d3\\'da\\'b6\\'d4\\'bc\\'bc\\'ca\\'f5\\'bf\\'c9\\'d0\\'d0\\'d0\\'d4\n\\'b5\\'c4\\'cc\\'bd\\'cb\\'f7\\'bc\\'b0\\'d1\\'d0\\'be\\'bf\\'a3\\'ac\\'b2\\'bb\\'bd\\'d3\\'ca\\'dc\\'c8\\'ce\\'ba\\'ce\\'c9\\'cc\\'d2\\'b5\\'a3\\'a8\\'b0\\'fc\\'c0\\'a8\\'b5\\'ab\\'b2\\'bb\\'cf\\'de\\'d3\\'da\\'b9\\'e3\\'b8\\'e6\\'b5\\'c8\\'a3\\'a9\\'ba\\'cf\\'d7\\'f7\\'bc\\'b0\\'be\\'e8\\'d4\\'f9\\'a1\\'a3}{\n\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'be\\'c5\\'a1\\'a2\\'bd\\'d3\\'ca\\'dc\\'d0\\'ad\\'d2\\'e9}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \n\\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 9.1 }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c8\\'f4\\'c4\\'e3\\'ca\\'b9\\'d3\\'c3\\'c1\\'cb\\'b1\\'be\\'cf\\'ee\\'c4\\'bf\\'a3\\'ac\\'bc\\'b4\\'b4\\'fa\\'b1\\'ed\n\\'c4\\'e3\\'bd\\'d3\\'ca\\'dc\\'b1\\'be\\'d0\\'ad\\'d2\\'e9\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 * }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c8\\'f4\\'d0\\'ad\\'d2\\'e9\\'b8\\'fc\\'d0\\'c2\\'a3\\'ac\\'cb\\'a1\\'b2\\'bb\\'c1\\'ed\\'d0\\'d0\\'cd\\'a8\\'d6\\'aa\n\\'a3\\'ac\\'bf\\'c9\\'b5\\'bd\\'bf\\'aa\\'d4\\'b4\\'b5\\'d8\\'d6\\'b7\\'b2\\'e9\\'bf\\'b4\\'a1\\'a3}{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \n\\par \n\\par \\hich\\af13\\dbch\\af13\\loch\\f13 By: }{\\rtlch\\fcs1 \\af13 \\ltrch\\fcs0 \\loch\\af13\\hich\\af13\\dbch\\af13\\insrsid2766548\\charrsid2766548 \\loch\\af13\\hich\\af13\\dbch\\f13 \\'c2\\'e4\\'d1\\'a9\\'ce\\'de\\'ba\\'db}{\\rtlch\\fcs1 \\af0 \\ltrch\\fcs0 \\insrsid12910709\\charrsid2766548 \n\n\\par }{\\*\\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a\n9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad\n5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6\nb01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0\n0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6\na7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f\nc7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512\n0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462\na1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865\n6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b\n4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b\n4757e8d3f729e245eb2b260a0238fd010000ffff0300504b0304140006000800000021006ab3c999d7060000941a0000160000007468656d652f7468656d652f\n7468656d65312e786d6cec595b6b1b47147e2ff43f2cfbaee8b6ab8b891c748ddbd8498894943c8ea59176e2d91db133b2234220244fa55028a4250f0d94bef4\na194061a68681ffa5feae290b63fa2676657ab1969543bc68550621bb33bfb9d33df9c73f63bb3bb97afdc0fa97388634e58d4708b970aae83a3211b9168d270\n6f0f7ab99aeb7081a211a22cc20d778eb97b65fbc30f2ea32d11e0103b601ff12dd4700321a65bf93c1fc230e297d81447706dcce21009388d27f9518c8ec06f\n48f3a542a1920f11895c274221b8bd311e932176fe78f5eb9b6f9ffdfee833f873b7177374294c14092e078634eecb19b061a8b0a383a244f0396fd3d83944b4\ne1c274237634c0f785eb50c4055c68b805f5e3e6b72fe7d1566a44c5065bcdaea77e52bbd46074505273c693fd6c52cff3bd4a33f3af0054ace3bad56ea55bc9\nfc29001a0e61a50917d367b5d4f652ac064a0e2dbe3bd54eb968e035ffe535ce4d5ffe1a78054afc7b6bf85eaf0d5134f00a94e0fd35bcdfaab73aa67f054af0\n95357cb5d0ec7855c3bf02059444076be8825f29b717abcd20634677acf0baeff5aaa5d4f91205d59055979c62cc22b1a9d642748fc53d0048204582448e984f\nf1180da1985ffff0e9eb5f7e7376c92480ba9ba28871182d940abd4219fecb5f4f1da984a22d8c3463490b88f0b52149c7e1c3984c45c3fd18bcba1ae4e4d5ab\ne3c72f8f1fff7cfce4c9f1e31fd3b9952bc36e074513ddeeafefbef8fbf923e7cf9fbef9ebe997c9d4ab78aee38da559ddc38a979138f9eac5eb972f4e9e7dfe\ne6fba716efcd18edebf001093177aee323e7160b61819609f07efc7616830011dda2194d388a909cc5e2bf2b02037d7d8e28b2e05ad88ce39d1894c606bc3abb\n6710ee07f14c108bc76b416800f718a32d165ba3704dcea58579308b26f6c9e3998ebb85d0a16dee368a8c2c77675390586273d90eb041f326459140131c61e1\nc86bec0063cbeaee1262c4758f0c63c6d958387789d342c41a9201d937aa6969b44342c8cbdc4610f26dc466ef8ed362d4b6ea0e3e3491706f206a213fc0d408\ne355341328b4b91ca090ea01df4522b091eccfe3a18eeb7201999e60ca9cee08736eb3b911c37ab5a45f43206ed6b4efd179682263410e6c3e7711633ab2c30e\nda010aa7366c9f44818efd881f408922e7261336f81e33ef10790e7940d1c674df21d848f7e96a701b1456a7b42c107965165b72791533a37efb733a4658490d\ne8bf21eb21894ed3f81575f7ff3b75070d3df9fab9654117a3e876c7463ade52cb9b31b1de4c3b2b0abe09b7aadb6d168fc8bb2fdb1d348b6e62b853d67bd77b\nd57eafdaeeff5eb537ddcf17afd54b7906e596bbd664b3aeb6eee1c69dfb9850da17738a77b9dabc73684aa31e0c4a3bf5f08ab327b9690087f24e86090cdc24\n46cac68999f88488a01fa0296cf18bae7432e1a9eb0977a68cc3ce5f0d5b7d4b3c9d857b6c943cb0168bf2e134110f8ec472bce067e3f0b0211274a5ba7c08cb\ndc2bb613f5b0bc20206ddf8684369949a26c21515d0cca20a94773089a85845ad985b0a85b58d4a4fb45aad65800b52c2bb06b7260afd5707d0f4cc0081eaa10\nc52399a724d58becaa645e64a63705d3a800d8432c2a6099e9bae4ba7179727549a99d21d30609addc4c122a32aa87f1008d705a9d72f42c34de36d7f5654a0d\n7a32146a3e28ad258d6aeddf589c37d760b7aa0d34d2958246ce51c3ad947d2899219a36dc313cf8c3613885dae172b78be8045ea20d459cdcf0e7519669cc45\n07f12009b8129d440d422270ec5012365cb9fc2c0d34521aa2b8154b2008ef2cb93ac8cabb460e926e26198fc77828f4b46b2332d2c929287ca215d6abcafcfc\n6069c96690ee7e303a72f6e92cbe85a0c4fc6a5106704438bc002a26d11c1178a19909d9b2fe561a532abbfa1b455543c938a2d300a51d4517f304aea43ca3a3\nceb2186867e99a21a05a48d246b83f910d560faad14db3ae9170d8d8754f379291d34473d9330d55915dd3ae62c60c8b36b012cbf335798dd522c4a0697a874f\na47b5572eb0bad5bd927645d02029ec5cfd275cfd010346acbc90c6a92f1ba0c4bcd4e47cddeb158e029d4ced22434d5af2cdcaec42deb11d6e960f05c9d1fec\n56ab1686c68b7da58ab4fa00a27f9c60fbf7403c3af01a78460557a9844f0f31820d515fed4912d9805be4be486f0d3872663169b80f0a7ed36b97fc76ae50f3\nbb39afec157235bf59ce357dbf5cecfac542a7557a088d450461d14f3ebef4e02d149da79f60d4f8da679870f1a2edd2908579a63eb3e41571f519a658b27d86\n19c80f2cae4340741e544abd7ab9deaae4eae5662fe7755ab55cbd5d69e53a9576b5d3ebb4fd5abdf7d0750e15d86b96db5ea55bcb558aed76ceab1424fd5a3d\n57f54aa5a6576dd6ba5ef361ba8d819527f291c602c2ab786dff030000ffff0300504b0304140006000800000021000dd1909fb60000001b0100002700000074\n68656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384\ne4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d26\n2452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe51\n4173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000\n000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b000000000000\n00000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000\n190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d00140006000800000021006ab3c999d7060000941a000016000000\n00000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b010000\n2700000000000000000000000000e10900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000dc0a00000000}\n{\\*\\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d\n617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169\n6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363\n656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}\n{\\*\\latentstyles\\lsdstimax376\\lsdlockeddef0\\lsdsemihiddendef0\\lsdunhideuseddef0\\lsdqformatdef0\\lsdprioritydef99{\\lsdlockedexcept \\lsdqformat1 \\lsdpriority0 \\lsdlocked0 Normal;\\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 1;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 3;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 4;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 5;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 6;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 7;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 8;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority9 \\lsdlocked0 heading 9;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 1;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 3;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 4;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 5;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 6;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 7;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 8;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index 9;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 1;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 3;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 4;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 5;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 6;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 7;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 8;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority39 \\lsdlocked0 toc 9;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Normal Indent;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 footnote text;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 annotation text;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 header;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 footer;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 index heading;\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority35 \\lsdlocked0 caption;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 table of figures;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 envelope address;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 envelope return;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 footnote reference;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 annotation reference;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 line number;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 page number;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 endnote reference;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 endnote text;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 table of authorities;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 macro;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 toa heading;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Bullet;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Number;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List 3;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List 4;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List 5;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Bullet 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Bullet 3;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Bullet 4;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Bullet 5;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Number 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Number 3;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Number 4;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Number 5;\\lsdqformat1 \\lsdpriority10 \\lsdlocked0 Title;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Closing;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Signature;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority1 \\lsdlocked0 Default Paragraph Font;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text Indent;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Continue;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Continue 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Continue 3;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Continue 4;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 List Continue 5;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Message Header;\\lsdqformat1 \\lsdpriority11 \\lsdlocked0 Subtitle;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Salutation;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Date;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text First Indent;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text First Indent 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Note Heading;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text 3;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text Indent 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Body Text Indent 3;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Block Text;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Hyperlink;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 FollowedHyperlink;\\lsdqformat1 \\lsdpriority22 \\lsdlocked0 Strong;\n\\lsdqformat1 \\lsdpriority20 \\lsdlocked0 Emphasis;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Document Map;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Plain Text;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 E-mail Signature;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Top of Form;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Bottom of Form;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Normal (Web);\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Acronym;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Address;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Cite;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Code;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Definition;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Keyboard;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Preformatted;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Sample;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Typewriter;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 HTML Variable;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 annotation subject;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 No List;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Outline List 1;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Outline List 2;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Outline List 3;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Balloon Text;\\lsdpriority39 \\lsdlocked0 Table Grid;\n\\lsdsemihidden1 \\lsdlocked0 Placeholder Text;\\lsdqformat1 \\lsdpriority1 \\lsdlocked0 No Spacing;\\lsdpriority60 \\lsdlocked0 Light Shading;\\lsdpriority61 \\lsdlocked0 Light List;\\lsdpriority62 \\lsdlocked0 Light Grid;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1;\\lsdpriority64 \\lsdlocked0 Medium Shading 2;\\lsdpriority65 \\lsdlocked0 Medium List 1;\\lsdpriority66 \\lsdlocked0 Medium List 2;\\lsdpriority67 \\lsdlocked0 Medium Grid 1;\\lsdpriority68 \\lsdlocked0 Medium Grid 2;\n\\lsdpriority69 \\lsdlocked0 Medium Grid 3;\\lsdpriority70 \\lsdlocked0 Dark List;\\lsdpriority71 \\lsdlocked0 Colorful Shading;\\lsdpriority72 \\lsdlocked0 Colorful List;\\lsdpriority73 \\lsdlocked0 Colorful Grid;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 1;\n\\lsdpriority61 \\lsdlocked0 Light List Accent 1;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 1;\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 1;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 1;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 1;\n\\lsdsemihidden1 \\lsdlocked0 Revision;\\lsdqformat1 \\lsdpriority34 \\lsdlocked0 List Paragraph;\\lsdqformat1 \\lsdpriority29 \\lsdlocked0 Quote;\\lsdqformat1 \\lsdpriority30 \\lsdlocked0 Intense Quote;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 1;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 1;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 1;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 1;\\lsdpriority70 \\lsdlocked0 Dark List Accent 1;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 1;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 1;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 1;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 2;\\lsdpriority61 \\lsdlocked0 Light List Accent 2;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 2;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 2;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 2;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 2;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 2;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 2;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 2;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 2;\\lsdpriority70 \\lsdlocked0 Dark List Accent 2;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 2;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 2;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 2;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 3;\\lsdpriority61 \\lsdlocked0 Light List Accent 3;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 3;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 3;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 3;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 3;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 3;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 3;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 3;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 3;\\lsdpriority70 \\lsdlocked0 Dark List Accent 3;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 3;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 3;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 3;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 4;\\lsdpriority61 \\lsdlocked0 Light List Accent 4;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 4;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 4;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 4;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 4;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 4;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 4;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 4;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 4;\\lsdpriority70 \\lsdlocked0 Dark List Accent 4;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 4;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 4;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 4;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 5;\\lsdpriority61 \\lsdlocked0 Light List Accent 5;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 5;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 5;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 5;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 5;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 5;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 5;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 5;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 5;\\lsdpriority70 \\lsdlocked0 Dark List Accent 5;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 5;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 5;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 5;\\lsdpriority60 \\lsdlocked0 Light Shading Accent 6;\\lsdpriority61 \\lsdlocked0 Light List Accent 6;\\lsdpriority62 \\lsdlocked0 Light Grid Accent 6;\n\\lsdpriority63 \\lsdlocked0 Medium Shading 1 Accent 6;\\lsdpriority64 \\lsdlocked0 Medium Shading 2 Accent 6;\\lsdpriority65 \\lsdlocked0 Medium List 1 Accent 6;\\lsdpriority66 \\lsdlocked0 Medium List 2 Accent 6;\n\\lsdpriority67 \\lsdlocked0 Medium Grid 1 Accent 6;\\lsdpriority68 \\lsdlocked0 Medium Grid 2 Accent 6;\\lsdpriority69 \\lsdlocked0 Medium Grid 3 Accent 6;\\lsdpriority70 \\lsdlocked0 Dark List Accent 6;\\lsdpriority71 \\lsdlocked0 Colorful Shading Accent 6;\n\\lsdpriority72 \\lsdlocked0 Colorful List Accent 6;\\lsdpriority73 \\lsdlocked0 Colorful Grid Accent 6;\\lsdqformat1 \\lsdpriority19 \\lsdlocked0 Subtle Emphasis;\\lsdqformat1 \\lsdpriority21 \\lsdlocked0 Intense Emphasis;\n\\lsdqformat1 \\lsdpriority31 \\lsdlocked0 Subtle Reference;\\lsdqformat1 \\lsdpriority32 \\lsdlocked0 Intense Reference;\\lsdqformat1 \\lsdpriority33 \\lsdlocked0 Book Title;\\lsdsemihidden1 \\lsdunhideused1 \\lsdpriority37 \\lsdlocked0 Bibliography;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdqformat1 \\lsdpriority39 \\lsdlocked0 TOC Heading;\\lsdpriority41 \\lsdlocked0 Plain Table 1;\\lsdpriority42 \\lsdlocked0 Plain Table 2;\\lsdpriority43 \\lsdlocked0 Plain Table 3;\\lsdpriority44 \\lsdlocked0 Plain Table 4;\n\\lsdpriority45 \\lsdlocked0 Plain Table 5;\\lsdpriority40 \\lsdlocked0 Grid Table Light;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light;\\lsdpriority47 \\lsdlocked0 Grid Table 2;\\lsdpriority48 \\lsdlocked0 Grid Table 3;\\lsdpriority49 \\lsdlocked0 Grid Table 4;\n\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful;\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 1;\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 1;\n\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 1;\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 1;\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 1;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 1;\n\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 1;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 2;\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 2;\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 2;\n\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 2;\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 2;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 2;\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 2;\n\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 3;\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 3;\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 3;\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 3;\n\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 3;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 3;\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 3;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 4;\n\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 4;\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 4;\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 4;\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 4;\n\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 4;\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 4;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 5;\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 5;\n\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 5;\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 5;\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 5;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 5;\n\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 5;\\lsdpriority46 \\lsdlocked0 Grid Table 1 Light Accent 6;\\lsdpriority47 \\lsdlocked0 Grid Table 2 Accent 6;\\lsdpriority48 \\lsdlocked0 Grid Table 3 Accent 6;\n\\lsdpriority49 \\lsdlocked0 Grid Table 4 Accent 6;\\lsdpriority50 \\lsdlocked0 Grid Table 5 Dark Accent 6;\\lsdpriority51 \\lsdlocked0 Grid Table 6 Colorful Accent 6;\\lsdpriority52 \\lsdlocked0 Grid Table 7 Colorful Accent 6;\n\\lsdpriority46 \\lsdlocked0 List Table 1 Light;\\lsdpriority47 \\lsdlocked0 List Table 2;\\lsdpriority48 \\lsdlocked0 List Table 3;\\lsdpriority49 \\lsdlocked0 List Table 4;\\lsdpriority50 \\lsdlocked0 List Table 5 Dark;\n\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful;\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 1;\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 1;\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 1;\n\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 1;\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 1;\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 1;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 1;\n\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 2;\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 2;\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 2;\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 2;\n\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 2;\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 2;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 2;\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 3;\n\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 3;\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 3;\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 3;\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 3;\n\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 3;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 3;\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 4;\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 4;\n\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 4;\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 4;\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 4;\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 4;\n\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 4;\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 5;\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 5;\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 5;\n\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 5;\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 5;\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 5;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 5;\n\\lsdpriority46 \\lsdlocked0 List Table 1 Light Accent 6;\\lsdpriority47 \\lsdlocked0 List Table 2 Accent 6;\\lsdpriority48 \\lsdlocked0 List Table 3 Accent 6;\\lsdpriority49 \\lsdlocked0 List Table 4 Accent 6;\n\\lsdpriority50 \\lsdlocked0 List Table 5 Dark Accent 6;\\lsdpriority51 \\lsdlocked0 List Table 6 Colorful Accent 6;\\lsdpriority52 \\lsdlocked0 List Table 7 Colorful Accent 6;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Mention;\n\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Smart Hyperlink;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Hashtag;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Unresolved Mention;\\lsdsemihidden1 \\lsdunhideused1 \\lsdlocked0 Smart Link;}}{\\*\\datastore 01050000\n02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000\nd0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nfffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\nffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000d096\nbf3efc84db01feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000\n00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000\n000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000000105000000000000}}"
  },
  {
    "path": "licenses/license_en.txt",
    "content": "This project is issued based on the Apache License 2.0 license. The following agreements are supplemental to the Apache License 2.0. In the event of a conflict, the following agreements shall prevail.\n\nDEFINITIONS:\n\n\"This Project\" refers to the LX Music Desktop Edition (aka lx-music-desktop) project. \"User\" refers to the user who signed this agreement. \"Official Music Streaming Service\" refers to collectively the official streaming service corresponding to the music source. \"Copyrighted Data\" refers to data including, but not limited to, pictures, audio, names, etc., for which others own the copyright.\n\n1. Data Source\n\n1.1 The principle of the online data sources of the various official music streaming services of this project is to draw data from its open servers (the same as the data obtained in the unlogged-in state in the official music streaming service app). After simply screening and merging the data, this project is responsible for the legitimacy and accuracy of the data.\n\n1.2 The ability of this project itself does not obtain certain audio data. The online audio data source used in this project comes from the online link provided by the \"API\" selected in the software settings. For example, when playing a song, this project only transmits information such as the song title, artist, and other information to the \"API\". If the \"API\" returns a link, this project will think that this is the audio data of the song for use, but as for whether this is the correct audio data, this project cannot verify its accuracy. So, in the process of using this project, there may be a problem that the audio you want to play does not correspond to the audio you actually play, or you cannot play it.\n\n1.3 Data in this project other than official music streaming service (such as lists in \"Your Library\") comes from the user's local system or a synchronization service that the user connected to. This project is not responsible for the legality and accuracy of this data.\n\n2. Copyrighted Data\n\n2.1 During the process of using this project, copyrighted data may be generated. For this copyrighted data, this project does not have their ownership. In order to avoid infringement, users must remove copyrighted data generated during the process of using this project ** within  24 hours **.\n\n3. Alias of Music Streaming Service\n\n3.1 The official music streaming service alias within this project is a term used within this project to refer to the official music streaming service and does not contain malicious intent. If the operator of the official music streaming service feels it is inappropriate, you can contact this project to request changes or removals.\n\n4. Use of Resources\n\n4.1 The parts used in this project include, but are not limited to, fonts, pictures, and other resources from the Internet. If infringement occurs, you can contact this project.\n\n5. Disclaimer\n\n5.1 The use of this project includes any direct, indirect, special, accidental, or result damage due to any nature caused by this agreement or use or inability to use this item (including but not limited to the loss of goodwill, stop work, computer, computer Damage compensation caused by faults or any or all other commercial damage or losses) is the responsibility of users.\n\n6. Use Restrictions\n\n6.1 This project is completely free and open source and is published on GitHub for the learning exchanges of technology for people all over the world. This project does not guarantee that the technology in this project may violate local laws and regulations.\n\n6.2 ** This project is prohibited in violation of local laws and regulations. ** The user is solely responsible for any violation of law caused by the use of this project that the user knows or does not know is not permitted by local law and regulations.\n\n7. Copyright Protection\n\n7.1 The operation of a music streaming service is not an easy task. Please respect copyrights and support the genuine.\n\n8. Non-commercial Nature\n\n8.1 This project is only used for exploring and research on technical feasibility and does not accept any business (including but not limited to advertising, etc.) cooperation and donations.\n\n9. Accepting Agreement\n\n9.1 If you use this project, it will represent you accept this agreement.\n\n* If the agreement is updated, you will not be notified separately. You can check it out by visiting the project address.\n* The content of this agreement is translated from the Chinese version of the agreement.\n\nBy: lyswhut\n"
  },
  {
    "path": "licenses/license_zh.txt",
    "content": "本项目基于 Apache License 2.0 许可证发行，以下协议是对于 Apache License 2.0 的补充，如有冲突，以以下协议为准。\n\n词语约定：本协议中的“本项目”指 LX Music 桌面版项目；“使用者”指签署本协议的使用者；“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称；“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。\n\n一、数据来源\n\n1.1 本项目的各官方平台在线数据来源原理是从其公开服务器中拉取数据（与未登录状态在官方平台 APP 获取的数据相同），经过对数据简单地筛选与合并后进行展示，因此本项目不对数据的合法性、准确性负责。\n\n1.2 本项目本身没有获取某个音频数据的能力，本项目使用的在线音频数据来源来自软件设置内“自定义源”设置所选择的“源”返回的在线链接。例如播放某首歌，本项目所做的只是将希望播放的歌曲名、艺术家等信息传递给“源”，若“源”返回了一个链接，则本项目将认为这就是该歌曲的音频数据而进行使用，至于这是不是正确的音频数据本项目无法校验其准确性，所以使用本项目的过程中可能会出现希望播放的音频与实际播放的音频不对应或者无法播放的问题。\n\n1.3 本项目的非官方平台数据（例如“我的列表”内列表）来自使用者本地系统或者使用者连接的同步服务，本项目不对这些数据的合法性、准确性负责。\n\n二、版权数据\n\n2.1 使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在 **24 小时内** 清除使用本项目的过程中所产生的版权数据。\n\n三、音乐平台别名\n\n3.1 本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼，不包含恶意。如果官方音乐平台觉得不妥，可联系本项目更改或移除。\n\n四、资源使用\n\n4.1 本项目内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系本项目移除。\n\n五、免责声明\n\n5.1 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。\n\n六、使用限制\n\n6.1 本项目完全免费，且开源发布于 GitHub 面向全世界人用作对技术的学习交流。本项目不对项目内的技术可能存在违反当地法律法规的行为作保证。\n\n6.2 **禁止在违反当地法律法规的情况下使用本项目。** 对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。\n\n七、版权保护\n\n7.1 音乐平台不易，请尊重版权，支持正版。\n\n八、非商业性质\n\n8.1 本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。\n\n九、接受协议\n\n9.1 若你使用了本项目，即代表你接受本协议。\n\n* 若协议更新，恕不另行通知，可到开源地址查看。\n\nBy: 落雪无痕\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"lx-music-desktop\",\n  \"version\": \"2.12.1\",\n  \"description\": \"一个免费的音乐查找助手\",\n  \"main\": \"./dist/main.js\",\n  \"scripts\": {\n    \"pack\": \"node build-config/pack.js && npm run pack:win:setup:x64\",\n    \"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\",\n    \"pack:win:setup:x86_64\": \"node build-config/build-pack.js target=win arch=x86_64 type=setup\",\n    \"pack:win:setup:x64\": \"node build-config/build-pack.js target=win arch=x64 type=setup\",\n    \"pack:win:setup:x86\": \"node build-config/build-pack.js target=win arch=x86 type=setup\",\n    \"pack:win:setup:arm64\": \"node build-config/build-pack.js target=win arch=arm64 type=setup\",\n    \"pack:win:portable\": \"npm run pack:win:portable:x86_64 && npm run pack:win:portable:x64 && npm run pack:win:portable:x86\",\n    \"pack:win:portable:x86_64\": \"node build-config/build-pack.js target=win arch=x86_64 type=portable\",\n    \"pack:win:portable:x64\": \"node build-config/build-pack.js target=win arch=x64 type=portable\",\n    \"pack:win:portable:x86\": \"node build-config/build-pack.js target=win arch=x86 type=portable\",\n    \"pack:win:7z\": \"npm run pack:win:7z:x64\",\n    \"pack:win:7z:x64\": \"node build-config/build-pack.js target=win arch=x64 type=green\",\n    \"pack:win:7z:arm64\": \"node build-config/build-pack.js target=win arch=arm64 type=green\",\n    \"pack:win7:setup:x64\": \"node build-config/build-pack.js target=win arch=x64 type=win7_setup\",\n    \"pack:win7:7z:x64\": \"node build-config/build-pack.js target=win arch=x64 type=win7_green\",\n    \"pack:win7:7z:x86\": \"node build-config/build-pack.js target=win arch=x86 type=win7_green\",\n    \"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\",\n    \"pack:linux:appImage\": \"node build-config/build-pack.js target=linux arch=x64 type=appImage\",\n    \"pack:linux:deb\": \"npm run pack:linux:deb:amd64 && npm run pack:linux:deb:arm64 && npm run pack:linux:deb:armv7l\",\n    \"pack:linux:deb:amd64\": \"node build-config/build-pack.js target=linux arch=x64 type=deb\",\n    \"pack:linux:deb:arm64\": \"node build-config/build-pack.js target=linux arch=arm64 type=deb\",\n    \"pack:linux:deb:armv7l\": \"node build-config/build-pack.js target=linux arch=armv7l type=deb\",\n    \"pack:linux:rpm\": \"node build-config/build-pack.js target=linux arch=x64 type=rpm\",\n    \"pack:linux:pacman\": \"node build-config/build-pack.js target=linux arch=x64 type=pacman\",\n    \"pack:mac\": \"node build-config/pack.js && npm run pack:mac:dmg && npm run pack:mac:dmg:arm64\",\n    \"pack:mac:dmg\": \"node build-config/build-pack.js target=mac arch=x64 type=dmg\",\n    \"pack:mac:dmg:arm64\": \"node build-config/build-pack.js target=mac arch=arm64 type=dmg\",\n    \"pack:dir\": \"node build-config/pack.js && node build-config/build-pack.js target=dir\",\n    \"publish\": \"node publish\",\n    \"publish:win:setup:x64\": \"node build-config/build-pack.js target=win arch=x64 type=setup publish=always\",\n    \"publish:win:setup:x86\": \"node build-config/build-pack.js target=win arch=x86 type=setup publish=always\",\n    \"publish:win:setup:arm64\": \"node build-config/build-pack.js target=win arch=arm64 type=setup publish=always\",\n    \"publish:win:setup:x86_64\": \"node build-config/build-pack.js target=win arch=x86_64 type=setup publish=always\",\n    \"publish:win:portable\": \"npm run publish:win:portable:x86_64 && npm run publish:win:portable:x64 && npm run publish:win:portable:x86\",\n    \"publish:win:portable:x86_64\": \"node build-config/build-pack.js target=win arch=x86_64 type=portable publish=always\",\n    \"publish:win:portable:x64\": \"node build-config/build-pack.js target=win arch=x64 type=portable publish=always\",\n    \"publish:win:portable:x86\": \"node build-config/build-pack.js target=win arch=x86 type=portable publish=always\",\n    \"publish:win:7z:x64\": \"node build-config/build-pack.js target=win arch=x64 type=green publish=always\",\n    \"publish:win:7z:arm64\": \"node build-config/build-pack.js target=win arch=arm64 type=green publish=always\",\n    \"publish:win7:setup:x64\": \"node build-config/build-pack.js target=win arch=x64 type=win7_setup publish=always\",\n    \"publish:win7:7z:x64\": \"node build-config/build-pack.js target=win arch=x64 type=win7_green publish=always\",\n    \"publish:win7:7z:x86\": \"node build-config/build-pack.js target=win arch=x86 type=win7_green publish=always\",\n    \"publish:mac:dmg\": \"node build-config/build-pack.js target=mac arch=x64 type=dmg publish=always\",\n    \"publish:mac:dmg:arm64\": \"node build-config/build-pack.js target=mac arch=arm64 type=dmg publish=always\",\n    \"publish:linux:deb:amd64\": \"node build-config/build-pack.js target=linux arch=x64 type=deb publish=always\",\n    \"publish:linux:deb:arm64\": \"node build-config/build-pack.js target=linux arch=arm64 type=deb publish=always\",\n    \"publish:linux:deb:armv7l\": \"node build-config/build-pack.js target=linux arch=armv7l type=deb publish=always\",\n    \"publish:linux:appImage\": \"node build-config/build-pack.js target=linux arch=x64 type=appImage publish=always\",\n    \"publish:linux:rpm\": \"node build-config/build-pack.js target=linux arch=x64 type=rpm publish=always\",\n    \"publish:linux:pacman\": \"node build-config/build-pack.js target=linux arch=x64 type=pacman publish=always\",\n    \"dev\": \"cross-env NODE_OPTIONS=--max-http-header-size=200000 node build-config/runner-dev.js\",\n    \"build:theme\": \"node src/common/theme/createThemes.js\",\n    \"build\": \"node build-config/pack.js\",\n    \"build:main\": \"cross-env NODE_ENV=production webpack --config build-config/main/webpack.config.prod.js --progress\",\n    \"build:renderer\": \"cross-env NODE_ENV=production webpack --config build-config/renderer/webpack.config.prod.js --progress\",\n    \"build:renderer-lyric\": \"cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress\",\n    \"build:renderer-scripts\": \"cross-env NODE_ENV=production webpack --config build-config/renderer-scripts/webpack.config.prod.js --progress\",\n    \"lint\": \"eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly src\",\n    \"lint:fix\": \"eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly --fix src\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"dp\": \"cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm run pack\",\n    \"up\": \"cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm i\"\n  },\n  \"browserslist\": [\n    \"Electron 22.3.0\"\n  ],\n  \"engines\": {\n    \"node\": \">= 22\",\n    \"npm\": \">=8.5.2\"\n  },\n  \"macLanguagesInfoPlistStrings\": {\n    \"en\": {\n      \"CFBundleDisplayName\": \"LX Music\",\n      \"CFBundleName\": \"LX Music\"\n    },\n    \"zh_CN\": {\n      \"CFBundleDisplayName\": \"LX Music\",\n      \"CFBundleName\": \"LX Music\"\n    },\n    \"zh_TW\": {\n      \"CFBundleDisplayName\": \"LX Music\",\n      \"CFBundleName\": \"LX Music\"\n    }\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lyswhut/lx-music-desktop.git\"\n  },\n  \"keywords\": [\n    \"music-player\",\n    \"electron-app\",\n    \"vuejs3\"\n  ],\n  \"author\": {\n    \"name\": \"lyswhut\",\n    \"email\": \"lyswhut@qq.com\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/lyswhut/lx-music-desktop/issues\"\n  },\n  \"homepage\": \"https://github.com/lyswhut/lx-music-desktop#readme\",\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.29.0\",\n    \"@babel/eslint-parser\": \"^7.28.6\",\n    \"@babel/plugin-syntax-dynamic-import\": \"^7.8.3\",\n    \"@babel/plugin-transform-class-properties\": \"^7.28.6\",\n    \"@babel/plugin-transform-modules-umd\": \"^7.27.1\",\n    \"@babel/plugin-transform-runtime\": \"^7.29.0\",\n    \"@babel/preset-env\": \"^7.29.0\",\n    \"@babel/preset-typescript\": \"^7.28.5\",\n    \"@babel/runtime\": \"^7.28.6\",\n    \"@tsconfig/recommended\": \"^1.0.13\",\n    \"@types/better-sqlite3\": \"^7.6.13\",\n    \"@types/needle\": \"^3.3.0\",\n    \"@types/node\": \"^20.19.33\",\n    \"@types/tunnel\": \"^0.0.7\",\n    \"@types/ws\": \"8.5.4\",\n    \"@vue/language-plugin-pug\": \"^3.2.4\",\n    \"babel-loader\": \"^10.0.0\",\n    \"browserslist\": \"^4.28.1\",\n    \"chalk\": \"^4.1.2\",\n    \"changelog-parser\": \"^3.0.1\",\n    \"copy-webpack-plugin\": \"^13.0.1\",\n    \"core-js\": \"^3.48.0\",\n    \"cross-env\": \"^10.1.0\",\n    \"css-loader\": \"^7.1.3\",\n    \"css-minimizer-webpack-plugin\": \"^7.0.4\",\n    \"del\": \"^6.1.1\",\n    \"electron\": \"37.6.1\",\n    \"electron-builder\": \"^26.8.0\",\n    \"electron-debug\": \"^3.2.0\",\n    \"electron-devtools-installer\": \"github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03\",\n    \"electron-to-chromium\": \"^1.5.286\",\n    \"electron-updater\": \"6.8.3\",\n    \"eslint\": \"^8.57.1\",\n    \"eslint-config-standard\": \"^17.1.0\",\n    \"eslint-config-standard-with-typescript\": \"^43.0.1\",\n    \"eslint-formatter-friendly\": \"github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53\",\n    \"eslint-plugin-html\": \"^8.1.4\",\n    \"eslint-plugin-vue\": \"^9.33.0\",\n    \"eslint-plugin-vue-pug\": \"^0.6.2\",\n    \"eslint-webpack-plugin\": \"^4.2.0\",\n    \"html-webpack-plugin\": \"^5.6.6\",\n    \"less\": \"^4.5.1\",\n    \"less-loader\": \"^12.3.1\",\n    \"mini-css-extract-plugin\": \"^2.10.0\",\n    \"node-loader\": \"^2.1.0\",\n    \"postcss\": \"^8.5.6\",\n    \"postcss-loader\": \"^8.2.1\",\n    \"postcss-pxtorem\": \"^6.1.0\",\n    \"pug\": \"^3.0.3\",\n    \"pug-plain-loader\": \"^1.1.0\",\n    \"rimraf\": \"^6.1.3\",\n    \"spinnies\": \"github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d\",\n    \"svg-sprite-loader\": \"^6.0.11\",\n    \"svg-transform-loader\": \"^2.0.13\",\n    \"svgo-loader\": \"^4.0.0\",\n    \"terser\": \"^5.46.0\",\n    \"terser-webpack-plugin\": \"^5.3.16\",\n    \"tree-kill\": \"^1.2.2\",\n    \"ts-loader\": \"^9.5.4\",\n    \"typescript\": \"5.9.3\",\n    \"vue-eslint-parser\": \"^9.4.3\",\n    \"vue-loader\": \"^17.4.2\",\n    \"webpack\": \"^5.105.2\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"webpack-dev-server\": \"5.2.3\",\n    \"webpack-hot-middleware\": \"github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1\",\n    \"webpack-merge\": \"^6.0.1\"\n  },\n  \"dependencies\": {\n    \"@simonwep/pickr\": \"^1.9.1\",\n    \"better-sqlite3\": \"^12.6.2\",\n    \"bufferutil\": \"^4.1.0\",\n    \"comlink\": \"~4.3.1\",\n    \"crypto-js\": \"^4.2.0\",\n    \"electron-log\": \"^5.4.3\",\n    \"font-list\": \"^2.0.2\",\n    \"iconv-lite\": \"^0.7.2\",\n    \"image-size\": \"^1.1.0\",\n    \"jschardet\": \"^3.1.4\",\n    \"long\": \"^5.3.2\",\n    \"message2call\": \"^0.1.3\",\n    \"music-metadata\": \"^11.12.0\",\n    \"needle\": \"github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060\",\n    \"node-id3\": \"^0.2.9\",\n    \"sortablejs\": \"^1.15.7\",\n    \"tunnel\": \"^0.0.6\",\n    \"undici\": \"^7.22.0\",\n    \"utf-8-validate\": \"^6.0.6\",\n    \"vue\": \"~3.3.13\",\n    \"vue-router\": \"~4.5.1\",\n    \"ws\": \"^8.19.0\"\n  },\n  \"overrides\": {\n    \"got\": \"^11\",\n    \"json5\": \"latest\",\n    \"node-abi\": \"latest\",\n    \"minimatch\": \"latest\",\n    \"semver\": \"latest\",\n    \"svg-transform-loader\": {\n      \"postcss\": \"^8\"\n    },\n    \"svg-sprite-loader\": {\n      \"postcss\": \"^8\"\n    },\n    \"svg-baker\": {\n      \"postcss\": \"^8\"\n    },\n    \"braces\": \"latest\",\n    \"node-gyp-build\": \"latest\",\n    \"micromatch\": \"latest\",\n    \"http-cache-semantics\": \"latest\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "// const autoprefixer = require('autoprefixer')\nconst pxtorem = require('postcss-pxtorem')\n\nmodule.exports = {\n  plugins: [\n    pxtorem({\n      rootValue: 16,\n      unitPrecision: 5,\n      propList: [\n        'font', 'font-size',\n        'letter-spacing',\n        'padding', 'margin',\n        'padding-*', 'margin-*',\n        'height', 'width',\n        '*-height', '*-width',\n        'flex', '::-webkit-scrollbar',\n        'top', 'left', 'bottom', 'right',\n        'border-radius', 'gap',\n      ],\n      selectorBlackList: ['html', 'ignore-to-rem'],\n      replace: true,\n      mediaQuery: false,\n      minPixelValue: 0,\n      exclude: [/node_modules/i],\n    }),\n    // autoprefixer(),\n  ],\n}\n"
  },
  {
    "path": "publish/changeLog.md",
    "content": "我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 优化\n\n- 优化托盘图标行为：在非 Windows 系统中，点击托盘图标时不再显示主窗口\n\n### 修复\n\n- 修复音量条在调整音量时实际音量与显示的数值不一致的问题（#2606）\n- 修复某些情况下搜索框的搜索按钮布局错位的问题（#2622）\n- 修复 wyy 部分歌曲无法被搜索的问题（#2666, @ikun0014）\n"
  },
  {
    "path": "publish/index.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst chalk = require('chalk')\nconst clearAssets = require('./utils/clearAssets')\n// const packAssets = require('./utils/packAssets')\n// const compileAssets = require('./utils/compileAssets')\nconst updateVersionFile = require('./utils/updateChangeLog')\n// const copyFile = require('./utils/copyFile')\n// const githubRelease = require('./utils/githubRelease')\n// const { parseArgv } = require('./utils')\n\nconst run = async() => {\n  // const params = parseArgv(process.argv.slice(2))\n  // const bak = await updateVersionFile(params.ver)\n  const bak = await updateVersionFile(process.argv.slice(2)[0])\n\n  try {\n    console.log(chalk.blue('Clearing assets...'))\n    await clearAssets()\n    console.log(chalk.green('Assets clear completed...'))\n\n    // console.log(chalk.blue('Compileing assets...'))\n    // await compileAssets()\n    // console.log(chalk.green('Asset compiled successfully.'))\n\n    // console.log(chalk.blue('Building assets...'))\n    // await packAssets()\n    // console.log(chalk.green('Asset build successfully.'))\n\n    // console.log(chalk.blue('Copy files...'))\n    // await copyFile()\n    // console.log(chalk.green('Complete copy of all files.'))\n\n    // console.log(chalk.blue('Create release...'))\n    // await githubRelease(params)\n    // console.log(chalk.green('Release created.'))\n\n    console.log(chalk.green('日志更新完成~'))\n  } catch (error) {\n    console.log(error)\n    console.log(chalk.red('程序发布失败'))\n    console.log(chalk.blue('正在还原版本信息'))\n    fs.writeFileSync(path.join(__dirname, './version.json'), bak.version_bak + '\\n', 'utf-8')\n    fs.writeFileSync(path.join(__dirname, '../package.json'), bak.pkg_bak + '\\n', 'utf-8')\n    console.log(chalk.blue('版本信息还原完成'))\n  }\n}\n\n\nrun()\n"
  },
  {
    "path": "publish/utils/clearAssets.js",
    "content": "const del = require('del')\n// const copyFile = require('./copyFile')\n\nmodule.exports = () => {\n  del.sync(['publish/assets/*'])\n  // return copyFile(false)\n}\n\n"
  },
  {
    "path": "publish/utils/compileAssets.js",
    "content": "const { spawn } = require('child_process')\nconst { jp } = require('./index')\nconst chalk = require('chalk')\n\nmodule.exports = () => new Promise((resolve, reject) => {\n  const pack = spawn('node', [jp('../../build-config/pack.js')])\n\n  // pack.stdout.on('data', (data) => {\n  //   console.log(chalk.blue(data))\n  // })\n\n  pack.stderr.on('data', (data) => {\n    console.log(chalk.red(data))\n  })\n\n  pack.on('close', code => {\n    if (code === 0) {\n      resolve()\n    } else {\n      console.log(chalk.red('Asset compilation failed.'))\n      reject()\n    }\n  })\n})\n\n"
  },
  {
    "path": "publish/utils/copyFile.js",
    "content": "const fs = require('fs')\nconst chalk = require('chalk')\nconst { jp, copyFile } = require('./index')\n\nconst buildDir = '../../build'\n\n\nconst getBuildFileName = () => {\n  const names = []\n  const pathRegExp = [\n    /latest\\.yml$/,\n    /\\.exe$/,\n    /\\.blockmap$/,\n  ]\n  const files = fs.readdirSync(jp(buildDir), 'utf8')\n  files.forEach(name => {\n    pathRegExp.forEach(regexp => {\n      if (regexp.test(name)) names.push(name)\n    })\n  })\n  return names\n}\n\nconst copy = names => {\n  const tasks = names.map(name => copyFile(jp(buildDir, name), jp('../assets', name)))\n  return Promise.all(tasks)\n}\n\n\nmodule.exports = (isCopyVersion = true) => {\n  copy(getBuildFileName()).then(() => {\n    if (isCopyVersion) fs.writeFileSync(jp('../assets/version.json'), JSON.stringify(require('../version.json')), 'utf8')\n  }).catch(err => {\n    console.log(err)\n    console.log(chalk.red('File copy failed.'))\n    return Promise.reject(err)\n  })\n}\n"
  },
  {
    "path": "publish/utils/cos.js",
    "content": "const fs = require('fs')\nconst { jp, sizeFormate } = require('./index')\nconst chalk = require('chalk')\nconst COS = require('cos-nodejs-sdk-v5')\nconst config = require('./cosConfig')\nconst MultiProgress = require('multi-progress')\nconst multi = new MultiProgress(process.stderr)\n\nconst cos = new COS({\n  SecretId: config.secretId,\n  SecretKey: config.secretKey,\n  KeepAlive: false,\n})\n\nconst getCosFileList = () => new Promise((resolve, reject) => {\n  cos.getBucket({\n    Bucket: config.bucket,\n    Region: config.region,\n    Prefix: config.prefix,\n  }, function(err, data) {\n    if (err) {\n      console.log(err)\n      reject(err)\n      console.log(chalk.red('COS文件列表获取失败'))\n    }\n    resolve(data.Contents.filter(o => o.Key !== config.prefix).map(o => o.Key.replace(config.prefix, '')))\n  })\n})\n\nconst getLocalFileList = () => fs.readdirSync(jp('../assets'), 'utf8')\n\nconst diffFileList = (localFiles, cosFiles) => {\n  const removeFiles = []\n  cosFiles.forEach(file => {\n    let index = localFiles.indexOf(file)\n    if (index < 0) return removeFiles.push(file)\n    localFiles.splice(index, 1)\n  })\n  if (cosFiles.includes('latest.yml')) {\n    removeFiles.push('latest.yml')\n    localFiles.push('latest.yml')\n  }\n  if (cosFiles.includes('version.json')) {\n    removeFiles.push('version.json')\n    localFiles.push('version.json')\n  }\n  return removeFiles\n}\n\nconst deleteCosFiles = files => new Promise((resolve, reject) => {\n  files = files.map(f => ({ Key: config.prefix + f }))\n  cos.deleteMultipleObject({\n    Bucket: config.bucket,\n    Region: config.region,\n    Objects: files,\n  }, function(err, data) {\n    if (err) {\n      console.log(err)\n      reject(err)\n    }\n    resolve()\n  })\n})\n\nconst createProgressBar = (name, spacekLen, total) => multi.newBar(\n  `${`  ${name}`.padEnd(spacekLen, ' ')} :status [:bar] :current/:total  :percent  :speed`, {\n    complete: '=',\n    incomplete: ' ',\n    width: 30,\n    total,\n  })\n\n\nconst uploadFile = (fileName, len) => new Promise((resolve, reject) => {\n  const filePath = jp('../assets', fileName)\n  // let size = fs.statSync(filePath).size\n  let bar = null\n  let prevLoaded = 0\n\n  cos.sliceUploadFile({\n    Bucket: config.bucket,\n    Region: config.region,\n    Key: config.prefix + fileName, /* 必须 */\n    FilePath: filePath, /* 必须 */\n    // TaskReady: function(taskId) { /* 非必须 */\n    //   console.log(taskId)\n    // },\n    onHashProgress(progressData) { /* 非必须 */\n      if (!bar) {\n        bar = createProgressBar(fileName, len, progressData.total)\n        prevLoaded = 0\n      }\n      bar.tick(progressData.loaded - prevLoaded, {\n        status: '校验中',\n        speed: sizeFormate(progressData.speed) + '/s',\n      })\n      prevLoaded = progressData.loaded\n      // console.log('校验', fileName, JSON.stringify(progressData))\n      // console.log('校验', JSON.stringify(progressData))\n    },\n    onProgress(progressData) { /* 非必须 */\n      if (!bar) {\n        bar = createProgressBar(fileName, len, progressData.total)\n        prevLoaded = 0\n      }\n      bar.tick(progressData.loaded - prevLoaded, {\n        status: '上传中',\n        speed: sizeFormate(progressData.speed) + '/s',\n      })\n      prevLoaded = progressData.loaded\n      // console.log('上传', fileName, JSON.stringify(progressData))\n      // console.log('上传', JSON.stringify(progressData))\n    },\n  }, (err, data) => {\n    if (err) {\n      console.log(err)\n      return reject(err)\n    }\n    bar.tick({\n      status: '已完成',\n      speed: '',\n    })\n    resolve(data)\n  })\n})\n\n\nmodule.exports = async() => {\n  console.log(chalk.blue('正在获取COS文件列表...'))\n  const cosFiles = await getCosFileList()\n  console.log(chalk.green('COS文件列表获取成功'))\n  const uploadFiles = getLocalFileList()\n  const removeFiles = diffFileList(uploadFiles, cosFiles)\n  if (removeFiles.length) {\n    console.log(chalk.blue('共需删除') + chalk.yellow(removeFiles.length) + chalk.blue('个文件'))\n    console.log(chalk.blue('正在从COS删除多余的文件...'))\n    await deleteCosFiles(removeFiles)\n    console.log(chalk.green('多余文件删除成功'))\n  } else {\n    console.log(chalk.blue('没有在COS发现多余的文件'))\n  }\n  if (uploadFiles.length) {\n    console.log(chalk.blue('共需上传') + chalk.green(uploadFiles.length) + chalk.blue('个文件'))\n    console.log(chalk.blue('正在上传新文件到COS...'))\n    let max = Math.max(...uploadFiles.map(f => f.length)) + 2\n    let tasks = uploadFiles.map(f => uploadFile(f, max))\n    await Promise.all(tasks)\n    console.log(''.padEnd(Math.max(2, tasks.length - 2), '\\n'))\n    console.log(chalk.green('所有文件上传完成'))\n  } else {\n    console.log(chalk.blue('没有需要上传的文件'))\n  }\n}\n"
  },
  {
    "path": "publish/utils/cosConfig.js",
    "content": "module.exports = {\n  secretId: '',\n  secretKey: '',\n\n  bucket: '', // 存储桶\n  region: '', // 区域\n  prefix: '', // 路径\n}\n"
  },
  {
    "path": "publish/utils/githubRelease.js",
    "content": "const fs = require('fs')\nconst ghRelease = require('gh-release')\nconst token = require('./githubToken')\nconst pkg = require('../../package.json')\nconst { jp } = require('./index')\n\nconst changeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8')\n\nconst assetsDir = '../assets'\n\nconst getBuildFiles = () => {\n  const files = []\n  const pathRegExp = [\n    /latest\\.yml$/,\n    /\\.exe$/,\n    /\\.blockmap$/,\n  ]\n  const names = fs.readdirSync(jp(assetsDir), 'utf8')\n  names.forEach(name => {\n    pathRegExp.forEach(regexp => {\n      if (regexp.test(name)) files.push(jp(assetsDir, name))\n    })\n  })\n  return files\n}\n\n// all options have defaults and can be omitted\nconst options = {\n  tag_name: `v${pkg.version}`,\n  target_commitish: 'master',\n  name: `v${pkg.version}`,\n  body: changeLog,\n  draft: false,\n  prerelease: false,\n  repo: pkg.name,\n  owner: pkg.author,\n  endpoint: 'https://api.github.com', // for GitHub enterprise, use http(s)://hostname/api/v3\n  auth: {\n    token,\n  },\n  assets: getBuildFiles(),\n}\n\n\nmodule.exports = ({ isDraft = false, isPrerelease = false, target_commitish = 'master' }) => new Promise((resolve, reject) => {\n  options.target_commitish = target_commitish\n  options.draft = isDraft\n  options.prerelease = isPrerelease\n\n  ghRelease(options, function(err, result) {\n    if (err) return reject(err)\n    resolve(result)\n    console.log(result) // create release response: https://developer.github.com/v3/repos/releases/#response-4\n  })\n})\n\n"
  },
  {
    "path": "publish/utils/index.js",
    "content": "const fs = require('fs')\nconst path = require('path')\n\nexports.jp = (...p) => p.length ? path.join(__dirname, ...p) : __dirname\n\nexports.copyFile = (source, target) => new Promise((resolve, reject) => {\n  const rd = fs.createReadStream(source)\n  rd.on('error', err => reject(err))\n  const wr = fs.createWriteStream(target)\n  wr.on('error', err => reject(err))\n  wr.on('close', () => resolve())\n  rd.pipe(wr)\n})\n\n/**\n * 时间格式化\n * @param {Date} d 格式化的时间\n * @param {boolean} b 是否精确到秒\n */\nexports.formatTime = (d, b) => {\n  const _date = d == null ? new Date() : typeof d == 'string' ? new Date(d) : d\n  const year = _date.getFullYear()\n  const month = fm(_date.getMonth() + 1)\n  const day = fm(_date.getDate())\n  if (!b) return year + '-' + month + '-' + day\n  return year + '-' + month + '-' + day + ' ' + fm(_date.getHours()) + ':' + fm(_date.getMinutes()) + ':' + fm(_date.getSeconds())\n}\n\nfunction fm(value) {\n  if (value < 10) return '0' + value\n  return value\n}\n\nexports.sizeFormate = size => {\n  // https://gist.github.com/thomseddon/3511330\n  if (!size) return '0 b'\n  let units = ['b', 'kB', 'MB', 'GB', 'TB']\n  let number = Math.floor(Math.log(size) / Math.log(1024))\n  return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`\n}\n\nexports.parseArgv = argv => {\n  const params = {}\n  argv.forEach(item => {\n    const argv = item.split('=')\n    switch (argv[0]) {\n      case 'ver':\n        params.ver = argv[1]\n        break\n      case 'draft':\n        params.isDraft = argv[1] === 'true' || argv[1] === undefined\n        break\n      case 'prerelease':\n        params.isPrerelease = argv[1] === 'true' || argv[1] === undefined\n        break\n      case 'target_commitish':\n        params.target_commitish = argv[1]\n        break\n    }\n  })\n  return params\n}\n"
  },
  {
    "path": "publish/utils/packAssets.js",
    "content": "const builder = require('electron-builder')\nconst chalk = require('chalk')\n\n// Promise is returned\nmodule.exports = () => builder.build().catch(error => {\n  console.log(error)\n  console.log(chalk.red('Asset build failed.'))\n  return Promise.reject(error)\n})\n\n"
  },
  {
    "path": "publish/utils/parseChangelog.js",
    "content": "/**\n *\n * @param {string} text\n * @returns\n */\nexport const parseChangelog = async(text) => {\n  const versions = []\n  const lines = text.split(/\\r\\n|\\r|\\n/)\n  let currentVersion = null\n  let currentDate = null\n  let currentDesc = ''\n\n  for (const line of lines) {\n    const versionMatch = line.match(/^\\s*##\\s+\\[?(\\d+\\.\\d+\\.\\d+)\\]?.*?-\\s+(\\d{4}-\\d{2}-\\d{2})$/)\n    if (versionMatch) {\n      if (currentVersion) {\n        versions.push({\n          version: currentVersion,\n          date: currentDate,\n          desc: currentDesc.trim(),\n        })\n      }\n      currentVersion = versionMatch[1]\n      currentDate = versionMatch[3]\n      currentDesc = ''\n    } else {\n      currentDesc += `${line}\\n`\n    }\n  }\n\n  if (currentVersion) {\n    versions.push({\n      version: currentVersion,\n      date: currentDate,\n      desc: currentDesc.trim(),\n    })\n  }\n\n  return versions\n}\n"
  },
  {
    "path": "publish/utils/updateChangeLog.js",
    "content": "const fs = require('fs')\nconst { jp, formatTime } = require('./index')\nconst pkgDir = '../../package.json'\nconst pkg = require(pkgDir)\nconst version = require('../version.json')\nconst chalk = require('chalk')\nconst pkg_bak = JSON.stringify(pkg, null, 2)\nconst version_bak = JSON.stringify(version, null, 2)\nconst changelogPath = jp('../../CHANGELOG.md')\nconst { parseChangelog } = require('./parseChangelog')\n\n// const md_renderer = markdownStr => new (require('markdown-it'))({\n//   html: true,\n//   linkify: true,\n//   typographer: true,\n//   breaks: true,\n// }).render(markdownStr)\n\nconst getPrevVer = () => parseChangelog(fs.readFileSync(changelogPath, 'utf-8').toString()).then(versions => {\n  if (!versions.length) throw new Error('CHANGELOG 无法解析到版本号')\n  return versions[0].version\n})\n\nconst updateChangeLog = async(newVerNum, newChangeLog) => {\n  let changeLog = fs.readFileSync(changelogPath, 'utf-8')\n  const prevVer = await getPrevVer()\n  const log = `## [${newVerNum}](${pkg.repository.url.replace(/^git\\+(http.+)\\.git$/, '$1')}/compare/v${prevVer}...v${newVerNum}) - ${formatTime()}\\n\\n${newChangeLog}`\n  fs.writeFileSync(changelogPath, changeLog.replace(/(## \\[(?:\\d+\\.))/, log + '\\n$1'), 'utf-8')\n}\n\n// const renderChangeLog = md => md_renderer(md)\n\n\nmodule.exports = async newVerNum => {\n  if (!newVerNum) newVerNum = pkg.version\n  const newMDChangeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8')\n  // const newChangeLog = renderChangeLog(newMDChangeLog)\n  version.history.unshift({\n    version: version.version,\n    desc: version.desc,\n  })\n  version.version = newVerNum\n  version.desc = newMDChangeLog.replace(/(?:^|(\\n))#{1,6} (.+)\\n/g, '$1$2').trim()\n  pkg.version = newVerNum\n\n  console.log(chalk.blue('new version: ') + chalk.green(newVerNum))\n\n  fs.writeFileSync(jp('../version.json'), JSON.stringify(version) + '\\n', 'utf-8')\n\n  fs.writeFileSync(jp(pkgDir), JSON.stringify(pkg, null, 2) + '\\n', 'utf-8')\n\n  await updateChangeLog(newVerNum, newMDChangeLog)\n\n  return {\n    pkg_bak,\n    version_bak,\n    // changeLog: newChangeLog,\n  }\n}\n\n"
  },
  {
    "path": "publish/version.json",
    "content": "{\"version\":\"2.12.1\",\"desc\":\"我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\\n\\n优化\\n- 优化托盘图标行为：在非 Windows 系统中，点击托盘图标时不再显示主窗口\\n\\n修复\\n- 修复音量条在调整音量时实际音量与显示的数值不一致的问题（#2606）\\n- 修复某些情况下搜索框的搜索按钮布局错位的问题（#2622）\",\"history\":[{\"version\":\"2.12.0\",\"desc\":\"我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\\n\\n新增\\n- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项 (#2360)\\n  *默认启用，关闭后将使用系统原生的窗口样式，该选项重启软件后生效*\\n- 开放 API 新增播放器声音大小、静音、播放进度控制、完整歌词获取，详情看接入文档 (#2386)\\n- 新增「设置 → 播放设置 → 调换歌词翻译与歌词罗马音位置」选项，默认关闭 (#2451)\\n- 新增启动参数 `-hidden`，在启动时将软件最小化到系统托盘 (#2459)\\n- 新增 Any Listen 歌词（用于支持已下载歌曲的歌词逐字播放）标签数据读取与播放 (#2485)\\n- 新增 Any Listen 歌词（包含逐字歌词、翻译、罗马音歌词，如果有）嵌入与下载，默认启用\\n- 下载列表菜单新增歌曲添加弹窗，允许将所选歌曲的在线版本添加到收藏列表 (#2537)\\n\\n修复\\n- 尝试修复进度为0时仍然显示下载完成的问题 (#2471)\\n- 修复TX源搜索失败 (#2575 @Folltoshe)\\n- 修复MG源歌单加载失败\\n- 修复MG源评论加载失败\\n\\n变更\\n- 调换「歌词翻译」与「歌词罗马音」的位置，现在歌词罗马音在歌词翻译的上方展示\\n  *若你想要恢复以前的行为，可以开启「调换歌词翻译与歌词罗马音位置」选项*\\n- 更新代理配置规则，现在不启用代理时，图片、音频加载将不再走系统代理 (#2382 @Folltoshe)\\n- 字体设置可以最多设置两种字体（[any-listen#82](https://github.com/any-listen/any-listen/issues/82)）\\n\\n其他\\n- 更新 Electron 到 37.6.0\"},{\"version\":\"2.11.0\",\"desc\":\"新增\\n- 新增「快进/快退5秒」自定义快捷键设置（#2289）\\n- 新增「设置 → 桌面歌词设置 → 暂停时提高歌词透明度」设置，默认启用（#2294）\\n\\n修复\\n- 修复 Windows 下桌面歌词最小高度与宽度设置问题（#2244）\\n- 修复 Windows 下界面缩放后移动桌面歌词会改变歌词窗口大小的问题（#2244）\\n- 修复 tx 歌单搜索名字、描述出现乱码的问题（#2250）\\n- 修复本地 FLAC 文件内嵌歌词无法读取的问题\\n- 修复潜在播放暂停的问题\\n- 修复 kw 歌单详情出现打开失败的问题（#2317）\\n- 修复 kg 热门评论无法获取的问题\\n- 修复桌面歌词被遮挡时会被暂停的问题（#2320）\\n- 修复 kg 歌单打开失败的问题（thanks @Folltoshe）\\n\\n优化\\n- 允许更小的桌面歌词窗口宽度\\n- 允许拖动桌面歌词控制栏空白处移动歌词窗口（#2280）\\n- 优化「自定义源管理」对话框在小窗口下的布局（#2247, @3gf8jv4dv）\\n- 优化软件文案编排（#2259, #2266, #2269, #2296, @3gf8jv4dv）\\n\\n变更\\n- 我的列表-歌曲菜单中的 歌曲换源 功能从之前的类似软连接的形式改成替换歌曲的形式，也就是说，现在该功能相当于快速在线搜索歌曲，确认换源后将自动将原来的歌曲删除再将选择的歌曲插入被删除歌曲的位置。\\n\\n其他\\n- 更新项目文档（@3gf8jv4dv）\\n- 更新 Electron 到 35.2.2\"},{\"version\":\"2.10.0\",\"desc\":\"落雪祝大家新年快乐！\\n\\n关于之前提到的新项目\\n新项目我取名叫 Any Listen，希望它能像它的名字一样让我们能到处任意听歌。\\n经过一年多的开发，因各种原因，实际进度比预期的慢，但还是赶在年前发布了第一个web服务预览版，第一个版本仅支持播放服务器上的歌曲，扩展功能暂时未能开放，但已趋于完成，一两个月内可以搞定。\\n目前的版本仅是“能用”的状态，因时间关系，部分UI未能重新设计，但后面会继续完善。\\n该项目目前的目标用户是拥有自己服务器且上面存储有歌曲的人使用。\\n\\n项目刚发布，文档未能完善，遇到使用问题或有任何建议欢迎提 issue 交流，\\n项目地址： https://github.com/any-listen/any-listen\\n\\n---\\n\\n*感谢 @3gf8jv4dv 对 LX 系列项目翻译、文档等文案的大幅修订优化。*\\n\\n不兼容性变更\\nLinux 系统至少需要 `GLIBC_2.29` 版本才能运行。\\n\\n由于将 Electron 升级到 v32.x，原生库的编译被限制到不低于 C++ 20，试了几次无法在 docker 镜像 `node:16` 安装 gcc-10，最终将构建使用镜像更新到 `node:18`。\\n\\n新增\\n- 新增下载的歌曲按列表名分组的功能，默认关闭，可以通过「设置 → 下载设置 → 将文件保存到以对应列表命名的子目录中」启用（#2145）\\n- 新增托盘图标样式「跟随系统亮暗模式」设置，可以在「设置 → 其他」里启用 （#2016）\\n- 支持本地同名 `.krc` 格式歌词文件的读取（#2053）\\n- 开放 API 新增播放器播放/暂停、切歌、收藏当前播放歌曲等接口调用，详情看文档「开放 API 服务」部分（#2077, @14Kay）\\n\\n优化\\n- 优化正常播放结束时的下一首歌曲播放衔接度，在歌曲即将结束播放时将预获取下一首歌曲的播放链接，减少自动切歌时的等待时间（#2126）\\n- 优化歌曲换源机制，提升换源正确率\\n- 优化 Windows 平台上桌面歌词窗口大小调整机制，改用原生的窗口调整方式（#2137）\\n- 修正搜索歌曲提示框文案（#2050）\\n- 优化播放详情页 UI，修复「歌曲名」「艺术家」等文字过长时被截断的问题（#2049）\\n- Scheme URL 的播放歌曲允许更长的专辑名称\\n- 播放本地歌曲时，将优先尝试读取本地同名 `.jpg` 或 `.png` 图片作为播放封面显示，若文件不存在则从音频文件内读取，最后再尝试使用在线图片（#2096）\\n- 客户端模式的同步服务连接允许重定向 5 次（#2109）\\n- 更新软件默认使用的字体，修复 macOS Sequoia (15) 上界面出现乱码的问题（#2076）\\n- 优化简体、繁体中文文案编排，大幅修订英语文案编排（#2159, #2166, #2174 等, @3gf8jv4dv）\\n- 优化排序歌曲、主题名称、添加/编辑主题、列表更新管理等对话框布局及长文本显示效果（#2176, #2188, #2189, #2198 等, @3gf8jv4dv）\\n\\n修复\\n- 修复歌单详情页内歌单名字过长时的 UI 显示问题（#2028）\\n- 修复获取自定义环境音效预设列表逻辑问题\\n- 修复 `.m4a` 文件内嵌歌词无法读取的问题（#2090）\\n- 修复 Windows 任务管理器中的进程名显示为软件描述的问题（#2147）\\n- 修复本地歌曲同名歌词文件调整偏移时间后，下次再播放时调整的设置未被应用的问题（#2139）\\n- 修复首次打开软件后直接创建并删除列表时的报错问题（#2175, @14Kay）\\n\\n变更\\n- 不再长期缓存换源歌曲信息\\n- 更新软件默认使用的字体，现在软件尽量使用系统自带的默认字体\\n- Linux 系统至少需要 `GLIBC_2.29` 版本才能运行\\n\\n其他\\n- 更新 Readme 文档，优化文案编排（#2146, Thanks @3gf8jv4dv）\\n- 更新 Issue 模板（#2153, @3gf8jv4dv）\\n- 更新项目文档（@3gf8jv4dv）\\n- 修订项目协议文件（#2146, #2152, @3gf8jv4dv）\\n- 更新 Electron 到 v32.3.0\"},{\"version\":\"2.9.0\",\"desc\":\"新增\\n- 新增 设置-播放设置-是否将歌词显示在状态栏 设置，默认关闭，该功能只在 MacOS 下可用（#1940）\\n- 新增设置-播放详情页设置-延迟歌词滚动设置（#1985）\\n- 新增鼠标在音量按钮使用滚轮时可以调整音量大小的功能（#2000）\\n- 新增设置-下载设置-同时下载任务数设置（#1498）\\n- 新增 我的列表-歌曲右击菜单-歌曲换源 功能，换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲，该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题\\n\\n优化\\n- 优化侧栏图标显示，修复图标可能被裁切的问题（#1960）\\n- 托盘图标添加当前播放歌曲名字显示\\n- 优化本地歌曲内嵌封面过大时的加载方式\\n- 将下载歌曲的歌手信息中的分隔符从 `、` 替换为 `;` 以确保音乐元数据在写入时的兼容性和一致性（#1989 @qnnp-me）\\n\\n修复\\n- 修复 MacOS 下点击 dock 右键菜单的退出按钮时，程序没有退出的问题（#1923）\\n- 修复 OpenAPI 的 `lyricLineAllText` 在切换到无歌词的音乐时内容没有更新的问题（#1925）\\n- 修复切换音源时可能出现切换死循环的问题\\n- 尝试修复某些情况下播放音频时，处于播放状态但是进度条不走的问题\\n- 修复程序目录路径存在 `#` 或 `%` 时，自定义源、托盘等图标异常的问题（#1997）\\n\\n变更\\n- 简化了应用退出行为，据测试，现在 linux 下，若启用了托盘，dock 右键菜单的 退出、关闭所有 之类的功能将不再退出程序，需改用托盘的退出按钮退出程序\\n- 现在如果在设置或者启动参数配置了代理服务，那么应用内的图片、音频加载，歌曲下载也将走代理\\n\\n其他\\n- 更新 electron 到 v30.4.0\"},{\"version\":\"2.8.0\",\"desc\":\"我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\\n\\n新增\\n- 新增 设置-播放设置-使用设备能处理的最大声道数输出音频 设置（未启用时固定为2声道输出），由于这用到高级音频API，考虑到在某些设备上的兼容问题，默认禁用（#1873）\\n- 允许添加 `m4a`、`oga` 格式的本地歌曲到列表中（#1864）\\n- 开放API支持跨域请求（#1872 @Ceale）\\n- Scheme URL API新增 `music/searchPlay` 支持，用于搜索并播放指定的歌曲名字，详细入参请阅读 Scheme URL 支持文档（#1886）\\n\\n优化\\n- 优化白色托盘图标显示，修复windows下托盘图标不清晰的问题（#1842）\\n\\n修复\\n- 修复存在多级弹窗时的背景显示问题\\n- 增大在线导入自定义源文件的大小限制问题（#1857）\\n- 修复Mac下窗口出现残留阴影的问题，这解决了Mac下桌面歌词出现残留阴影的远古bug，感谢 @zclorne （#1869, Thanks @zclorne）\\n- 增大在线导入自定义源文件的大小限制，解决某些音源无法导入的问题（#1857）\\n- 修复Mac下即使开启了托盘， `cmd+w` 仍会中断播放的问题（#1844）\\n- 修复播放详情页的歌词无法使用触碰拖动的问题（#1865）\\n- 修复与优化繁体中文、英语翻译显示（#1845）\\n- 修复歌曲时文件名过长导致歌曲无法下载的问题（#1877）\\n- 修复文本提示气泡在内容过长时，文本未被换行而被截断的问题\\n- 修复翻页按钮栏切页按钮只显示前几页的问题\\n\\n变更\\n- 设置-播放设置-优先播放320k音质选项改为“优先播放的音质”，允许选择更高优先播放的音质，如果歌曲及音源支持的话（#1839）\\n\\n开放API变更\\n- `/status` 的入参现在与 `/subscribe-player-status` 保持一致\\n- `/status` 新增 `filter` 入参用于过滤返回的字段，并内置了默认值，与之前相比默认不再返回 `picUrl`\\n- `/status` 及 `/subscribe-player-status` 的可用字段名添加了 `lyricLineAllText`，它对应的值是当前句歌词及扩展歌词文本（扩展歌词包含翻译、罗马音等，按换行符分割）\\n\\n详情看开放API接入文档\\n\\n其他\\n- 更新 electron 到 v28.3.3\"},{\"version\":\"2.7.0\",\"desc\":\"新增\\n- 主题编辑器添加“深色字体”选项，启用后将减少字体颜色梯度，各类字体（正文、标签字体等）颜色将更接近，这有助于解决创建全透明主题时可能出现的字体配色问题（#1799）\\n- 新增在线自定义源导入功能，允许通过http/https链接导入自定义源\\n- 新增HTTP开放API服务，默认关闭，该服务可以为第三方软件提供调用LX的能力，可用API看[说明文档](https://lyswhut.github.io/lx-music-doc/desktop/open-api)（#1824）\\n- 托盘菜单新增播放、切歌、收藏控制\\n- 添加当前软件版本所对应的代码提交版本、提交时间的显示，可到设置-版本更新查看\\n\\n优化\\n- 主题设置默认折叠其他主题以优化进入设置界面时的性能\\n- 不再丢弃kg源逐行歌词（@helloplhm-qwq）\\n- 支持kw源排行榜显示大小（revert @Folltoshe #1460）\\n- 托盘菜单添加多语言支持（#1802）\\n- 优化本地歌曲换源匹配机制\\n\\n修复\\n- 修复某些情况下歌曲加载时间过长时不会自动跳到下一首的问题\\n- 修复mg歌词在某些情况下获取失败的问题（#1783）\\n- 修复mg歌单搜索（@helloplhm-qwq）\\n- 修复kg最新评论无法获取的问题（@helloplhm-qwq）\\n- 修复更新超时弹窗在非更新阶段意外弹出的问题（#1797）\\n- 修复网络代理设置没有对自定义源的网络请求生效的问题（#1814）\\n\\n移除\\n- 移除未使用的网络代理设置用户名、密码设置，实际上在 v1.20.0 起这两个设置就没有在被内部使用\\n\\n其他\\n- 更新 electron 到 v28.3.0\"},{\"version\":\"2.6.0\",\"desc\":\"提交祝大家新年快乐！\\n\\n更新前需要注意：\\n由于自定义源的调用方式变更，可能会导致某些第三方源停止工作，如果出现这种情况，你需要将LX回退到 v2.5.0\\n\\n新增\\n- 若自定义源初始化失败，将会出现弹窗提示初始化失败的详情\\n- 添加win7_x64架构的安装版安装包构建\\n- 新增播放歌曲时阻止电脑休眠，默认启用，可到设置-播放设置关闭（#1563）\\n\\n优化\\n- 更新zh-tw翻译\\n- 自定义源列显示源版本号、作者名字\\n- 优化列表全选机制，修复列表未获得焦点时仍然可以全选的问题\\n- 优化搜索框交互逻辑，防止鼠标操作时意外搜索候选列表的内容\\n- 添加对wy源某些歌曲有问题的歌词进行修复\\n- 改进本地音乐在线信息的匹配机制\\n- 优化任务下载状态显示，现在下载时若数据传输完成但数据写入未完成时会显示相应的状态\\n- 添加对下载歌曲时封面图片大小的控制处理（#1609）\\n- 添加创建同名列表时的二次确认（#1621）\\n\\n修复\\n- 修复备份文件无法导入json格式的问题\\n- Windows、MacOS平台下的字体列表取消使用原生方式获取以修复某些字体应用后无效的问题（#1596）\\n- 修复亮暗主题自动切换功能无效的问题（#1697）\\n- 修复 MacOS 平台在 Finder 打开文件或目录时应用卡死的问题（#1684）\\n- 修复下载模块在数据写入速度较慢的情况下出现任务及文件异常的问题\\n- 修复临时列表变更会意外触发同步的问题\\n- 修复最小化后再隐藏窗口时，托盘菜单的显示主界面功能异常的问题\\n\\n变更\\n- 播放歌曲时默认会阻止系统进入休眠状态，若你不行软件阻止系统休眠，可以到设置-播放设置取消勾选“播放歌曲时阻止电脑休眠”设置\\n\\n其他\\n- 移除所有内置源，由于收到腾讯投诉要求停止提供软件内置的连接到他们平台的在线播放及下载服务，所以从即日（2023年10月18日）起LX本身不再提供上述服务\\n- 更新 electron 到 v25.9.8\\n- 更新许可协议的排版，使其看起来更加清晰明了，更新数据来源原理说明\\n\\n自定义源的不兼容变更与新增内容（源开发者需要看）\\n自定义源的调用方式已改变：\\n\\n- 为了与移动端的调用方式统一，不再推荐使用 `window.lx` 对象（移动端无`window`对象），改用 `globalThis.lx`\\n- `inited` 事件不再需要传递 `status` 属性，脚本运行过程中，在成功调用 `inited` 事件之前的任何首次未捕获的错误都将视为初始化失败，所以现在若想人为让脚本初始化失败，直接抛出一个错误即可\\n- 新增 `globalThis.lx.env` 属性，桌面端环境固定为 `desktop`，移动端环境固定为 `mobile`\\n- 新增 `globalThis.lx.currentScriptInfo` 对象，可以从这里获取解析后的脚本头部注释信息及脚本原始内容，具体可用属性看文档说明\\n- `globalThis.lx.version` 属性更新到 `2.0.0`\\n- 自定义源不再使用`script`标签的形式执行，若要获取脚本原始代码字符串需从 `globalThis.lx.currentScriptInfo.rawScript` 属性获取\\n- 自定义源新增支持`local`源的`musicUrl`、`pic`、`lyric`的获取操作详情看自定义源文档说明\"},{\"version\":\"2.5.0\",\"desc\":\"落雪提前祝大家中秋快乐~🥮😘！\\n\\n不兼容性变更\\n- 由于微软及Electron即将结束对 Windows 7、Windows 8 的支持，所以从这个版本起，LX的默认 Windows 版也不再支持这些版本的系统，但考虑到仍然有许多人使用 Windows 7，我们特别构建了能在 Windows 7 上使用的免安装版（文件名带win7），需要注意的是这个版本将缺乏安全更新，若非必要情况，不要使用该版本\\n- 由于微软在 Windows 10 2004版本已删除对32位的OEM支持，所以在这个版本起，LX的默认 Windows 版已不再提供32位的支持\\n- 更改构建的文件名格式，主要修改linux下deb、rpm文件命名格式\\n\\n新增\\n- 新增Scheme URL对播放器的控制操作，新增的操作包含 播放、暂停、下一首、上一首等，详情看Scheme URL文档\\n\\n优化\\n- 通过歌曲菜单添加不喜欢歌曲时需要二次确认防止手抖\\n\\n修复\\n- 修复音频输出设备设置在重启软件后被重置的问题（#1568）\\n- 修复更换语言设置后源名称未更新的问题\\n- 修复点击搜索、排行榜等在线列表歌曲右键菜单歌曲详情页会意外将该歌曲添加不喜欢的问题\\n\\n其他\\n- 更新 electron 到 v25.8.3\"},{\"version\":\"2.4.1\",\"desc\":\"目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** ，其他渠道均为第三方转载发布，可信度请自行鉴别。\\n本项目无微信公众号之类的官方账号，谨防被骗。\\n\\n修复\\n- 修复 v2.4.0 的默认数据库版本号不对导致首次安装该版本的用户无法再次启动软件的问题\"},{\"version\":\"2.4.0\",\"desc\":\"目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** ，其他渠道均为第三方转载发布，可信度请自行鉴别。\\n本项目无微信公众号之类的官方账号，谨防被骗。\\n\\n不兼容性变更\\n该版本修改了同步协议逻辑，同步功能至少需要PC端v2.4.0或移动端v1.1.0版本或同步服务v2.0.0才能连接使用。\\n\\n新增\\n- 新增我的列表名右键菜单-排序歌曲-随机乱序功能，使用它可以对选中列表内歌曲进行随机重排（#1440）\\n- 新增数据同步服务端模式已认证设备列表管理，该功能位置：设置-数据同步-服务端模式-已认证设备列表\\n- 新增“不喜欢歌曲”功能，可以在我的列表或者在线列表内歌曲的右击菜单使用，还可以去“设置-其他”手动编辑不喜欢规则，注：“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲，但你仍可以手动播放这些歌曲\\n- 新增同步功能对“不喜欢歌曲”列表的同步\\n- 新增软件内快捷键“不喜欢该歌曲”设置，全局快捷键“收藏歌曲”、“取消收藏”、“不喜欢该歌曲”设置\\n- 新增设置-播放设置-点击相同列表内的歌曲切歌时是否清空已播放列表（随机模式下列表内所有歌曲会重新参与随机）选项，默认关闭\\n\\n优化\\n- 优化音效设置-环境音效启用、禁用时的操作效果显示，修复禁用环境音效时仍然可以调整增益、新增预设的问题\\n- 过滤翻译歌词或罗马音歌词中只有“//”的行（#1499）\\n- 点击打开歌单弹窗背景将不再自动关闭弹窗，防止选择输入框里的内容时意外关闭弹窗\\n- 优化数据传输逻辑，列表同步指令使用队列机制，保证列表同步操作的顺序\\n- 优化桌面歌词在开启 缩放当前播放的歌词 并关闭 延迟歌词滚动 时的歌词滚动位置计算问题，现在歌词滚动应该可以正确滚动到目标位置了\\n- 优化歌词在短时间内快速播放时的滚动效果，现在遇到这种情况时滚动将更平滑\\n\\n修复\\n- 修复字体设置某些字体无法应用的问题\\n- 修复搜索提示功能失效的问题（#1452, @Folltoshe）\\n- 修复我的列表名右键菜单-排序歌曲按专辑名排序无效的问题（#1440）\\n- 修复若路径存在 # 字符时，软件无法启动的问题\\n- 修复搜索框在某些情况下输入内容后搜索时会自动清空的问题（#1472）\\n- 修复某些tx源歌词因数据异常解析失败的问题\\n- 修复windows平台下隐藏窗口后再显示时任务栏按钮丢失的问题\\n- 修复首句歌词被提前播放的问题\\n- 修复潜在导致列表数据不同步的问题\\n- 修复kg无评论时的加载处理问题\\n\\n变更\\n- 播放模式应该只适用于列表内的歌曲，所以单曲循环模式不应对“稍后播放”的歌曲有效，该行为现在与移动端一致\\n- 随机模式下，通过点击与播放列表相同的列表切歌时，将不再清空已播放列表，即已播放的歌曲不再重新参与随机，若想恢复之前的行为可以去设置-播放设置启用清空已播放列表选项\\n\\n其他\\n- 更新 electron 到 v22.3.23\\n- 重构同步服务端功能部分代码，使其更易扩展新功能\"},{\"version\":\"2.3.0\",\"desc\":\"新增\\n- 新增音效设置（实验性功能），支持10段均衡器设置、内置的一些环境混响音效、音调升降调节、3D立体环绕音效（由于升降调需要实时处理音频数据，这会导致额外的CPU占用，已知问题：如果CPU资源不够时将处理导致任务堆积而出现声音异常，这时需要暂停播放一段时间等堆积的任务处理完毕再播放）\\n- 播放速率设置面板新增是否音调补偿设置，在调整播放速率后，可以选择是否启用音调补偿，默认启用\\n\\n优化\\n- Windows、MacOS平台下的字体列表改用原生方式获取，现在Windows平台下能显示当前已安装的更多类型字体了（注：MacOS平台未测，可用性未知）\\n- 移除桌面歌词窗口透明边距，在Linux下的桌面歌词可以完全拖到贴合屏幕边缘了\\n- 过滤嵌入、下载的翻译、罗马音歌词时间标签，与主歌词时间不匹配的歌词将被丢弃，防止出现原歌词与翻译歌词顺序错乱的问题（#1358）\\n\\n修复\\n- 修复列表名翻译显示\\n- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题，同时也可能导致同步数据丢失的问题（要完全修复这个问题还需要同时将移动端、同步服务更新到最新版本）\\n- 修复下载时出现302错误的问题\\n- 修复播放某些在线音频会没有声音的问题\\n- 修复改变播放速率时会导致歌词报错的问题\\n- 修复tx热门评论昵称被错误切割的问题 (#1397, By: @helloplhm-qwq, @Folltoshe)\\n- 修复wy源热搜词失效的问题（#1401, @Folltoshe）\\n- 修复Deepin 20下启用桌面歌词时可能会导致桌面卡死的问题（#1288）\\n- 修复添加单首歌曲弹窗列表创建按钮无法取消的问题\\n- 修复mg歌单搜索歌单播放数量显示问题\\n- 修复tx翻译歌词解析丢失的问题（更新版本后需手动清理一次歌词缓存）\\n\\n其他\\n- 更新 electron 到 v22.3.15\"},{\"version\":\"2.2.2\",\"desc\":\"修复\\n- 修复在低版本Linux amd64系统上无法启动的问题（glibc版本要求过高导致的，采用内置预编译二进制文件的方式解决）\\n- 修复添加歌曲弹窗默认列表名字显示问题\"},{\"version\":\"2.2.1\",\"desc\":\"优化\\n- 优化对系统Media Session的支持，现在切歌不会再会导致信息丢失的问题了\\n- 启用桌面歌词时，取消对歌词窗口的聚焦\\n- 增加kg歌单歌曲flac24bit显示（@helloplhm-qwq）\\n- 增加tx源热门评论图片显示（@Folltoshe）\\n- 优化更新弹窗弹出时机\\n- 优化搜索框背景配色，使其适应高透明主题\\n- 支持wy热门评论翻页\\n\\n修复\\n- 修复启用全局快捷键时与Media Session注册冲突的问题，启用全局快捷键时，不再注册媒体控制快捷键\\n- 修复mg搜索不显示时长的问题（@Folltoshe）\\n- 修复mg评论加载失败的问题（@Folltoshe）\\n- 修复对存在错误时间标签的歌词的解析\\n\\n其他\\n- 自定义源API utils对象新增`zlib.inflate`与`zlib.deflate`方法，API版本更新到 v1.3.0\\n- 更新kg、tx、wy等平台排行榜列表\\n- 更新 electron 到 v22.3.7\"},{\"version\":\"2.2.0\",\"desc\":\"从v2.2.0起，我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)，如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明\\n\\n不兼容性变更说明\\n- 同步功能，从这个版本起，数据同步功能至少需要移动端v1.0.0的版本才能连接，连接的地址格式也略有改变，详情看[文档说明](https://lyswhut.github.io/lx-music-doc/desktop/faq/sync)\\n\\n新增\\n- 重构数据同步功能，新增客户端模式\\n- 新增全屏时自动关闭歌词设置，默认开启，可以去设置-桌面歌词设置更改\\n- 新增设置-桌面歌词设置-重置窗口设置功能，点击时会重置桌面歌词窗口大小及位置\\n- 新增设置-其他-列表数据清理功能，点击时会清空已创建的所有列表及所有收藏的歌曲\\n\\n优化\\n- 支持wy源flac hires歌曲类型的显示\\n- 快捷键调整音量时每次加减2%音量改为4%（#1220）\\n- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容，保留点击切换显示/隐藏\\n- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示（#1231, #1238 By @helloplhm-qwq, @Folltoshe）\\n- 播放速率的粒度调整为0.01，范围0.6-2.0x\\n\\n修复\\n- 修复同步连接的处理问题\\n- 修复记住播放进度的情况下，使用Scheme URL打开应用播放的歌曲进度没有被重置的问题\\n- 修复使用酷狗码无法打开某些类型的歌单的问题\\n- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题\\n- 修复连续选择时的初始选择歌曲位置被意外改变的问题\\n\\n其他\\n- 更新 Electron 到v22.3.4\"},{\"version\":\"2.1.2\",\"desc\":\"修复\\n- 修复处于最新版本时更新弹窗日志内容显示异常的问题\\n- 修复更新到最新版本后的首次启动时的更新日志未显示的问题\"},{\"version\":\"2.1.1\",\"desc\":\"修复\\n- 修复检查更新日志地址不正确的问题\"},{\"version\":\"2.1.0\",\"desc\":\"由于软件内功能在设计时只考虑简单便捷性，是否对新手友好并不是我们考虑的重点，功能的新增、变更会在更新日志中注明，不会在软件内做指引提示，\\n因此为了更愉快地使用本软件，我们建议在使用新版本时阅读一遍更新日志以了解软件的变更情况，同时若遇到问题可以去阅读常见问题找解决方案\\n\\n新增\\n- 新增桌面歌词设置字体加粗设置，可以到设置-桌面歌词设置-加粗字体修改\\n- 新增是否自动下载更新设置，默认开启，可以去设置-软件更新更改\\n- 新增当前版本更新日志显示弹窗（建议大家阅读更新日志以了解当前版本的变化），在更新版本后将自动弹出\\n- 新增是否在更新版本的首次启动时显示更新日志弹窗设置，默认开启，可以去设置-软件更新更改\\n- 新增播放速率调整功能，可以去播放详情页的控制按钮调整，范围限制为x0.5至x2之间（#13）\\n- 添加wy、tx源（感谢某位不愿透露姓名的大佬提供的C++算法源码，但由于作者不希望公开，所以将会以预构建二进制文件的形式加入代码仓库中）逐字歌词的支持\\n- 新增设置-下载设置-是否嵌入翻译歌词、罗马音歌词设置，默认关闭\\n- 添加启动时的数据库表及表结构完整性校验，若未通过校验，则会显示弹窗提示后将该数据库重命名添加`.bak`后缀后重建数据库启动。对于某些人遇到更新到v2.0.0后出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表的问题，可以通过此特性自动重建数据库并重新迁移数据，不再需要手动去数据目录删除数据库\\n\\n优化\\n- 微调了桌面歌词逐行字体阴影，使其看起来更匀称\\n- 调整了桌面歌词在启用滚动到顶部时的距离，现在滚动到顶部的歌词更靠边，不再受字体大小、歌词间距影响\\n- 优化更新弹窗内容的显示，添加了自动更新失败时的更新指引\\n- 为所有文本输入框添加右键快速粘贴的功能，右击输入框可以自动粘贴剪贴板的文字，若选中文字时将粘贴并替换选中文字\\n- 防止桌面歌词窗口在屏幕分辨率变小时，窗口位置跟随分辨率变化的问题，现在若屏幕分辨率变小后窗口位置仍会在原始分辨率的位置（添加这个机制是为了解决屏幕分辨率被临时调整时的位置更新问题，如运行某些低分辨率的全屏游戏、高分辨率外接屏幕休眠时），但若你的分辨率调整不是临时的，因窗口在原始位置导致看不到窗口可以开关桌面歌词即可重新自动调节回屏幕内\\n\\n修复\\n- 修复播放下载列表的歌曲时，调整歌词偏移时间功能异常的问题\\n- 修复较旧Linux arm64系统下无法启动软件的问题（将预构建模块的所需glibc版本降级到2.28）（#1161）\\n- 修改列表响应式更新机制，尝试修复偶现的删除歌曲列表未更新的问题\\n- 修复某些kg歌单链接无法打开的问题\\n- 修复将桌面歌词放到屏幕边缘时，偶现的开启桌面歌词后出现歌词窗口位置出现少许偏移的问题，以及将歌词窗口调整到全屏大小后，重开桌面歌词窗口被缩小出现边距的问题\\n\\n其他\\n- 更新Electron到v22.3.0\"},{\"version\":\"2.0.5\",\"desc\":\"这应该是LX今年的最后一个版本，提前祝大家新年快乐~😘\\n\\n修复\\n- 修复声音输出设备更改时后的自动暂停播放设置无效的问题\\n- 重写桌面歌词窗口坐标的计算逻辑，修复桌面歌词移动到最边缘时，某些情况下在启用歌词后会出现窗口偏移的问题（远古bug了）\\n- 修复随机播放模式下使用稍后播放功能播放我的列表的歌曲时，切换下一曲永远是当前歌曲的问题（#1147）\\n- 修复macOS下的软件系统菜单中的退出功能不会完全退出软件的问题（#1148）\"},{\"version\":\"2.0.4\",\"desc\":\"修复\\n- 修复备份文件导入指引无法识别v2配置的问题\\n- 修复从搜索界面进入歌单详情后，若启用强迫症设置的清空功能会导致意外清空搜索框、搜索列表的问题\\n- 修复桌面歌词在启用卡拉OK歌词后字体边缘可能被截断的问题（特别是纵向歌词某些字的边角被截断导致后面的阴影露出来或阴影不均匀的问题）\\n- 修复桌面歌词启用歌词缩放后的阴影显示问题\\n- 修复Linux armv7l系统（如树莓派）下无法启动的问题（与修复Linux arm64的方法一样采用内置预编译模块的方式修复）\\n- 修复备份与恢复的列表导入列表信息设置逻辑问题与潜在导入问题\"},{\"version\":\"2.0.3\",\"desc\":\"修复\\n- 修复初始设置的桌面歌词窗口没有完全居右下角的问题\\n- 修复Linux arm64系统下无法启动的问题（#1102）\\n- 修复桌面歌词使用斜体出现截断的问题（#1106）\\n- 修复某些情况下歌词的滚动问题\\n- 修复禁用切歌时歌曲播放完毕后的歌曲信息显示问题\\n- 修复修改播放设置-音频输出设置后，所做的更改没有被保存的问题\\n\\n优化\\n- 点击打开歌单弹窗背景可以关闭弹窗（#1096）\"},{\"version\":\"2.0.2\",\"desc\":\"若你更新v2.0.0后，出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题，可以按以下方式解决：\\n\\n1. 根据你的平台类型，进入软件数据目录\\n   - Windows：`%APPDATA%/lx-music-desktop`\\n   - Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\\n   - macOS：`~/Library/Application Support/lx-music-desktop`\\n\\n2. 进入`LxDatas`目录，退出LX，删除`lx.data.db`文件，再启动软件即可\\n\\n若以上操作仍然不行，可以加交流群或者在GitHub开issue反馈\\n\\n修复\\n- 修复无效的歌曲信息导致我的列表数据迁移失败的问题\"},{\"version\":\"2.0.1\",\"desc\":\"若你更新v2.0.0后，出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题，可以按以下方式解决：\\n\\n1. 根据你的平台类型，进入软件数据目录\\n   - Windows：`%APPDATA%/lx-music-desktop`\\n   - Linux：`$XDG_CONFIG_HOME/lx-music-desktop` 或 `~/.config/lx-music-desktop`\\n   - macOS：`~/Library/Application Support/lx-music-desktop`\\n\\n2. 进入`LxDatas`目录，退出LX，删除`lx.data.db`文件，再启动软件即可\\n\\n若以上操作仍然不行，可以加交流群或者在GitHub开issue反馈\\n\\n优化\\n- 单次执行所有sql语句，尝试解决某些情况下某些表没有成功创建的问题\"},{\"version\":\"2.0.0\",\"desc\":\"不兼容性变更说明\\n- 数据迁移，升级此版本时，会使用旧版本的我的列表、下载设置、快捷键设置、自定义源等数据会自动迁移到新的数据格式版本，旧的数据仍然会保留，但下载列表的数据不做迁移\\n- 备份文件，v2.0.0及以后版本导出的列表、配置不支持导入v2.0.0之前版本，但v2.0.0之前版本导出的列表、配置支持导入v2.0.0以及以后版本（移动端需v0.15.0起才支持导入PC端v2生成的备份数据）\\n- 同步功能，该功能不支持与移动端v1.0.0之前版本的使用，需等待后面的新版移动端，目前移动端v1的开发工作已在进行中\\n\\n新增\\n- 新增自定义主题功能\\n- 新增歌单搜索功能\\n- 新增将本地歌曲添加到我的列表的支持，此功能可以在列表的右击菜单中使用（本地歌曲的歌词优先尝试读取相同路径下的同名歌词文件，若文件不存在则尝试读取歌曲文件内的歌词，若还是找不到歌词则尝试利用换源功能获取在线歌词，歌曲封面则是尝试读取歌曲文件内的封面，若不存在则利用换源功能获取在线封面）\\n- 启动软件时自动回到上次的界面，例如上次退出软件时在我的列表，下次启动软件时会自动进入我的列表\\n- 新增启动软件时自动播放音乐设置，默认关闭，可去设置-播放设置开启\\n- 新增“蛋雅深藍”、“近墨者黑”皮肤\\n- 新增下载歌词时是否同时下载歌词翻译、罗马音设置，默认关闭，可以去设置-下载设置开启（#344）\\n- 新增下载时，若目录存在同名的文件时是否跳过下载此任务的设置（默认跳过，可以去设置-下载设置更改）\\n- 新增界面字体大小设置\\n- 桌面歌词新增竖排歌词显示功能（#971）\\n- 桌面歌词新增歌词对齐方式、是否不允许歌词换行、歌词颜色、滚动对齐方式、歌词间距设置\\n- 桌面歌词新增歌曲频谱显示（得益于主窗口与桌面歌词进程通信的改进，可以将此功能以CPU使用率“相对较低”的方式带到桌面歌词中）\\n- 桌面歌词新增在任务栏显示歌词进程设置（此设置用于在录屏软件无法捕获歌词窗口时的变通解决方法）（#1063）\\n- 添加kg源罗马音歌词的支持（感谢@helloplhm-qwq）\\n- 支持打开波点音乐歌单（需在酷我源打开）\\n- 新增设置-基本设置-播放栏进度条样式设置（此版本默认使用迷你进度条样式，对于某些不喜欢该样式的人可以将其换成其他样式）\\n- 添加kg源评论图片展示（感谢@helloplhm-qwq）\\n\\n优化（界面/交互/功能）\\n- 调整软件界面及配色，使其更加清爽\\n- 处于单曲循环、顺序播放、禁用切歌模式时，手动切歌将会按列表循环模式的逻辑处理切歌（#864）\\n- 歌单右键菜单的“重复歌曲”扫描功能现在会将歌曲名字内的括号内容移除再对比，这可以有效找出歌曲的变体，例如：`突然的自我`、`突然的自我(Live)`、`突然的自我（女生版）`、`突然的自我(DJ版)`等都会被找出来（#987）\\n- 允许更小的桌面歌词窗口高度，可以取消“不允许拖动到主屏幕之外”设置后，再启用“不允许歌词换行”、“置顶歌词”与“自动刷新置顶”等设置，把它拖动到任务栏上，当做任务栏歌词使用（具体可以按你想要的显示方式使用这些设置组合去调）\\n\\n优化（程序）\\n- 优化程序启动性能，优化与程序交互的流畅度\\n- 重构整个程序，重新梳理了程序逻辑，使其更容易扩展及维护，将大部分代码从JavaScript迁移到TypeScript\\n- 重写配置管理、列表管理功能，列表、歌词数据从json文件迁移到sqlite3存储，这应该能解决因为意外的字符编码导致的数据文件损坏问题\\n\\n变更\\n- 列表右侧的操作按钮栏默认不再显示，歌曲的操作可以使用右键菜单代替，若想恢复它们的显示，可以去设置-列表设置-启用操作按钮栏开启\\n- 窗口大小设置时不再自动调整字体大小，想要调整字体大小可以使用新增的字体大小设置调整\\n\\n修复\\n- 修复Linux、macOS下若程序路径存在百分号时会导致软件无法启动的问题（#963）\\n- 支持单行多时间标签歌词解析，修复某些歌词会出现时间标签的问题\\n\\n移除\\n- 移除“信口雌黄”皮肤（由于该皮肤的配色有点刺眼），若你正在使用该皮肤，可以使用自定义主题功能恢复它\\n- 移除Linux deb x86包构建，Electron/Chromium已不再支持 32-bit Linux（electron/electron#34787）\\n- 移除桌面歌词主题设置，改用桌面歌词字体颜色设置功能代替\\n\\n其他\\n- 更新Electron到v19.1.9\"},{\"version\":\"1.22.3\",\"desc\":\"修复\\n- 修复因音源的域名到期导致的音源失效的问题\"},{\"version\":\"1.22.2\",\"desc\":\"优化\\n- 为tx、kw源添加 Flac 24bit 音质显示，注：由于之前没有记录此音质，所以之前收藏的歌曲信息中不包含它\\n\\n修复\\n- 修复无法批量排序歌曲的问题\\n- 修复某些缺失的繁体中文翻译\\n- 修复企鹅音乐搜索失效的问题\\n\\n其他\\n- 降级electron到v15.5.7\"},{\"version\":\"1.22.1\",\"desc\":\"优化\\n- 歌单列表添加歌单内歌曲数量显示，注：目前只有kw、mg、wy、tx（部分）源支持显示\\n\\n修复\\n- 修复处于不支持的源时，歌单、排行榜的右键下载菜单没有禁用的问题\\n- 修复若桌面歌词窗口与主窗口重叠时，鼠标划过重叠区域鼠标会闪烁的问题，注：此修复只对未启用“鼠标移入歌词区域时降低歌词透明度”时有效\\n- 修复tx源搜索失效的问题\\n\\n其他\\n- 升级Electron到 v17.4.10\"},{\"version\":\"1.22.0\",\"desc\":\"新增\\n- 新增设置-以全屏模式启动设置\\n- 新增设置-桌面歌词设置-鼠标移入歌词区域时降低歌词透明度（#883），默认关闭，此设置不支持linux，注：此功能存在兼容性问题，若鼠标移出后无法恢复到正常透明度，可尝试再移入移出即可恢复\\n\\n优化\\n- 添加歌曲到“我的列表”时，若按住`ctrl`键（Mac对应`Command`），则不会自动关闭添加窗口，这对想要将同一首（一批）歌曲添加到多个列表时会很有用\\n- 支持mg源逐字歌词的播放，感谢 @mozbugbox 提供的帮助\\n- 添加歌曲列表更新操作的二次确认\\n- 添加导入文件错误时的指引提示\\n\\n修复\\n- 修复若配置了`http_proxy`环境变量时，会意外使用此代理配置的问题\\n- 修复多选后切换列表后不会清空多选内容的问题\\n- 修复设置快捷键时的处理逻辑问题\\n- 修复在新建歌单输入框、歌单内歌曲搜索输入框会意外触发设置的全局快捷键的问题（#879）\\n\\n文档\\n桌面版文档已迁移到：<https://lyswhut.github.io/lx-music-doc/desktop>\\n\\n其他\\n- 更新 Electron 到 v17.4.7\"},{\"version\":\"1.21.0\",\"desc\":\"新增\\n- 新增设置-播放设置-显示歌词罗马音，默认关闭，注：目前只有网易源能获取到罗马音歌词（得益于 Binaryify/NeteaseCloudMusicApi/pull/1523），如果你知道其他源的歌词罗马音获取方式，欢迎PR或开issue交流！\\n\\n优化\\n- 同时删除一首歌以上时将需要二次确认删除\\n- 禁用透明窗口时右侧不再偏移5px距离（在win7、Ubuntu等系统上测试发现不偏移也不影响滚动条的拖动了）\\n- 删除未下载完成的任务时，只同时尝试删除已有下载进度的本地文件\\n- 在全屏状态下使用`Esc`键可以退出全屏（#827）\\n\\n修复\\n- 修复某些情况下歌曲播放出错时不会自动切歌的问题\\n- 修复关闭“显示切换动画”设置后，在应用启动时该设置没有被应用的问题\\n- 修复原始歌词存在偏移时，歌词偏移设置的重置未按预期工作的问题\\n- 修复长度大于一行的歌词在使用歌词调整播放进度时的时间不准问题\\n- 修复潜在歌单更新失败的问题\\n\\n文档\\n- 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\\n- 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\"},{\"version\":\"1.20.0\",\"desc\":\"特别说明：Scheme URL其实是支持Linux系统的，但好像需要deb之类的安装包创建出`.desktop`文件才行。\\n\\n新增\\n- 新增播放详情页歌词右键菜单，原来设置-播放详情页设置的字体重置已迁移到此菜单内\\n- 新增歌词偏移设置，可以在播放详情页歌词右键菜单中使用\\n- 新增设置-播放设置-播放错误时自动切换歌曲设置，默认开启（原来的行为），若你不想在遇到音频加载失败、url获取失败等错误时自动切歌可以关闭此设置\\n- 新增设置-桌面歌词设置-自动刷新歌词置顶（当歌词置顶后仍被某些程序遮挡时可尝试启用此设置）\\n- 新增列表更新管理，可以在鼠标移入“我的列表”标题时出现的按钮中进入，这可以用来设置启动软件时需要自动从原平台更新的列表\\n\\n优化\\n- 优化播放详情页背景显示，现在有背景图片的主题可以在播放详情页显示它的图片了\\n- 播放详情页在全屏状态下仍会显示退出播放详情页按钮，同时在其旁边添加退出全屏按钮\\n- 播放详情页在全屏状态下鼠标在空白处静止不动3秒后自动将其隐藏\\n\\n修复\\n- 修复Linux无法全屏的问题\\n- 修复播放下载列表的歌曲时，使用Windows任务栏缩略图工具栏控制按钮的收藏按钮收藏歌曲时的异常问题\\n- 修复启用搜索历史但不启用热门搜索时，搜索历史不显示的问题\\n- 修复窗口尺寸设置对应的字体大小在启动后不生效的问题\\n- 修复wy源搜索某些歌曲时第一页之后的歌曲无法加载的问题\\n- 修复使用Scheme URL搜索歌曲时，不会自动关闭播放详情页（若处于打开状态）的问题\\n- 修复换源失败时的处理问题\\n- 修复启用代理时，https请求可能被挂起或被转为http的问题\\n- 修复正在下载的歌曲暂停任务后，再开始会导致程序卡死的问题\\n\\n变更\\n- 播放详情页的任意地方右键双击隐藏详情页的行为，“任意区域”改为在“非歌词区域”\\n\\n移除\\n- 移除设置-播放详情页设置-歌词字体重置，此设置项已迁移到播放详情页的歌词菜单中\\n- 移除播放详情页使用+-快捷键调整字体大小的功能，改用歌词右键菜单的字体大小调整功能\"},{\"version\":\"1.19.0\",\"desc\":\"新增\\n- 新增对播放详情页歌词大小、是否缩放、对齐方式的设置，可以去设置-播放详情页设置查看\\n- 新增播放详情页通过歌词调整播放进度，默认关闭，需要到设置-播放详情页设置开启，开启后在播放详情页拖动歌词时将会出现跳转当前行歌词播放的按钮\\n- 新增全屏状态，按F11可以进入、退出全屏状态，由于全屏时会隐藏控制栏按钮，所以需要使用鼠标右键双击（详情页的任意地方都可以）来关闭播放详情页\\n- 新增动态主题“道法自然”，你可以预先设置一个亮色主题及暗色主题，此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。注：鼠标 右击 此主题项即可打开亮、暗色主题设置窗口。\\n- 新增对kw源卡拉OK歌词的支持\\n\\n优化\\n- 优化Windows任务栏缩略图工具栏控制按钮在浅色任务栏下的显示效果\\n- 添加音频可视化与音频输出设备冲突的提示\\n- 优化歌词的播放偏移\\n- 优化托盘菜单操作（#686）\\n- 优化播放下载列表时的切歌性能\\n\\n修复\\n- 修复“当前的声音输出设备被改变时暂停播放歌曲”设置无效的问题\\n- 修复桌面歌词没有处理停止播放状态的问题\\n- 修复AppImage包无法运行的问题\\n- 修复Windows任务栏缩略图工具栏控制按钮的歌曲收藏按钮状态更新问题\\n- 修复使用链接导入的歌单无法在我的列表打开原歌单详情页的问题\\n- 修复播放下载列表的歌曲时增删下载任务导致正在的歌曲序号改变时，不会更新到新增序号的问题\\n\\n文档\\n添加LX中定义的快捷操作汇总说明到常见问题中，这是目前可用的鼠标、键盘快捷操作，它们都可以在更新日志中找到\\n\\n- 鼠标右击播放栏的歌曲图片封面可以定位当前播放的歌曲\\n- 鼠标右击播放栏进度条上的LRC按钮可以锁定/解锁桌面歌词\\n- 歌曲搜索框、歌单链接输入框内鼠标右击可以将当前剪贴板上的文字粘贴到输入框内\\n- 鼠标右击搜索界面中的单条搜索历史可以将其移除\\n- 歌曲列表内的文字在选中后，鼠标右击可以复制已选中的文字，此功能只对搜索、歌单、排行榜、我的列表中的列表有效\\n- 鼠标在播放详情页内右键双击可以关闭播放详情页\\n- 鼠标左击播放栏上的歌曲名字可以将它复制\\n- 鼠标右击“道法自然（英文Auto）”主题可以打开亮、暗主题设置窗口\\n- 歌曲搜索框的候选内容可以用键盘上下方向键选择，按回车键搜索已选内容\\n- 在歌单详情页按退格键可以返回歌单列表\\n- 歌曲列表中可以使用Ctrl、Shift键进行多选，这类似Windows下的文件选择，详情看常见问题列表多选部分\\n- 在我的列表内可以使用Ctrl+f键打开搜索框进行列表内歌曲搜索，搜索框按Esc键可以关闭搜索框，搜索框内按上下方向键可以选择歌曲，按回车键跳转到已选歌曲，按Ctrl+回车可以跳转并播放已选歌曲\\n- 在我的列表按住Ctrl键可以进入列表拖动模式，此时可以用鼠标拖动列表调整列表的位置\\n- 编辑列表名时按Esc键可以取消编辑\\n- 按F11可以进入、退出全屏状态\"},{\"version\":\"1.18.0\",\"desc\":\"新增\\n- 新增“双击列表里的歌曲时自动切换到当前列表播放”设置，此功能仅对歌单、排行榜有效，默认关闭\\n- 新增打开收藏的在线列表的对应平台详情页功能，可以在我的列表-列表右键菜单中使用\\n- 新增定时暂停播放功能，由于此功能大多数人可能不常用，所以将其放在设置-基本设置中\\n- 新增任务栏缩略图工具栏控制按钮（此功能仅在Windows平台可用），按钮分别为收藏/取消收藏（将歌曲添加到“我的收藏”列表）、上一曲、播放/暂停、下一曲\\n- 新增设置-基本设置-软件字体设置，此设置可用于设置主界面的字体（已知的问题：Windows 7 下可能会出现字体列表为空的情况，这是当前系统的 Powershell 版本小于5.1导致的，请自行尝试看常见解决）\\n- 新增Scheme URL对音乐搜索的调用支持，详情看常见问题-Scheme URL支持\\n- 新增Scheme URL以url传参的方式调用，详情看常见问题-Scheme URL支持\\n- 自定义源新增更新弹窗方法，同时自定义源管理新增是否允许源显示更新弹窗设置（出于防止滥用考虑），当源作者想要通知用户源已更新时，可以调用此方法弹窗告诉用户，调用说明看常见问题-自定义源部分\\n\\n优化\\n- 过滤tx源某些不支持播放的歌曲，解决播放此类内容会导致意外的问题\\n- 把歌曲的热门评论与最新评论拆分成两个列表显示\\n\\n修复\\n- 修复排行榜名字右击菜单的播放功能在播放非激活的列表时的列表获取问题\\n- 修复修改列表名时无法使用`Ctrl`键的问题\\n- 修复wy源某些歌曲获取歌词翻译的问题处理\\n- 修复下载功能的歌词换源时会进入死循环的问题\\n- 修复某些歌曲无法下载的问题\\n- 修复windows平台下软件目录存在`portable`文件夹时，仍会创建`C:\\\\Users\\\\<user>\\\\AppData\\\\Roaming\\\\lx-music-desktop\\\\Dictionaries\\\\en-US-9-0.bdic`文件的问题，现在不会再创建文件，但仍会创建空目录（Electron的问题，目前暂无解决方法）\\n- 修复播放器的停止逻辑问题\\n\\n其他\\n- 更新electron到v13.6.9\"},{\"version\":\"1.17.1\",\"desc\":\"优化\\n- 优化kw源英文与翻译歌词的匹配\\n\\n修复\\n- 修复快捷键与默认按键行为冲突的问题，现在若将某些有默认行为的按键（如在列表中上、下箭头、Home、End等键可以使列表滚动）设置为快捷键时，将禁用其默认行为\\n- 修复列表的聚焦问题，现在在列表中使用上、下箭头、空格等键滚动列表时不会导致滚动到一定距离后丢失焦点的问题\\n\\n其他\\n- 更新electron到v13.6.8\"},{\"version\":\"1.17.0\",\"desc\":\"新增\\n- 新增“便携”功能，在Windows平台下，若程序目录下存在 portable 目录，则自动使用此目录作为数据存储目录\\n- 新增 Scheme URL 支持，同时发布lx-music-script项目配合使用（一个油猴脚本，可以在浏览器中的官方平台网页直接调用LX Music），Scheme URL的调用说明看Readme.md文档的Scheme URL支持部分\\n- 新增启动参数`-proxy-server`与`-proxy-bypass-list`，详细介绍看Readme.md文档的启动参数部分\\n- 新增桌面歌词是否延迟滚动设置，默认开启，若你不想要桌面歌词延迟滚动可以去设置-桌面歌词设置关掉\\n\\n优化\\n- 为可视化音频的频谱整体添加频谱均值加成，使频谱显示更有节奏感\\n- 优化程序初始化逻辑，修复无网络的情况下的初始化问题\\n- 我的列表-列表名的右击菜单更新已收藏的在线列表时，将始终重新加载，不再使用缓存，解决在原平台更新歌单后，在LX点击更新可能看到的还是在原平台更新前的歌单的问题\\n\\n修复\\n- 修复代理不生效的问题\\n- 修复`openDevTools`选项无效的问题\\n- 修复播放状态的提示问题\\n- 修复tx源无搜索结果的问题\\n\\n其他\\n- 更新 Electron 到 v13.6.7\"},{\"version\":\"1.16.0\",\"desc\":\"这算是一个大版本，对主窗口部分的代码逻辑做了较大改动，但由于界面的改动不大，所以没有更新大版本号。\\n虽然经过一个月的测试与问题修复，但可能仍然存在未发现的问题，若你发现某些界面异常、某些行为与旧版本存在差异等问题，欢迎反馈！\\n另外祝大家元旦快乐~！\\n\\n新增\\n- 播放详情页新增音量控制条\\n- 播放详情页新增桌面歌词切换按钮\\n- 新增将我的列表保存为TXT、CSV格式，可以去设置-备份与恢复中使用（注意：此类格式的备份目前不支持恢复到LX Music中）\\n- 新增根据歌曲名、歌手名等字段对列表自动排序的功能，可以在我的列表右击列表名弹出的菜单中使用\\n- 新增将播放与下载的歌词转换为繁体中文选项，默认关闭，可在设置-播放设置中开启\\n- 现在已允许进入临时播放列表，即：使用歌单详情页、排行榜名称右键菜单的“播放”按钮播放歌曲时，可右击播放封面进入此临时列表\\n- 播放详情页新增音频可视化功能（实验性）\\n- 我的列表新增拖动调整位置功能，按住Ctrl键（Mac上对应Command键）的时候将进入“拖动模式”，此时可以拖动列表的位置来调整顺序\\n\\n优化\\n- 优化列表性能，软件整体性能\\n- 调整Mac平台下的图标大小\\n- 同步功能添加对列表顺序调整的控制，确保手动调整位置后的列表与不同的电脑同步时，列表位置不会被还原\\n- 优化歌单详情、排行榜名右键的播放按钮的播放机制，现在不用等待整个列表（多页时）加载完成才能播放了\\n- 为播放详情页、桌面歌词添加延迟滚动，播放详情页略微减小已激活歌词的缩放大小及桌面歌词翻译大小\\n- 修改右边控制按钮为windows风格\\n- 更新了新年皮肤的背景与配色，欢迎体验~\\n\\n修复\\n- 修复kw源某些歌曲的歌词提取异常的问题\\n\\n变更\\n- 现在使用繁体中文语言时将不再自动转换歌词，转换行为将由上面新增的转换开关控制\\n\\n移除\\n- 移除我的列表右键菜单的“上移、下移列表”功能，调整改用新增的拖动功能去调整位置\\n\\n其他\\n- 升级vue到 3.x\"},{\"version\":\"1.15.3\",\"desc\":\"修复\\n- 修复设置-控制按钮位置选项与下载歌词编码格式选项命名冲突导致选项显示异常的问题\\n- 修复播放下载列表时存在失效的歌曲会导致切歌不准确的问题\\n- 修复潜在的音乐加载超时不会切歌的问题\\n- 修复因kw源歌词接口停用导致该源歌词获取失败的问题\"},{\"version\":\"1.15.2\",\"desc\":\"其他\\n- 降级electron到v13.4.0（这修复了windows 7下播放歌曲时软件会崩溃的问题）\"},{\"version\":\"1.15.1\",\"desc\":\"优化\\n- 优化我的列表、下载列表等列表的滚动流畅度\\n- 优化下载功能的批量添加、删除、暂停任务时的流畅度，现在进行这些操作应该不会再觉得卡顿了\\n- 支持启动软件时恢复播放下载列表里的歌曲\\n- 添加媒体播放进度条的信息设置\\n\\n修复\\n- 修复某些情况下获取URL失败时会意外切歌的问题\\n- 修复了某些情况下会列表同步失败，导致连接断开无限重连或一直卡在 `syncing...` 的问题\\n- 修复列表数据过大导致同步失败的问题\\n\\n其他\\n- 更新electron到v15.3.1（这修复了媒体控制失效的问题）\"},{\"version\":\"1.15.0\",\"desc\":\"新增\\n- 添加黑色托盘图标\\n- 自定义源新增`version`字段，新增`utils.buffer.bufToString`方法\\n\\n优化\\n- 大幅优化我的列表、下载、歌单、排行榜列表性能，现在即使同一列表内的歌曲很多时也不会卡顿了\\n- 优化列表同步代码逻辑\\n- 优化开关评论时的动画性能\\n- 优化进入、离开播放详情页的性能\\n- 兼容桌面歌词以触摸的方式移动、调整大小\\n- 调整图标尺寸\\n\\n修复\\n- 修复kg源的歌单链接无法打开的问题\\n- 修复同一首歌的URL、歌词等同时需要换源时的处理问题\\n\\n其他\\n- 更新 Electron 到 v15.3.0\"},{\"version\":\"1.14.1\",\"desc\":\"修复\\n- 修复我的列表搜索无法搜索小括号、中括号等字符的问题\\n- 修复v1.14.0出现的备份与恢复功能备份的数据无法恢复的问题，同时兼容使用v1.14.0导出的存在问题的数据\"},{\"version\":\"1.14.0\",\"desc\":\"新增\\n- 新增歌词简体中文转繁体中文，当软件语言被设置为繁体中文后，播放歌曲的歌词也将自动转成繁体中文显示\\n- 新增单个列表导入/导出功能，可以方便分享歌曲列表，可在右击“我的列表”里的列表名后弹出的菜单中使用\\n- 新增删除列表前的确认弹窗，防止误删列表\\n- 新增歌词文本选择复制功能，可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式，选择完成后可鼠标右击或者使用系统快捷键复制\\n- 新增重复歌曲列表，可以方便移除我的列表中的重复歌曲，此列表会列出目标列表里歌曲名相同的歌曲，可在右击“我的列表”里的列表名后弹出的菜单中使用\\n\\n修复\\n- 修复mg排行榜无法加载的问题\\n- 修复点击播放详情页的进度条跳进度时会出现偏移的问题\\n- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题\\n- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题\\n- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题\"},{\"version\":\"1.13.0\",\"desc\":\"如果你喜欢并经常使用洛雪音乐，并想要第一时间尝鲜洛雪的新功能，可以加入测试企鹅群768786588，\\n注意：测试版的功可能会不稳定，打算潜水的勿加。\\n\\n新增\\n- 歌曲搜索框新增清理按钮，点击此按钮可以清理搜索框并返回初始搜索界面\\n- 新增“下载的歌词文件编码格式”设置，默认下载的歌词编码仍是`UTF-8`，对于某些在设备(如车机)上出现歌词中文乱码的用户可以尝试选择以`GBK`编码格式保存歌词文件\\n- 新增设置-桌面歌词-歌词字体设置，此设置可用于设置桌面歌词的字体（已知的问题：Windows 7 下可能会出现字体列表为空的情况，这是当前系统的 Powershell 版本小于5.1导致的，请自行**尝试**看常见解决）\\n\\n优化\\n- 支持网易源“我喜欢”歌单以注入token的方式打开。由于网易源的“我喜欢”歌单需要登录才能打开（若你看不懂后半句就去阅读 常见问题-无法打开外部歌单），现若想要打开此类歌单，需要在歌单链接后面拼上 `###` 再加上有效的token，拼接格式：`[id|url]###token`，例子（最后面的xxxxxx替换成你的token）：`https://music.163.com/#/playlist?id=123456&userid=123456###xxxxxx`\\n- 软件内快捷键的最小化触发时，如果已启用托盘，则隐藏程序，否则最小化程序\\n\\n修复\\n- 修复某些情况下同步功能会导致切歌混乱的问题\\n- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题\"},{\"version\":\"1.12.2\",\"desc\":\"修复\\n- 修复播放下载列表的歌曲时切歌的问题\\n- 修复播放下载列表的歌曲时歌词无法显示的问题\\n- 修复下载列表稍后播放功能无效的问题\\n- 修复同步服务器启动失败时，关闭同步服务不会清空失败信息的问题\"},{\"version\":\"1.12.1\",\"desc\":\"修复\\n- 修复随机播放下无法切歌的问题\"},{\"version\":\"1.12.0\",\"desc\":\"新增\\n- 新增局域网同步功能（实验性，首次使用前建议先备份一次列表），此功能需要配合PC端使用，移动端与PC端处在同一个局域网（路由器的网络）下时，可以多端实时同步歌曲列表，使用问题请看\\\"常见问题\\\"。\\n\\n优化\\n- 添加播放器对系统媒体控制与显示的兼容处理，现在在windows下的锁屏界面可以正确显示当前播放的音乐信息及切换歌曲了\\n\\n修复\\n- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注：现在可以加载1000+首歌曲的歌单，但出于未知原因会导致部分歌曲无法加载（可能是无版权导致的），目前酷狗码仍然最多只能加载500首歌\\n- 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题\"},{\"version\":\"1.11.0\",\"desc\":\"新增\\n- 添加 win arm64 架构的安装包构建\\n- 新增“添加歌曲到列表时的位置”设置，可选项为列表的“顶部”与“底部”\\n\\n优化\\n- 优化网络请求，尝试去解决无法连接服务器的问题\\n- 优化mg源打开歌单的链接兼容\\n\\n修复\\n- 修复mg源搜索失效的问题\\n\\n移除\\n- 因wy源的歌单列表已没有“最新”排序的选项，所以现跟随移除wy源歌单列表按“最新”排序的按钮\\n\\n变更\\n- 添加歌曲到列表时从原来的底部改为顶部，若你想要将你的列表歌曲顺序反转以适应这一变更，可先按住`shift`键的情况下点击列表的最后一首歌，然后再点击列表的第一首歌，完成倒序选中，最后随便右击列表的任意一首歌，在弹出的菜单中选择调整顺序，在弹出框输入1后确定即可反转列表。\\n若你想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项。\\n\\n其他\\n- 更新electron到v13.1.7\"},{\"version\":\"1.10.2\",\"desc\":\"修复\\n- 修复企鹅音乐搜索歌曲没有结果的问题\"},{\"version\":\"1.10.1\",\"desc\":\"修复\\n- 修复企鹅音乐搜索歌曲没有结果的问题\\n- 修复播放在空的歌单列表点击播放全部时报错的问题\"},{\"version\":\"1.10.0\",\"desc\":\"lx music移动端已经发布了，使用习惯仍跟桌面版一样，不过功能、界面仍比较简单，有兴趣的可以去体检一下，项目地址：\\nhttps://github.com/lyswhut/lx-music-mobile#readme\\n\\n新增\\n- 排行榜界面添加播放、收藏整个排行榜功能，可以右击排行榜名字后，在弹出的右键菜单中使用。注：收藏、播放存在分页的排行榜时需等待操作完成后才能切换排行榜，不然会导致操作中断。\\n- 新增Mac arm64位dmg包的构建\\n\\n修复\\n- 修复全局快捷键对桌面歌词无效的问题\\n- 修复快捷键设置框内的提示问题\\n- 修复在当前正常播放的列表中使用稍后播放功能时，播放完后稍后播放的歌曲后不会恢复原来播放位置播放的问题\\n- 修复kw部分歌单无法打开的问题\\n- 修复wy源的歌曲音质匹配问题\\n- 修复mg源歌单标签、排行榜歌曲列表无法加载的问题\\n- 修复了一个歌曲下载失败时不会跳过任务的问题\\n\\n其他\\n- 更新 Electron 到 12.0.8\"},{\"version\":\"1.9.0\",\"desc\":\"新增\\n- 新增启动参数`-dhmkh`，此参数将禁用Chromium的Hardware Media Key Handling特性，用于解决漫步者部分型号耳机与本程序冲突导致耳机意外关机的问题\\n- 新增Windows arm64位免安装版的构建\\n- 新增黑色皮肤“黑灯瞎火”，有关于皮肤配色的建议欢迎反馈\\n- 新增自动换源下载功能，默认关闭，当无法从歌曲的原始源下载时，将尝试切换到其他源下载，注：此功能不100%保证换源后的歌曲版本与原版一致\\n\\n优化\\n- 程序启动时对数据文件做读取校验，数据出现损坏时自动备份损坏的数据，若出现数据读取错误的弹窗并出现我的列表丢失时可到GitHub或加群反馈\\n- 当设置-代理启用，但主机地址为空的时，将不再使用代理配置进行网络连接，并且在离开设置界面时自动禁用代理\\n- 优化歌曲自动换源匹配\\n- 分离歌词与歌曲列表信息的保存，以减小列表列表文件损坏的几率\\n- 兼容打开咪咕移动端分享的歌单链接，添加打开歌单的信息显示\\n\\n修复\\n- 修复备份与恢复功能在恢复数据时某些设置不立即生效的问题\\n- 修正设置页“搜索设置”部分内容的缩进显示问题\\n- 修复正在播放“稍后播放”的歌曲时，对“稍后播放”前播放的列表进行添加、删除操作会导致切歌的问题\"},{\"version\":\"1.8.2\",\"desc\":\"### 修复\\r\\n\\r\\n- 修复歌曲ID存储变更导致酷狗图片获取失败的问题\\n- 修复收藏的在线列表id迁移保存出错的问题\"},{\"version\":\"1.8.1\",\"desc\":\"修复\\n- 修复歌词翻译的主题颜色适配问题\"},{\"version\":\"1.8.0\",\"desc\":\"新增\\n- 新增设置-其他-列表缓存信息清理功能，注：此功能一般情况下不要使用\\n- 新增启动参数`-play`，可以在启动软件时播放指定歌单，使用方法看Readme.md的\\\"启动参数\\\"部分\\n- 新增逐字歌词播放，默认开启，可到设置界面关闭，注：本功能目前仅对酷狗源的歌曲有效\\n- 新增自定义源功能，源编写规则可以去常见问题查看\\n\\n优化\\n- 允许播放除了搜索列表以外的所有歌曲，即原来没有播放按钮或者灰色的歌曲都可以去尝试点击播放。注：该功能的原理是尝试自动切换到其他源播放，所以不一定会播放成功，特别是对于那些独家的资源\\n- 优化单首歌曲的“添加到列表”弹窗歌曲列表状态的显示；现在在收藏单首歌曲时，若列表存在本歌曲则列表名字将变成灰色不可点击状态。总的来说，在添加单首歌曲时若列表名是灰色，则证明当前歌曲已在那个列表中\\n- 将歌词翻译放到原文的下方，同时新增当前播放翻译的高亮功能\\n\\n移除\\n- 移除虾米源。注：虽然已移除该源，但仍可尝试去播放之前添加的歌曲，虽然不一定会成功\\n\\n修复\\n- 修复音乐搜索列表的稍后播放功能无效的问题\\n- 修复搜索列表双击不支持播放的源时会导致切歌的问题\\n- 修复歌单列表加载失败时无法进入歌单打开界面的问题\\n- 修复mg源歌单列表无法加载的问题\\n- 修复kg跳转到官方歌曲详情页的歌曲无法播放的问题\\n- 修复我的列表的歌曲添加到其他列表时不排除当前列表的问题\\n- 修复在下载列表右击未下载完成的歌曲弹出的右击菜单中没有开始下载选项的问题\\n\\n变更\\n- 歌词翻译显示功能修改为默认关闭，注：此变更仅影响首次安装软件的用户\\n\\n其他\\n- 更新electron到v9.4.4\"},{\"version\":\"1.7.1\",\"desc\":\"修复\\n- 修复非透明模式下右侧滚动条无法拖动的问题\\n- 修复MAC下xm音乐滑块验证问题\"},{\"version\":\"1.7.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>搜索界面新增搜索状态的提示</li>\\n<li>新增“稍后播放”功能，可在歌曲列表右键菜单使用</li>\\n<li>新增“记住播放进度”功能的控制，该功能默认不再开启，可到播放设置-记住播放进度开启</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化播放歌曲换源匹配</li>\\n<li>优化设置界面设置项的展示</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复快速切换歌曲时, 会出现播放的歌曲和界面展示的歌曲不一致的问题</li>\\n<li>修复了一个由版本更新日志显示导致的潜在远程代码执行攻击漏洞，该漏洞影响v1.6.1及之前的所有版本，请务必更新到最新版本</li>\\n<li>修复xm搜索源验证问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新electron到9.4.2</li>\\n</ul>\\n\"},{\"version\":\"1.6.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>改进自动换源时的歌曲匹配</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复某些情况下自动换源的时间过长时会终止换源自动切歌的问题</li>\\n<li>修复自动换源导致的搜索列表每页变成10条数据的问题</li>\\n<li>降级electron到9.3.3修复部分系统没有声音的问题</li>\\n</ul>\\n\"},{\"version\":\"1.6.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>我的列表右键菜单新增列表排序功能，可调整单曲、多选后的歌曲的顺序。注意：多选排序还将会按照选中歌曲时的顺序排序</li>\\n<li>添加鼠标提示的自动关闭功能，鼠标长时间（目前是10秒）不动时鼠标提示将会自动关闭</li>\\n<li>添加鼠标指向歌曲封面的提示（对于进度条左边的歌曲封面，你可能不知道的操作-&gt;右击在“我的列表”定位当前播放的歌曲）</li>\\n<li>隐藏播放详情页按钮添加快速隐藏详情页提示（你可能不知道的操作-&gt;在播放详情页内的任意非窗口可拖动区域右键双击可以快速隐藏详情页）</li>\\n<li>添加桌面歌词字体、透明度调整按钮微调提示（你可能不知道的操作-&gt;对于字体、透明度可右击微调）</li>\\n<li>我的列表右键菜单添加搜索当前歌曲功能</li>\\n<li>新增<code>-dha</code>参数，添加此启动参数将禁用硬件加速启动（Disable Hardware Acceleration），窗口显示有问题时可以尝试添加此参数启动，Linux系统的界面显示有问题时可尝试添加此参数启动，若不行可尝试添加<code>-dt</code>参数启动</li>\\n<li>新增播放自动换源功能~</li>\\n</ul>\\n<h3>变更</h3>\\n<ul>\\n<li><code>-nt</code>参数更名为<code>-dt</code>（Disable Transparent），目前原来的<code>-nt</code>参数仍然可用，但将在后续的版本中移除</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复恢复上次播放的歌曲时在随机播放模式下不把恢复播放的歌曲放入已播放队列的问题（该问题会导致随机模式下会导致未播放完整个列表前就会再次随机到该歌曲，以及无法通过上一曲切回该歌曲）</li>\\n<li>修复音乐嵌入的封面在 Mac 系统无法显示的问题</li>\\n<li>修复<code>-dt</code>（原来的<code>-nt</code>）启动参数不真正生效的问题</li>\\n</ul>\\n\"},{\"version\":\"1.5.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>直接从歌单详情收藏的列表新增同步功能。注意：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化软件启动时恢复上一次播放的歌曲进度功能</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复MAC平台上下载歌曲封面嵌入无法显示的问题</li>\\n<li>修复MAC平台首次运行软件最小化、关闭控制按钮默认在右边的问题</li>\\n<li>修复酷狗源的某些歌曲没有专辑字段导致的列表加载失败问题</li>\\n<li>修复某些酷狗源歌单链接无法打开的问题</li>\\n</ul>\\n\"},{\"version\":\"1.4.1\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复有歌词翻译与无歌词的音乐间切换会导致歌词翻译残留显示的问题</li>\\n<li>修复歌曲URL过期时，等待刷新URL的自动切换歌曲时间间隔太短的问题</li>\\n<li>修复某些电脑上的某些歌曲没有声音的问题（升级Electron9.3.4导致的，现降级到9.3.3）</li>\\n</ul>\\n\"},{\"version\":\"1.4.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>托盘菜单新增显示、隐藏主界面选项，为Linux、MAC版添加托盘菜单</li>\\n<li>新增播放进度信息保存</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>移除kg源的歌词文件开头的空白字符串</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复专辑图片无法嵌入的问题</li>\\n<li>修复播放状态栏切换“上一首”歌曲按钮提示错误的问题</li>\\n<li>修复移动单首歌曲时，如果目标列表存在该歌曲，会导致将源列表与目标列表里的目标歌曲移除</li>\\n<li>修复kg源歌曲信息带有单引号等特殊字符被转义的问题</li>\\n</ul>\\n\"},{\"version\":\"1.3.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>播放详情页新增歌曲评论加载显示（某些平台暂不支持显示子评论）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>修改播放详情页的歌曲图片的显示效果</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复小芸源音乐搜索结果最多只有20条搜索结果的问题</li>\\n</ul>\\n\"},{\"version\":\"1.2.2\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>降级 Electron 到 9.x.x 版本修复 Linux 版桌面歌词窗口变白的问题</li>\\n</ul>\\n\"},{\"version\":\"1.2.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>Linux版的软件界面默认使用圆角与阴影，顺便修复了桌面歌词窗口变白的问题，已在Ubuntu 18.10测试正常，若显示异常可尝试添加<code>-nt</code>参数启动</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复聚合搜索的分页问题</li>\\n<li>修复代理输入框输入的内容不生效的问题</li>\\n</ul>\\n\"},{\"version\":\"1.2.0\",\"desc\":\"<p>提前祝大家中秋&amp;国庆快乐~</p>\\n<h3>新增</h3>\\n<ul>\\n<li>播放控制栏开启/关闭桌面歌词按钮 新增右击按钮时锁定/解锁桌面歌词功能</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化我的列表滚动条位置的保存逻辑</li>\\n<li>更新设置-备份与恢复功能的描述</li>\\n<li>优化软件内鼠标悬停的提示界面</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复桌面歌词窗口不允许拖出桌面之外的位置计算偏移Bug</li>\\n<li>修复网易云KTV嗨榜无法加载的问题</li>\\n<li>修复初始化搜索历史列表功能</li>\\n<li>修复重启软件后试听列表与收藏列表无法恢复上次的滚动位置的问题</li>\\n<li>修复歌曲封面无法嵌入的Bug</li>\\n<li>修复酷狗歌词格式问题</li>\\n<li>修复关闭切换动画时从搜索候选列表点击内容无效的问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新 Electron 到 v10.1.3</li>\\n</ul>\\n\"},{\"version\":\"1.1.1\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复某些情况下桌面歌词不会播放的问题</li>\\n</ul>\\n\"},{\"version\":\"1.1.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>在歌单详情界面新增播放当前歌单按钮、收藏歌单按钮，注：播放歌单不会将歌曲添加到试听列表</li>\\n<li>新增<code>不允许将歌词窗口拖出主屏幕之外</code>的设置项，默认开启，在连接多个屏幕时想要拖动到其他屏幕时可关闭此设置</li>\\n<li>新增大部分平台的歌词翻译，感谢 @InoriHimea 提供的<a href=\\\"https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784\\\">krc解码算法</a></li>\\n<li>新增<code>显示歌词翻译</code>设置，默认开启，仅支持某些平台，注：无论该设置是否开启，嵌入或下载歌词时都不会带上翻译</li>\\n<li>新增<code>显示切换动画</code>设置，默认开启，关闭时将基本禁用软件内的所有切换动画</li>\\n<li>播放状态栏新增桌面歌词的开关、播放模式的切换、歌曲的收藏按钮，Thanks to @andylow for the <a href=\\\"https://github.com/lyswhut/lx-music-desktop/pull/309\\\">icon</a>!</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复使用全局快捷键还原窗口时，窗口没有获取焦点的问题</li>\\n<li>修复我的列表搜索对最后一个字符的匹配问题</li>\\n<li>修复窗口在<code>较小</code>模式下最小化/关闭按钮不居中的问题</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>桌面歌词当前播放行改为上下居中</li>\\n<li>为区分静音状态，静音时音量条会变淡，调整音量条时将会取消静音</li>\\n<li>优化随机播放机制，现在通过<code>下一曲</code>切换歌曲时，直到播放完整个列表之前将不会再随机到之前播放过的歌曲，并且通过<code>上一曲</code>可以正确播放上一首歌曲</li>\\n<li>当下载目录没有写入权限时将显示没有写入权限的提示</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>移除默认的全局声音媒体快捷键接管</li>\\n<li>移除对百度音乐的支持，因百度音乐原有的大部分API失效，而且该平台相对其他平台来说音乐太少了，可有可无，以后再看情况恢复</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新electron到 10.1.2</li>\\n</ul>\\n\"},{\"version\":\"1.0.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>对我的列表歌曲搜索结果进行相似度排序</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复在 Windows 系统下缩放比非100%时，拖动桌面歌词会自动加大桌面歌词窗口的问题</li>\\n</ul>\\n\"},{\"version\":\"1.0.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<code>rpm</code>、<code>pacman</code>包的构建（未测试可用性）</li>\\n<li>新增因系统音频设备列表改变导致的当前音频输出设备改变时是否暂停播放的设置，默认关闭</li>\\n<li>新增歌曲列表右击菜单</li>\\n<li>新增自定义列表，创建列表的按钮在表头<code>#</code>左侧，鼠标移上去才会显示；编辑列表名字时，按<code>ESC</code>键可快速取消编辑，按回车键或使输入框失去焦点即可保存列表名字，右击列表可编辑已创建的列表，“试听列表”与“我的收藏”两个列表固定不可编辑</li>\\n<li>改变排行榜布局，新增更多排行榜</li>\\n<li>新增我的列表右键菜单复制歌曲名选项</li>\\n<li>新增桌面歌词，默认关闭，可到设置或者托盘菜单开启（建议使用全局快捷键控制）；调整字体大小、透明度时，鼠标左击按钮正常调整，右击微调；<strong>Windows 7未开启Aero效果时桌面歌词会有问题</strong>，详情看常见问题解决；Linux版桌面歌词有问题，以后再尝试优化；</li>\\n<li>新增“清热板蓝”皮肤</li>\\n<li>新增软件最小化、关闭按钮位置设置，MAC版默认为左边，非MAC为右边，不想用默认的可到设置修改</li>\\n<li>新增快捷键设置，软件内快捷键默认开启，全局快捷键默认关闭（注：若想开启蓝牙耳机切歌需开启全局快捷键，当快捷键被中划线划掉时，表示当前快捷键被其他程序占用导致注册失败）</li>\\n<li>新增首次运行时自动根据当前系统使用的语言设置软件显示的语言</li>\\n<li>新增歌词区域的触摸板、鼠标滚轮等对歌词滚动的支持</li>\\n<li>为了方便支持正版资源，歌曲列表右击菜单新增跳转到当前歌曲源官方详情页菜单（注意：在本版本之前添加的虾米源歌曲无法跳转详情页，需要移除后重新搜索添加）</li>\\n<li>新增我的列表内歌曲搜索，在我的列表按<code>ctrl+f</code>将显示搜索框；鼠标滑过或键盘上下方向键选择搜索结果；鼠标点击或按回车键定位选中的歌曲；按<code>ctrl</code>键的情况下鼠标点击或按回车键确认定位歌曲时，将会在定位歌曲结束后播放该歌曲（搜索框激活的情况下按<code>esc</code>可快速清空搜索框/关闭搜索框）</li>\\n<li>新增托盘图标样式设置，可到设置-其他切换</li>\\n<li>新增开关下载功能控制，默认关闭，可到设置-下载设置开启</li>\\n<li>新增将歌词嵌入音频文件中，默认关闭，可到设置-下载设置开启</li>\\n<li>新增当列表文件损坏时对损坏文件的备份，若出现该情况可打开<code>%HOMEPATH%\\\\AppData\\\\Roaming\\\\lx-music-desktop</code>找到<code>playList.json.bak</code>尝试手动修复列表文件，列表文件以<code>JSON</code>格式存储</li>\\n<li>新增在歌单详情列表按退格（Backspace）键可快速返回歌单列表</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>改进歌曲切换时的歌词滚动效果</li>\\n<li>优化批量添加、删除播放列表的歌曲操作逻辑，大幅提升批量添加、删除列表歌曲的流畅度</li>\\n<li>改进歌单列表展示</li>\\n<li><strong>改进聚合搜索的搜索结果排序</strong>，修复当某些源搜索失败时导致其他源无法显示结果的问题，现在聚合搜索已达到最初的理想效果，为了使排序更精确，<strong>建议同时输入 歌曲名 歌手名 搜索</strong>（歌曲名在前歌手名在后），欢迎体验~！</li>\\n<li>压缩备份数据文件大小</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复按住<code>Ctrl</code>等键触发多选机制时不松开按键的情况下切换到其他窗口后再松开按键，这时切回软件不按按键都处在多选模式的问题</li>\\n<li>修复Linux版开启托盘无法退出的问题</li>\\n<li>修复某些情况下可能导致的音源输出问题</li>\\n<li>修复某些情况下无法开始下载任务的问题</li>\\n<li>修复 tab 组件边框溢出问题</li>\\n<li>修复错误更新试听列表外的歌曲时间的问题</li>\\n<li>修复网易音乐源歌单、排行榜歌曲列表加载显示的数量与实际不对的问题，同时支持加载大于1000首歌的歌单（歌曲大于1000首会分页），注意：目前软件一下子显示太多歌曲时会卡顿，不建议在同一列表内添加太多歌曲</li>\\n<li>修复歌曲图片链接没有扩展名的情况下无法嵌入图片的问题</li>\\n<li>修复无法检测最新版本时弹窗提示的显示</li>\\n<li>修复某些情况下从托盘还原窗口后无法操作的问题</li>\\n<li>修复Linux下无法<code>ctrl+a</code>全选的问题</li>\\n<li>修复主题背景图片覆盖不全的问题</li>\\n<li>修复聚合搜索音源标签的皮肤配色问题</li>\\n</ul>\\n<h3>更变</h3>\\n<ul>\\n<li>修改设置-列表-是否显示歌曲源的默认设置为选中（该变更不影响之前的设置）</li>\\n<li>移除浮动按钮，现在在多选完成后可鼠标右击随意一项在弹出的右键菜单中进行原来悬浮按钮的操作</li>\\n<li>为了避免出现误会，现在下载弹窗中不可用的音质将直接隐藏</li>\\n<li>更改初始设置的搜索设置为聚合搜索（该变更不影响之前的设置）</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新 Electron 到 9.1.1</li>\\n</ul>\\n\"},{\"version\":\"0.18.2\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复开启托盘时，可能导致无法自动更新的问题</li>\\n</ul>\\n\"},{\"version\":\"0.18.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>win下的托盘图标使用更大的图片</li>\\n<li>加长软件协议的强制停留时间</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复导入设置某些设置未立即生效的问题</li>\\n</ul>\\n\"},{\"version\":\"0.18.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增FLAC格式音乐标签信息写入与封面嵌入（因128k以外的音质已失效，目前该功能用不上了）</li>\\n<li>添加软件启动时是否自动聚焦搜索框的设置</li>\\n<li>新增托盘设置，默认关闭，可到设置开启，感谢 @LasyIsLazy 提交的PR</li>\\n<li>新增打开酷狗源用户歌单</li>\\n<li>新增使用协议</li>\\n<li>新增虾米音源</li>\\n<li>新增新皮肤“粉妆玉琢”、“青出于黑”，可去体验下~</li>\\n<li>新增“超大”、“巨大”窗口尺寸</li>\\n<li>新增播放详情页（退出详情页可点击右上角退出按钮或者在播放详情页任意地方<strong>鼠标快速右击两次</strong>）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>略微加深音量条底色</li>\\n<li>优化其他界面细节</li>\\n<li>优化英语翻译，感谢 @CPCer</li>\\n<li>优化程序的流畅度</li>\\n</ul>\\n<h3>更变</h3>\\n<ul>\\n<li>下载列表的歌曲下载、播放将随设置中的保存路径改变而改变，不再固定指向其初始位置</li>\\n<li>移除列表多选框，现在多选需要键盘配合，想要多选前需按下<code>Shift</code>或<code>Ctrl</code>键然后再鼠标点击想要选中的内容即可触发多选机制，其中<code>Shift</code>键用于连续选择，<code>Ctrl</code>键用于不连续选择，<code>Ctrl+a</code>用于快速全选。例子一：想要选中1-5项，则先按下<code>Shift</code>键后，鼠标点击第一项，再点击第五项即可完成选择；例子二：想要选中1项与第3项，则先按下<code>Ctrl</code>键后，鼠标点击第一项，再点击第三项即可完成选择；例子三：想要选中当前列表的全部内容，键盘先按下<code>Ctrl</code>键不放，然后按<code>a</code>键，即可完成选择。用<code>Shift</code>或<code>Ctrl</code>选择时，鼠标点击未选中的内容会将其选中，点击已选择的内容会将其取消选择，若想全部取消选择，在不按<code>Shift</code>或<code>Alt</code>键的情况下，随意点击列表里的一项内容即可全部取消选择。(P.S：<code>Ctrl</code>键对应Mac OS上的<code>Command</code>键)</li>\\n<li>现在进度条的封面图左击改为打开播放详情页，在列表定位歌曲改为右击</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复网易源某些歌曲提示没有可播放的音质的问题</li>\\n<li>修复下载管理刷新URL失败时不标记任务下载失败的问题</li>\\n<li>修复列表导出的文字描述，感谢 @CPCer</li>\\n<li>修复歌曲切换方式无法取消勾选的问题</li>\\n<li>修复打开歌单详情的情况下切到其他界面再切回来报错的问题</li>\\n<li>修正播放列表浮动按钮错误的文字提示</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>因128k以外的音质失效，So 禁止所有128k外的音质下载</li>\\n</ul>\\n<h3>其他</h3>\\n<p>更新 Electron 到 8.2.5</p>\\n\"},{\"version\":\"0.17.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增多语言设置，目前软件内置了简体中文、繁体中文、英语三种语言，欢迎提交PR翻译更多语言！</li>\\n<li>新增无法打开外部歌单FAQ</li>\\n<li>新增启动参数<code>search</code>，使用例子：<code>.\\\\lx-music-desktop.exe -search=&quot;突然的自我 - 伍佰&quot;</code></li>\\n<li>新增音频输出设置</li>\\n<li>新增软件内的包括字体在内的界面内容大小调整，现在当窗口大小切换到“较小/大/较大”时，软件内的元素将会适当减小或加大，窗口大小的“小”与“中”内的元素将保持之前的大小暂不做改变</li>\\n<li>新增音源别名，默认将显示别名，想要显示回原名可到设置切换（免责声明：别名仅是本软件用于描述各音源的标签，其名字归版权方所有）</li>\\n<li>新增发现新版本更新失败弹窗的忽略提醒按钮，忽略提醒后，以后同一个版本再失败时将不会弹窗提醒，但仍可到设置-版本更新手动点开更新弹窗查看或恢复提醒</li>\\n<li>新增热搜词，默认关闭，可到设置开启</li>\\n<li>新增历史搜索记录，默认关闭，可到设置开启（右击单个历史记录标签可移除所点击的记录）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化月里嫦娥皮肤侧栏鼠标悬浮颜色</li>\\n<li>优化播放进度条的动画效果</li>\\n<li>现在添加下载任务时，后面添加的任务会在列表顶部插入</li>\\n<li>优化歌单打开机制，现在歌单加载失败时会提示加载失败了，并且支持直接打开企鹅、酷我手机分享出来的歌单了</li>\\n<li>优化右上角最小化/关闭按钮布局</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复歌单详情处于加载状态时无法返回的问题</li>\\n<li>修复鼠标右击复制列表内容时会复制音质标签的问题</li>\\n<li>修复<code>0.6.2</code>及以前的版本导出的“所有数据”内的歌曲列表无法导入的问题</li>\\n<li>修复下载列表在某些情况下无法取消全选的问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新Electron到 8.1.1</li>\\n</ul>\\n\"},{\"version\":\"0.16.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>允许选中列表内歌曲名、歌手名、专辑名内的文字，选中后可使用键盘快捷键进行复制</li>\\n<li>新增在列表可选内容区域<strong>鼠标右击</strong>时自动复制列表已选文字的功能</li>\\n<li>新增在搜索框<strong>鼠标右击</strong>时自动粘贴剪贴板的文本到搜索框中</li>\\n<li>任务下载失败时将显示搜索按钮，方便在其他源搜索该歌曲</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化木叶之村主题翻页器背景颜色</li>\\n<li>优化各个主题音质标签颜色</li>\\n<li>优化其他一些界面细节及用户交互效果</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复启用透明窗口鼠标不穿透的bug</li>\\n<li>修复大窗口时设置的音乐来源选项不换行的问题</li>\\n<li>修复某些情况下暂停任务会自动开始任务的问题</li>\\n<li>修复移除暂停、错误的任务时不删除未下载完成的文件的问题</li>\\n<li>修复酷狗源歌单热门标签歌单列表无法加载问题</li>\\n<li>修复QQ源歌单热门标签歌单列表无法加载问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新electron到 8.0.1</li>\\n</ul>\\n\"},{\"version\":\"0.15.0\",\"desc\":\"<p>洛雪提前祝大家新年快乐、身体健康、阖家幸福！</p>\\n<h3>修复</h3>\\n<ul>\\n<li>修复歌曲下载列表无法加载的问题</li>\\n<li>修复歌曲下载任务数大于最大下载任务数的问题</li>\\n<li>修复某些情况下歌曲下载错误的问题</li>\\n<li>修复下载列表数据没有被迁移直接被丢弃的问题</li>\\n</ul>\\n\"},{\"version\":\"0.14.1\",\"desc\":\"<p>洛雪提前祝大家新年快乐、身体健康、阖家幸福！</p>\\n<h3>修复</h3>\\n<ul>\\n<li>修复由于旧版配置文件迁移出错导致的软件界面无法显示的问题</li>\\n</ul>\\n\"},{\"version\":\"0.14.0\",\"desc\":\"<p>洛雪提前祝大家新年快乐、身体健康、阖家幸福！</p>\\n<h3>新增</h3>\\n<ul>\\n<li>新增各大平台歌单热门标签显示（显示在歌单界面的第一个下拉标签菜单中）</li>\\n<li>恢复QQ音乐源128k音质试听</li>\\n<li>新增不强制win7开启透明效果即可使用，但要配置运行参数<code>-nt</code>，例如：<code>.\\\\lx-music-desktop.exe -nt</code>，添加方法可自行百度“给快捷方式加参数”</li>\\n<li>新增“新年快乐”主题，可自行切换体验</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>减淡各个主题的歌曲列表分隔线颜色</li>\\n<li>在线音乐列表音质标签优化，当歌曲有无损音质时隐藏高品质标签</li>\\n<li>更新改进的歌词播放插件，现在歌词的播放显示将更准确</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复咪咕源无法搜索的问题</li>\\n<li>修复更新弹窗底部文字颜色没有适配当前主题颜色的问题</li>\\n<li>修复导入设置窗口大小、代理设置不立即生效的问题</li>\\n<li>修复在线音乐列表获取失败时无限循环请求的问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>将软件设置与播放列表分离存储成两个文件</li>\\n<li>更新 Electron 到 7.1.9</li>\\n</ul>\\n\"},{\"version\":\"0.13.1\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复全局更新弹窗无法遮盖搜索框的问题</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>由于electron 7.1.3 - 7.1.5 的自动更新功能存在Bug，现降级到7.1.2</li>\\n</ul>\\n\"},{\"version\":\"0.13.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增搜索框搜索建议键盘上下方向键选择功能</li>\\n<li>聚合搜索新增音源显示</li>\\n<li>新增“离开搜索界面时清空搜索列表”设置选项，默认关闭，可到设置-强迫症设置开启</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化“信口雌黄”皮肤配色</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复存在弹出层时，搜索建议列表被弹出层覆盖的问题</li>\\n<li>修复搜索、排行榜、歌单列表多选框从不定状态到选中的Bug</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>因Q音接口失效，移除Q音源的试听与下载</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新electron到7.1.5</li>\\n<li>更新vue到2.6.11</li>\\n</ul>\\n\"},{\"version\":\"0.12.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>优化定位歌曲时的列表滚动机制</li>\\n<li>优化链接点击效果</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复使用酷我源下载歌曲时，当歌曲无封面时下载报错的问题</li>\\n<li>修复酷我源排行榜、歌单详情列表里的歌曲音质匹配问题（原来无论歌曲有无高品、无损都会显示有）</li>\\n<li>禁止外部链接在软件内打开，将所有外部链接从默认浏览器打开</li>\\n</ul>\\n<h3>其他</h3>\\n<ul>\\n<li>更新electron到7.1.2</li>\\n</ul>\\n\"},{\"version\":\"0.12.0\",\"desc\":\"<p>由于新下载库仍然没有完成，但下载功能已经可用，so 移除之前使用的第三方下载库，暂时把新下载库的下载模块直接加入本程序，若出现下载问题欢迎反馈！</p>\\n<h3>新增</h3>\\n<ul>\\n<li>新增下载功能对代理设置的支持，现在若在软件设置了代理服务器，下载功能也将会走代理网络了</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>新下载模块将对恢复下载的任务进行字节校验，用于解决下载进度超过100%后仍然下载的问题</li>\\n<li>注意：目前仍然无法暂停处于<strong>链接获取</strong>状态中的任务</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复Linux deb版本<code>.desktop</code>桌面文件缺少图标的问题，新增中文名称显示、软件分类，感谢@lowy的反馈！</li>\\n<li>修复下载列表歌曲状态分类列表操作Bug</li>\\n<li>修复歌曲封面下载失败时仍然执行嵌入封面操作导致报错的问题</li>\\n<li>跳过重复添加<strong>相同歌曲名与扩展名的歌曲</strong>，例如你之前下载了A歌曲的128k音质，现在想要下载它的320k音质，但由于两者都是MP3格式，会因为重名导致之前的128k音质被覆盖但列表中仍然显示两种音质的问题（但实际上都是指向后面的320k音质）</li>\\n</ul>\\n\"},{\"version\":\"0.11.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增歌曲缓冲定时器，尝试用于解决网络正常但是歌曲缓冲过久的问题</li>\\n<li>新增下载管理的任务状态分类</li>\\n<li>添加<strong>杀毒软件提示有病毒或恶意行为</strong>的说明，可到<strong>常见问题</strong>拉到最后查看（常见问题可在开源地址找到）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化更新弹窗机制及其内容描述，对于可以自动更新的版本，现在可以看到软件的下载进度了</li>\\n</ul>\\n\"},{\"version\":\"0.10.0\",\"desc\":\"<h4>优化</h4>\\n<ul>\\n<li>大幅减少程序<strong>播放时</strong>对CPU与GPU的使用，经测试CPU使用减少60%以上，GPU使用减少90%以上，这应该能解决MAC系统上的温度上涨的问题</li>\\n</ul>\\n<h4>修复</h4>\\n<ul>\\n<li>修复酷我源<strong>搜索提示</strong>、<strong>排行榜</strong>无法获取的问题</li>\\n<li>修复咪咕源无法播放的问题</li>\\n</ul>\\n\"},{\"version\":\"0.9.1\",\"desc\":\"<h4>修复</h4>\\n<ul>\\n<li>修复没有配置文件时程序启动出错的问题</li>\\n</ul>\\n\"},{\"version\":\"0.9.0\",\"desc\":\"<h4>新增</h4>\\n<ul>\\n<li>新增窗口大小设置，若觉得软件窗口小可以到设置页调大点</li>\\n<li>新增定位当前播放歌曲，点击播放栏左侧的<strong>歌曲图片</strong>可在播放列表定位当前播放的歌曲（该功能对播放下载列表的歌曲无效）</li>\\n</ul>\\n<h4>修复</h4>\\n<ul>\\n<li>修复搜索提示失效的问题</li>\\n<li>修复从歌单或列表点击搜索按钮搜索目标歌曲时，搜索框未聚焦仍然弹出候选搜索列表的问题</li>\\n</ul>\\n\"},{\"version\":\"0.8.2\",\"desc\":\"<h4>修复</h4>\\n<ul>\\n<li>兼容旧版酷我源搜索列表过滤128k音质的bug（注：0.8.1版本仅修复了酷我源的歌曲过滤问题，该修复仅对以后添加的歌曲有效，如果是之前添加的歌曲仍会出现这个问题，现修复对之前旧列表数据的兼容处理）</li>\\n</ul>\\n\"},{\"version\":\"0.8.1\",\"desc\":\"<h4>修复</h4>\\n<ul>\\n<li>修复酷我源搜索歌曲结果未添加128k音质导致播放128k音质时显示“该歌曲没有可播放的音频”的问题</li>\\n</ul>\\n\"},{\"version\":\"0.8.0\",\"desc\":\"<h4>新增</h4>\\n<ul>\\n<li>新增网易云源歌曲搜索</li>\\n<li>新增网易云源歌单</li>\\n<li>新增各平台通过输入歌单链接或歌单ID打开歌单详情列表，目前只适配了<strong>网页版歌单链接</strong>，其他方式的歌单链接可能无法解析，但你可想办法获取歌单ID后输入打开。注：各平台歌单ID均为纯数字，若遇到链接里存在歌单ID但无法解析的歌单链接，可以到GitHub提交issue或发送邮件或加群830125506反馈！</li>\\n<li>新增音量调整滑动功能，现在支持鼠标左右拖动调整音量了</li>\\n</ul>\\n<h4>优化</h4>\\n<ul>\\n<li>优化搜索框搜索体验</li>\\n<li>优化音量条交互视觉效果</li>\\n<li>缓存歌单详情列表数据</li>\\n</ul>\\n<h4>修复</h4>\\n<ul>\\n<li>修复QQ源歌单无法翻页Bug</li>\\n<li>修复默认列表没有创建时无法显示收藏列表的Bug</li>\\n<li>修复网易云128k直接试听</li>\\n<li>修复歌曲音质不存在时仍然播放或下载的Bug</li>\\n<li>修复调整音量时，调整的位置与鼠标点击的位置不一致的问题</li>\\n</ul>\\n\"},{\"version\":\"0.7.0\",\"desc\":\"<h4>新增</h4>\\n<ul>\\n<li>新增“我的收藏”本地播放列表</li>\\n<li>新增缓存清理功能，可到<strong>设置-其他</strong>查看与清理软件缓存</li>\\n<li>新增QQ音乐源搜索</li>\\n<li>新增咪咕源搜索</li>\\n<li>新增咪咕源歌单</li>\\n<li>新增咪咕源排行榜</li>\\n<li>新增我的音乐列表歌曲源显示，默认关闭，可到<strong>设置-列表设置</strong>开启</li>\\n</ul>\\n<h4>优化</h4>\\n<ul>\\n<li>优化选择框动画效果</li>\\n<li>尝试优化选我的音乐列表内容很多时多选的卡顿问题</li>\\n</ul>\\n<h4>修复</h4>\\n<ul>\\n<li>修复列表延迟显示的Bug</li>\\n<li>修复QQ音源128k音质试听</li>\\n</ul>\\n\"},{\"version\":\"0.6.2\",\"desc\":\"<p>祝贺祖国成立70周年~！</p>\\n<h4>新增</h4>\\n<ul>\\n<li>新增QQ音乐源歌单</li>\\n</ul>\\n<h4>修复</h4>\\n<ul>\\n<li>修正火影皮肤名字</li>\\n<li>修复当试听列表为空时，无法切到其他界面的Bug</li>\\n<li>修复百度源搜索结果为空时的接口处理Bug</li>\\n<li>恢复<strong>酷狗</strong>其他音质播放</li>\\n</ul>\\n\"},{\"version\":\"0.6.1\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增试听列表<strong>滚动条位置恢复</strong>设置（可自动恢复到上次离开时的列表滚动位置），本功能默认开启，若不需要可到设置-列表设置将其关闭</li>\\n<li>新增 <strong>《海贼王》</strong> 皮肤，喜欢个性化的可以试试~</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>新增DNS解析缓存，加快请求速度</li>\\n<li>优化代码逻辑，减少软件对系统资源的占用</li>\\n<li>优化新版本信息检测，尽量减少弹出版本获取失败弹窗弹出的概率</li>\\n<li>优化下拉列表动画效果</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复请求超时的逻辑处理Bug，尝试修复请求无法取消导致的正在播放的歌曲与界面显示的信息不一致的问题</li>\\n<li>修复其他一些小Bug</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>移除 <code>192k</code> 音质</li>\\n<li>移除酷我音源 <code>ape</code> 音质，无损推荐 <code>flac</code> 格式</li>\\n</ul>\\n\"},{\"version\":\"0.6.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增音乐<strong>聚合搜索</strong>，目前支持酷我、酷狗、百度源搜索</li>\\n<li>新增代理功能</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化从《梦里嫦娥》皮肤切换到其他皮肤时侧栏动画的切换效果</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复试听列表没有歌曲时会显示列表加载中的Bug</li>\\n<li>修复切换歌单列表详情时的UI Bug</li>\\n</ul>\\n\"},{\"version\":\"0.5.5\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>月是故乡明，祝大家中秋快乐🥮~~新增个性皮肤**《月里嫦娥》**，时间仓促，皮肤还不是很完善，可以试试喜不喜欢~😉</li>\\n<li>新增 MAC 版本退出快捷键支持</li>\\n<li>新增点击播放器中的歌曲标题可以复制标题的功能（遇到好听的歌曲方便分享）</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复 MAC 系统下软件关闭时再次从 dock 打开时报错的Bug</li>\\n<li>修复下载的歌曲文件名中包含命名规则不允许的符号时下载失败的问题（若歌曲名包含这些符号会自动将其移除）</li>\\n<li>修复 MAC 版本不能复制粘贴的问题</li>\\n</ul>\\n\"},{\"version\":\"0.5.4\",\"desc\":\"<h3>移除</h3>\\n<ul>\\n<li>下载的FLAC文件在修改歌曲信息后，软件无法播放，但使用本地播放器可以播放</li>\\n<li>为了稳妥起见，暂时移除FLAC格式的meta信息修改</li>\\n<li>MP3格式无此问题</li>\\n</ul>\\n\"},{\"version\":\"0.5.3\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>更新所有依赖包到最新</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复试听酷狗源的音乐仍然获取320k音质导致获取失败的Bug</li>\\n</ul>\\n\"},{\"version\":\"0.5.2\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增强迫症设置-离开搜索界面时是否清空搜索框</li>\\n<li>设置-关于板块新增常见问题链接</li>\\n<li>歌单左上角的分类按钮添加一个<strong>向下图标</strong>，方便识别该按钮为下拉框（该按钮可选择歌单类型，请自行尝试）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>略微优化最小化按钮字符</li>\\n<li>优化试听列表的加载体验，当歌曲数过多时列表将延迟加载</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复下载管理的一些Bug</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>因接口失效，移除网易云音源，酷狗音源仅支持播放128k音质</li>\\n</ul>\\n\"},{\"version\":\"0.5.1\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增右上角最小化/关闭按钮鼠标滑过符号</li>\\n<li>新增下载列表定位文件按钮</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复百度源歌单全部分类无法加载的问题</li>\\n<li>修复更新弹窗无法弹出的问题</li>\\n</ul>\\n\"},{\"version\":\"0.5.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<strong>封面嵌入</strong>（默认开启，可到设置-下载设置关闭）</li>\\n<li>新增<strong>歌词下载</strong>（默认关闭，可到设置-下载设置开启）</li>\\n<li>新增单例应用功能（实现软件单开功能，禁止软件多开）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化歌单列表动画</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复歌单无法翻页的问题</li>\\n<li>修复在某些情况下，添加下载歌曲导致下载列表崩溃的问题</li>\\n<li>修复版本更新弹窗Bug</li>\\n<li>修复酷狗歌单推荐歌单出现在其他分类中的Bug</li>\\n</ul>\\n\"},{\"version\":\"0.4.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<strong>歌单</strong>功能，目前支持酷我、酷狗、百度源歌单</li>\\n<li>在设置界面-关于洛雪音乐说明部分新增<strong>最新版网盘下载地址</strong>与<strong>打赏地址</strong></li>\\n<li>新增酷狗 电音热歌榜、DJ热歌榜</li>\\n<li>新增版本更新超时功能，对于部分无法访问GitHub的用户做更新超时提醒</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li><strong>注意</strong>：0.4.0以前的版本即将失效，请更新到0.4.0版本</li>\\n</ul>\\n\"},{\"version\":\"0.3.5\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<strong>测试接口</strong>，该接口同样速度较慢，但软件的大部分功能可用，<strong>请自行切换到该接口</strong>，找接口辛苦，且用且珍惜！</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>取消需要刷新URL时windows任务栏进度显示错误状态（现显示为暂停状态）</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复使用临时接口时在试听列表双击灰色歌曲仍然会进行播放的Bug</li>\\n<li>修复歌词加载Bug</li>\\n</ul>\\n\"},{\"version\":\"0.3.4\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>减少接口不稳定带来的影响，适当增加请求等待时间</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复播放过程中URL过期不会刷新URL的问题</li>\\n</ul>\\n\"},{\"version\":\"0.3.3\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li><strong>messoer</strong>的接口已经关闭，暂时切换到临时接口使用，部分功能受限。。。</li>\\n<li>修复设置界面更新出错时仍然显示更新下载中的问题</li>\\n<li>修复手动定位播放进度条时存在偏差的问题</li>\\n<li>屏蔽播放器中没有歌曲时对进度条的点击</li>\\n</ul>\\n\"},{\"version\":\"0.3.2\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增酷狗排行榜其他音质下载</li>\\n</ul>\\n\"},{\"version\":\"0.3.1\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复音量条主题适配</li>\\n</ul>\\n\"},{\"version\":\"0.3.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<strong>MAC</strong>及<strong>Linux</strong>版本（需要的可自行下载）</li>\\n<li>新增音量调整</li>\\n<li>新增任务栏播放进度条控制选项（现在可在设置界面关闭在任务栏显示的播放进度）</li>\\n<li>新增更新出错时的弹窗提示</li>\\n<li>从该版本起，非安装版也会有更新弹窗提醒了，但仍然需要手动下载新版本更新，版本信息可到设置页面查看</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>强制把临时接口设置回 <code>messoer</code> 接口</li>\\n</ul>\\n\"},{\"version\":\"0.2.3\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增任务栏程序标题改变功能（播放歌曲时任务栏标题将显示当前播放的歌曲）</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>使用临时接口时，试听列表中的下载按钮仍然能点击的Bug</li>\\n<li>修复某些情况下歌曲链接未能缓存的问题</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>移除临时接口（因服务器被攻击，本接口已关闭）</li>\\n<li>移除列表栏设置的隐藏专辑栏选项（感觉这个设置并没有什么luan用，并且还会打破布局）</li>\\n</ul>\\n\"},{\"version\":\"0.2.2\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复下载过程中出错重试5次都失败后不会自动开始下一个任务的Bug</li>\\n<li>修复播放到一半URL过期时不会刷新URL直接播放下一首的问题</li>\\n</ul>\\n\"},{\"version\":\"0.2.1\",\"desc\":\"<h3>优化</h3>\\n<ul>\\n<li>新增歌曲URL存储，当URL无效时才重新获取，以减少接口不稳定的影响</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复歌曲加载无法加载时自动切换混乱的Bug</li>\\n<li>修复移除列表最后一首歌曲时播放器不停止播放的问题</li>\\n</ul>\\n\"},{\"version\":\"0.2.0\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增<strong>百度音乐</strong>排行榜及其音乐直接试听与下载</li>\\n<li>新增网易云排行榜音乐直接试听与下载（目前仅支持128k音质）</li>\\n<li>新增酷狗排行榜音乐直接试听与下载（目前仅支持128k音质）</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复更新弹窗历史版本描述多余的换行问题</li>\\n<li>修复歌曲无法播放的情况下歌词仍会播放的问题</li>\\n</ul>\\n\"},{\"version\":\"0.1.6\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复列表多选音源限制Bug</li>\\n</ul>\\n\"},{\"version\":\"0.1.5\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增搜索列表批量试听与下载功能</li>\\n<li>新增排行榜列表批量试听与下载功能</li>\\n<li>新增试听列表批量移除与下载功能</li>\\n<li>新增下载列表批量开始、暂停与移除功能</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化歌曲切换机制</li>\\n</ul>\\n\"},{\"version\":\"0.1.4\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增音乐来源切换，可到设置页面-基本设置 look look !</li>\\n<li>为搜索结果列表添加多选功能。<br>\\nP.S：暂时没想好多选后的操作按钮放哪…</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>重构与改进checkbox组件，使其支持不定选中状态</li>\\n<li>完善上一个版本的http请求封装并切换部分请求到该方法上</li>\\n<li>优化其他一些细节</li>\\n</ul>\\n\"},{\"version\":\"0.1.3\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>新增win32应用构建</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复安装包许可协议乱码问题</li>\\n<li><strong>messoer 提供的接口已挂</strong>，暂时切换到临时接口！</li>\\n</ul>\\n<h3>移除</h3>\\n<ul>\\n<li>由于messoer接口无法使用，QQ音乐排行榜直接播放/下载功能暂时关闭</li>\\n</ul>\\n\"},{\"version\":\"0.1.2\",\"desc\":\"<h3>修复</h3>\\n<ul>\\n<li>修复更新弹窗的内容显示问题</li>\\n</ul>\\n\"},{\"version\":\"0.1.1\",\"desc\":\"<h3>新增</h3>\\n<ul>\\n<li>QQ音乐排行榜直接试听与下载（该接口貌似不太稳定，且用且珍惜！）</li>\\n</ul>\\n<h3>优化</h3>\\n<ul>\\n<li>优化http请求机制</li>\\n<li>更新关于本软件说明</li>\\n</ul>\\n<h3>修复</h3>\\n<ul>\\n<li>修复当上一个歌曲链接正在获取时切换歌曲请求不会取消的问题</li>\\n<li>修复切换歌曲时仍然播放上一首歌曲的问题</li>\\n</ul>\\n\"},{\"version\":\"0.1.0\",\"desc\":\"0.1.0版本发布\"}]}\n"
  },
  {
    "path": "src/common/.eslintrc.cjs",
    "content": "/* eslint-env node */\nconst { base, typescript } = require('../../.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": "src/common/config.ts",
    "content": "export interface WindowSize {\n  id: number\n  name: string\n  width: number\n  height: number\n}\n\nexport const windowSizeList: WindowSize[] = [\n  {\n    id: 0,\n    name: 'smaller',\n    width: 828,\n    height: 540,\n  },\n  {\n    id: 1,\n    name: 'small',\n    width: 920,\n    height: 600,\n  },\n  {\n    id: 2,\n    name: 'medium',\n    width: 1020,\n    height: 660,\n  },\n  {\n    id: 3,\n    name: 'big',\n    width: 1114,\n    height: 718,\n  },\n  {\n    id: 4,\n    name: 'larger',\n    width: 1202,\n    height: 776,\n  },\n  {\n    id: 5,\n    name: 'oversized',\n    width: 1385,\n    height: 896,\n  },\n  {\n    id: 6,\n    name: 'huge',\n    width: 1700,\n    height: 1070,\n  },\n]\n\nexport const navigationUrlWhiteList: RegExp[] = []\n\n// 基础黑白色\n// export const commonColorNames = [\n//   '--color-000', '--color-050', '--color-100', '--color-200', '--color-300', '--color-400',\n//   '--color-500', '--color-600', '--color-700', '--color-800', '--color-900',\n// ] as const\n// export const commonLightColorValues = [\n//   'rgb(255, 255, 255)',\n//   'rgb(217,217,217)',\n//   'rgb(184,184,184)',\n//   'rgb(156,156,156)',\n//   'rgb(133,133,133)',\n//   'rgb(113,113,113)',\n//   'rgb(96,96,96)',\n//   'rgb(82,82,82)',\n//   'rgb(70,70,70)',\n//   'rgb(60,60,60)',\n//   'rgb(51,51,51)',\n// ] as const\n// export const commonDarkColorValues = [\n//   'rgb(11, 11, 11)',\n//   'rgb(60,60,60)',\n//   'rgb(99,99,99)',\n//   'rgb(130,130,130)',\n//   'rgb(155,155,155)',\n//   'rgb(175,175,175)',\n//   'rgb(191,191,191)',\n//   'rgb(204,204,204)',\n//   'rgb(214,214,214)',\n//   'rgb(222,222,222)',\n//   'rgb(229,229,229)',\n// ] as const\n\n"
  },
  {
    "path": "src/common/constants.ts",
    "content": "export const URL_SCHEME_RXP = /^lxmusic:\\/\\//\n\nexport const SPLIT_CHAR = {\n  DISLIKE_NAME: '@',\n  DISLIKE_NAME_ALIAS: '#',\n} as const\n\nexport const STORE_NAMES = {\n  APP_SETTINGS: 'config_v2',\n  DATA: 'data',\n  SYNC: 'sync',\n  HOTKEY: 'hot_key',\n  USER_API: 'user_api',\n  LRC_RAW: 'lyrics',\n  LRC_EDITED: 'lyrics_edited',\n  THEME: 'theme',\n  SOUND_EFFECT: 'sound_effect',\n} as const\n\nexport const APP_EVENT_NAMES = {\n  winMainName: 'win_main',\n  winLyricName: 'win_lyric',\n  trayName: 'tray',\n} as const\n\nexport const LIST_IDS = {\n  DEFAULT: 'default',\n  LOVE: 'love',\n  TEMP: 'temp',\n  DOWNLOAD: 'download',\n  PLAY_LATER: null,\n} as const\n\nexport const DATA_KEYS = {\n  viewPrevState: 'viewPrevState',\n  playInfo: 'playInfo',\n  searchHistoryList: 'searchHistoryList',\n  listScrollPosition: 'listScrollPosition',\n  listPrevSelectId: 'listPrevSelectId',\n  listUpdateInfo: 'listUpdateInfo',\n  ignoreVersion: 'ignoreVersion',\n\n  leaderboardSetting: 'leaderboardSetting',\n  songListSetting: 'songListSetting',\n  searchSetting: 'searchSetting',\n\n  lastStartInfo: 'lastStartInfo',\n} as const\n\nexport const DEFAULT_SETTING = {\n  leaderboard: {\n    source: 'kw',\n    boardId: 'kw__16',\n  },\n\n  songList: {\n    source: 'kw',\n    sortId: 'new',\n    tagId: '',\n  },\n\n  search: {\n    temp_source: 'kw',\n    source: 'all',\n    type: 'music',\n  },\n\n  viewPrevState: {\n    url: '/search',\n    query: {},\n  },\n}\n\nexport const DOWNLOAD_STATUS = {\n  RUN: 'run',\n  WAITING: 'waiting',\n  PAUSE: 'pause',\n  ERROR: 'error',\n  COMPLETED: 'completed',\n} as const\n\nexport const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const\n\nexport const TRAY_AUTO_ID = -1\n"
  },
  {
    "path": "src/common/constants_sync.ts",
    "content": "export const ENV_PARAMS = [\n  'PORT',\n  'BIND_IP',\n  'CONFIG_PATH',\n  'LOG_PATH',\n  'DATA_PATH',\n  'PROXY_HEADER',\n  'MAX_SNAPSHOT_NUM',\n  'LIST_ADD_MUSIC_LOCATION_TYPE',\n  'LX_USER_',\n] as const\n\n\nexport const LIST_IDS = {\n  DEFAULT: 'default',\n  LOVE: 'love',\n  TEMP: 'temp',\n  DOWNLOAD: 'download',\n  PLAY_LATER: null,\n} as const\n\nexport const SYNC_CODE = {\n  helloMsg: 'Hello~::^-^::~v4~',\n  idPrefix: 'OjppZDo6',\n  authMsg: 'lx-music auth::',\n  msgAuthFailed: 'Auth failed',\n  msgBlockedIp: 'Blocked IP',\n  msgConnect: 'lx-music connect',\n\n\n  authFailed: 'Auth failed',\n  missingAuthCode: 'Missing auth code',\n  getServiceIdFailed: 'Get service id failed',\n  connectServiceFailed: 'Connect service failed',\n  connecting: 'Connecting...',\n  unknownServiceAddress: 'Unknown service address',\n} as const\n\nexport const SYNC_CLOSE_CODE = {\n  normal: 1000,\n  failed: 4100,\n} as const\n\nexport const TRANS_MODE: Readonly<Record<LX.Sync.List.SyncMode, LX.Sync.List.SyncMode>> = {\n  merge_local_remote: 'merge_remote_local',\n  merge_remote_local: 'merge_local_remote',\n  overwrite_local_remote: 'overwrite_remote_local',\n  overwrite_remote_local: 'overwrite_local_remote',\n  overwrite_local_remote_full: 'overwrite_remote_local_full',\n  overwrite_remote_local_full: 'overwrite_local_remote_full',\n  cancel: 'cancel',\n} as const\n\nexport const File = {\n  serverDataPath: 'sync/server',\n  clientDataPath: 'sync/client',\n\n  serverInfoJSON: 'serverInfo.json',\n  userDir: 'users',\n  userDevicesJSON: 'devices.json',\n  listDir: 'list',\n  listSnapshotDir: 'snapshot',\n  listSnapshotInfoJSON: 'snapshotInfo.json',\n  dislikeDir: 'dislike',\n  dislikeSnapshotDir: 'snapshot',\n  dislikeSnapshotInfoJSON: 'snapshotInfo.json',\n\n  syncAuthKeysJSON: 'syncAuthKey.json',\n} as const\n\nexport const FeaturesList = [\n  'list',\n  'dislike',\n] as const\n"
  },
  {
    "path": "src/common/defaultHotKey.ts",
    "content": "import { HOTKEY_PLAYER, HOTKEY_COMMON, HOTKEY_DESKTOP_LYRIC } from './hotKey'\n\nconst local: LX.HotKeyConfig = {\n  enable: true,\n  keys: {\n    'mod+f5': {\n      type: HOTKEY_PLAYER.toggle_play.type,\n      name: HOTKEY_PLAYER.toggle_play.name,\n      action: HOTKEY_PLAYER.toggle_play.action,\n    },\n    'mod+arrowleft': {\n      type: HOTKEY_PLAYER.prev.type,\n      name: HOTKEY_PLAYER.prev.name,\n      action: HOTKEY_PLAYER.prev.action,\n    },\n    'mod+arrowright': {\n      type: HOTKEY_PLAYER.next.type,\n      name: HOTKEY_PLAYER.next.name,\n      action: HOTKEY_PLAYER.next.action,\n    },\n    f1: {\n      type: HOTKEY_COMMON.focusSearchInput.type,\n      name: HOTKEY_COMMON.focusSearchInput.name,\n      action: HOTKEY_COMMON.focusSearchInput.action,\n    },\n  },\n}\n\nconst global: LX.HotKeyConfig = {\n  enable: false,\n  keys: {\n    // MediaPlayPause: {\n    //   type: HOTKEY_PLAYER.toggle_play.type,\n    //   name: '',\n    //   action: HOTKEY_PLAYER.toggle_play.action,\n    // },\n    // MediaPreviousTrack: {\n    //   type: HOTKEY_PLAYER.prev.type,\n    //   name: '',\n    //   action: HOTKEY_PLAYER.prev.action,\n    // },\n    // MediaNextTrack: {\n    //   type: HOTKEY_PLAYER.next.type,\n    //   name: '',\n    //   action: HOTKEY_PLAYER.next.action,\n    // },\n    'mod+alt+f5': {\n      type: HOTKEY_PLAYER.toggle_play.type,\n      name: HOTKEY_PLAYER.toggle_play.name,\n      action: HOTKEY_PLAYER.toggle_play.action,\n    },\n    'mod+alt+arrowleft': {\n      type: HOTKEY_PLAYER.prev.type,\n      name: HOTKEY_PLAYER.prev.name,\n      action: HOTKEY_PLAYER.prev.action,\n    },\n    'mod+alt+arrowright': {\n      type: HOTKEY_PLAYER.next.type,\n      name: HOTKEY_PLAYER.next.name,\n      action: HOTKEY_PLAYER.next.action,\n    },\n    'mod+alt+arrowup': {\n      type: HOTKEY_PLAYER.volume_up.type,\n      name: HOTKEY_PLAYER.volume_up.name,\n      action: HOTKEY_PLAYER.volume_up.action,\n    },\n    'mod+alt+arrowdown': {\n      type: HOTKEY_PLAYER.volume_down.type,\n      name: HOTKEY_PLAYER.volume_down.name,\n      action: HOTKEY_PLAYER.volume_down.action,\n    },\n    'mod+alt+0': {\n      type: HOTKEY_DESKTOP_LYRIC.toggle_visible.type,\n      name: HOTKEY_DESKTOP_LYRIC.toggle_visible.name,\n      action: HOTKEY_DESKTOP_LYRIC.toggle_visible.action,\n    },\n    'mod+alt+-': {\n      type: HOTKEY_DESKTOP_LYRIC.toggle_lock.type,\n      name: HOTKEY_DESKTOP_LYRIC.toggle_lock.name,\n      action: HOTKEY_DESKTOP_LYRIC.toggle_lock.action,\n    },\n    'mod+alt+=': {\n      type: HOTKEY_DESKTOP_LYRIC.toggle_always_top.type,\n      name: HOTKEY_DESKTOP_LYRIC.toggle_always_top.name,\n      action: HOTKEY_DESKTOP_LYRIC.toggle_always_top.action,\n    },\n  },\n}\n\nexport default {\n  local,\n  global,\n}\n"
  },
  {
    "path": "src/common/defaultSetting.ts",
    "content": "import path from 'node:path'\nimport os from 'node:os'\n\nconst isMac = process.platform == 'darwin'\nconst isWin = process.platform == 'win32'\n\nconst defaultSetting: LX.AppSetting = {\n  version: '2.1.0',\n\n  'common.windowSizeId': 3,\n  'common.fontSize': 16,\n  'common.startInFullscreen': false,\n  'common.langId': null,\n  'common.apiSource': 'temp',\n  'common.sourceNameType': 'alias',\n  'common.font': '',\n  'common.isShowAnimation': true,\n  'common.randomAnimate': true,\n  'common.isAgreePact': false,\n  'common.controlBtnPosition': isMac ? 'left' : 'right',\n  'common.playBarProgressStyle': 'mini',\n  'common.transparentWindow': !isMac,\n  'common.tryAutoUpdate': true,\n  'common.showChangeLog': true,\n\n  'player.startupAutoPlay': false,\n  'player.togglePlayMethod': 'listLoop',\n  'player.playQuality': '128k',\n  'player.isShowTaskProgess': true,\n  'player.isShowStatusBarLyric': false,\n  'player.volume': 1,\n  'player.powerSaveBlocker': true,\n  'player.isMute': false,\n  'player.playbackRate': 1,\n  'player.preservesPitch': true,\n  'player.isMaxOutputChannelCount': false,\n  'player.mediaDeviceId': 'default',\n  'player.isMediaDeviceRemovedStopPlay': false,\n  'player.isShowLyricTranslation': false,\n  'player.isShowLyricRoma': false,\n  'player.isSwapLyricTranslationAndRoma': false,\n  'player.isS2t': false,\n  'player.isPlayLxlrc': !isMac,\n  'player.isSavePlayTime': false,\n  'player.audioVisualization': false,\n  'player.waitPlayEndStop': true,\n  'player.waitPlayEndStopTime': '',\n  'player.autoSkipOnError': true,\n  'player.isAutoCleanPlayedList': false,\n  'player.soundEffect.convolution.fileName': '',\n  'player.soundEffect.convolution.mainGain': 10,\n  'player.soundEffect.convolution.sendGain': 0,\n  'player.soundEffect.biquadFilter.hz31': 0,\n  'player.soundEffect.biquadFilter.hz62': 0,\n  'player.soundEffect.biquadFilter.hz125': 0,\n  'player.soundEffect.biquadFilter.hz250': 0,\n  'player.soundEffect.biquadFilter.hz500': 0,\n  'player.soundEffect.biquadFilter.hz1000': 0,\n  'player.soundEffect.biquadFilter.hz2000': 0,\n  'player.soundEffect.biquadFilter.hz4000': 0,\n  'player.soundEffect.biquadFilter.hz8000': 0,\n  'player.soundEffect.biquadFilter.hz16000': 0,\n  'player.soundEffect.panner.enable': false,\n  'player.soundEffect.panner.soundR': 5,\n  'player.soundEffect.panner.speed': 25,\n  'player.soundEffect.pitchShifter.playbackRate': 1,\n\n  'playDetail.isZoomActiveLrc': false,\n  'playDetail.isShowLyricProgressSetting': false,\n  'playDetail.style.fontSize': 140,\n  'playDetail.style.align': 'center',\n  'playDetail.isDelayScroll': true,\n\n  'desktopLyric.enable': false,\n  'desktopLyric.isLock': false,\n  'desktopLyric.isAlwaysOnTop': false,\n  'desktopLyric.isAlwaysOnTopLoop': false,\n  'desktopLyric.isShowTaskbar': false,\n  'desktopLyric.audioVisualization': false,\n  'desktopLyric.fullscreenHide': true,\n  'desktopLyric.pauseHide': true,\n  'desktopLyric.width': 450,\n  'desktopLyric.height': 300,\n  'desktopLyric.x': null,\n  'desktopLyric.y': null,\n  'desktopLyric.isLockScreen': isWin,\n  'desktopLyric.isDelayScroll': true,\n  'desktopLyric.scrollAlign': 'center',\n  'desktopLyric.isHoverHide': false,\n  'desktopLyric.direction': 'horizontal',\n  'desktopLyric.style.align': 'center',\n  'desktopLyric.style.font': '',\n  'desktopLyric.style.fontSize': 20,\n  'desktopLyric.style.lineGap': 15,\n  'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',\n  'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',\n  'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.18)',\n  // 'desktopLyric.style.fontWeight': false,\n  'desktopLyric.style.opacity': 95,\n  'desktopLyric.style.ellipsis': false,\n  'desktopLyric.style.isZoomActiveLrc': false,\n  'desktopLyric.style.isFontWeightFont': true,\n  'desktopLyric.style.isFontWeightLine': true,\n  'desktopLyric.style.isFontWeightExtended': true,\n\n  'list.isClickPlayList': false,\n  'list.isShowSource': true,\n  'list.isSaveScrollLocation': true,\n  'list.addMusicLocationType': 'top',\n  'list.actionButtonsVisible': false,\n\n  'download.enable': false,\n  'download.isSavePathGroupByListName': false,\n  'download.savePath': path.join(os.homedir(), 'Desktop'),\n  'download.fileName': '歌名 - 歌手',\n  'download.maxDownloadNum': 3,\n  'download.skipExistFile': true,\n  'download.isDownloadLrc': false,\n  'download.isDownloadLxLrc': true,\n  'download.isDownloadTLrc': false,\n  'download.isDownloadRLrc': false,\n  'download.lrcFormat': 'utf8',\n  'download.isEmbedPic': true,\n  'download.isEmbedLyric': false,\n  'download.isEmbedLyricLx': true,\n  'download.isEmbedLyricT': false,\n  'download.isEmbedLyricR': false,\n  'download.isUseOtherSource': false,\n\n  'search.isShowHotSearch': false,\n  'search.isShowHistorySearch': false,\n  'search.isFocusSearchBox': false,\n\n  'network.proxy.enable': false,\n  'network.proxy.host': '',\n  'network.proxy.port': '',\n\n  'tray.enable': false,\n  // 'tray.isToTray': false,\n  'tray.themeId': 0,\n\n  'sync.mode': 'server',\n  'sync.enable': false,\n  'sync.server.port': '23332',\n  'sync.server.maxSsnapshotNum': 5,\n  'sync.client.host': '',\n\n  'openAPI.enable': false,\n  'openAPI.port': '23330',\n  'openAPI.bindLan': false,\n\n  // 'theme.id': 'blue_plus',\n  'theme.id': 'green',\n  'theme.lightId': 'green',\n  'theme.darkId': 'black',\n\n  'odc.isAutoClearSearchInput': false,\n  'odc.isAutoClearSearchList': false,\n\n}\n\n\n// 使用新年皮肤\nif (new Date().getMonth() < 2) {\n  defaultSetting['theme.id'] = 'happy_new_year'\n  defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 57, 71, 1)'\n}\n\n\nexport default defaultSetting\n\n"
  },
  {
    "path": "src/common/error.ts",
    "content": "import { log } from './utils'\n\nconst ignoreErrorMessage = [\n  'Possible side-effect in debug-evaluate',\n  'Unexpected end of input',\n]\n\nprocess.on('uncaughtException', err => {\n  if (ignoreErrorMessage.includes(err?.message)) return\n  console.error('An uncaught error occurred!')\n  console.error(err)\n  log.error(err)\n})\nprocess.on('unhandledRejection', (reason, p) => {\n  console.error('Unhandled Rejection at: Promise ', p)\n  console.error(' reason: ', reason)\n  log.error(reason)\n})\n"
  },
  {
    "path": "src/common/hotKey.ts",
    "content": "import { APP_EVENT_NAMES } from './constants'\n\n\nconst keyName = {\n  common: APP_EVENT_NAMES.winMainName,\n  player: APP_EVENT_NAMES.winMainName,\n  desktop_lyric: APP_EVENT_NAMES.winLyricName,\n}\n\nconst hotKey = {\n  common: {\n    min: {\n      name: 'min',\n      action: 'min',\n      type: '',\n    },\n    min_toggle: {\n      name: 'toggle_min',\n      action: 'toggle_min',\n      type: '',\n    },\n    hide_toggle: {\n      name: 'toggle_hide',\n      action: 'toggle_hide',\n      type: '',\n    },\n    close: {\n      name: 'toggle_close',\n      action: 'toggle_close',\n      type: '',\n    },\n    focusSearchInput: {\n      name: 'focus_search_input',\n      action: 'focus_search_input',\n      type: '',\n    },\n  },\n  player: {\n    toggle_play: {\n      name: 'toggle_play',\n      action: 'toggle_play',\n      type: '',\n    },\n    next: {\n      name: 'next',\n      action: 'next',\n      type: '',\n    },\n    prev: {\n      name: 'prev',\n      action: 'prev',\n      type: '',\n    },\n    seekbackward: {\n      name: 'seekbackward',\n      action: 'seekbackward',\n      type: '',\n    },\n    seekforward: {\n      name: 'seekforward',\n      action: 'seekforward',\n      type: '',\n    },\n    volume_up: {\n      name: 'volume_up',\n      action: 'volume_up',\n      type: '',\n    },\n    volume_down: {\n      name: 'volume_down',\n      action: 'volume_down',\n      type: '',\n    },\n    volume_mute: {\n      name: 'volume_mute',\n      action: 'volume_mute',\n      type: '',\n    },\n    music_love: {\n      name: 'music_love',\n      action: 'music_love',\n      type: '',\n    },\n    music_unlove: {\n      name: 'music_unlove',\n      action: 'music_unlove',\n      type: '',\n    },\n    music_dislike: {\n      name: 'music_dislike',\n      action: 'music_dislike',\n      type: '',\n    },\n  },\n  desktop_lyric: {\n    toggle_visible: {\n      name: 'toggle_visible',\n      action: 'toggle_visible',\n      type: '',\n    },\n    toggle_lock: {\n      name: 'toggle_lock',\n      action: 'toggle_lock',\n      type: '',\n    },\n    toggle_always_top: {\n      name: 'toggle_always_top',\n      action: 'toggle_always_top',\n      type: '',\n    },\n  },\n}\n\nfor (const type of Object.keys(hotKey) as Array<keyof typeof hotKey>) {\n  let keys = hotKey[type]\n  for (const key of Object.keys(keys) as Array<keyof typeof keys>) {\n    const keyInfo: LX.HotKey = keys[key]\n    keyInfo.action = `${type}_${keyInfo.action}`\n    keyInfo.name = `${type}_${keyInfo.name}`\n    keyInfo.type = keyName[type] as keyof typeof hotKey\n  }\n}\n\nexport const HOTKEY_COMMON = hotKey.common\nexport const HOTKEY_PLAYER = hotKey.player\nexport const HOTKEY_DESKTOP_LYRIC = hotKey.desktop_lyric\n"
  },
  {
    "path": "src/common/ipcNames.ts",
    "content": "const modules = {\n  common: {\n    get_env_params: 'get_env_params',\n    deeplink: 'deeplink',\n    clear_env_params_deeplink: 'clear_env_params_deeplink',\n    system_theme_change: 'system_theme_change',\n    theme_change: 'theme_change',\n    get_system_fonts: 'get_system_fonts',\n    get_app_setting: 'get_app_setting',\n    set_app_setting: 'set_app_setting',\n  },\n  player: {\n    invoke_play_music: 'play_music',\n    invoke_play_next: 'play_next',\n    invoke_play_prev: 'play_prev',\n    invoke_toggle_play: 'toggle_play',\n    player_play: 'player_play',\n    player_pause: 'player_pause',\n    player_stop: 'player_stop',\n    player_error: 'player_error',\n\n    list_data_overwire: 'list_data_overwire',\n    list_get: 'list_get',\n    list_add: 'list_add',\n    list_remove: 'list_remove',\n    list_update: 'list_update',\n    list_update_position: 'list_update_position',\n    list_music_get: 'list_music_get',\n    list_music_add: 'list_music_add',\n    list_music_move: 'list_music_move',\n    list_music_remove: 'list_music_remove',\n    list_music_update: 'list_music_update',\n    list_music_update_position: 'list_music_update_position',\n    list_music_overwrite: 'list_music_overwrite',\n    list_music_clear: 'list_music_clear',\n    list_music_check_exist: 'list_music_check_exist',\n    list_music_get_list_ids: 'list_music_get_list_ids',\n  },\n  dislike: {\n    get_dislike_music_infos: 'get_dislike_music_infos',\n    add_dislike_music_infos: 'add_dislike_music_infos',\n    overwrite_dislike_music_infos: 'overwrite_dislike_music_infos',\n    clear_dislike_music_infos: 'clear_dislike_music_infos',\n  },\n  winMain: {\n    focus: 'focus',\n    close: 'close',\n    min: 'min',\n    max: 'max',\n    fullscreen: 'fullscreen',\n    set_app_name: 'set_app_name',\n    clear_cache: 'clear_cache',\n    get_cache_size: 'get_cache_size',\n    inited: 'inited',\n    show_save_dialog: 'show_save_dialog',\n    show_select_dialog: 'show_select_dialog',\n    show_dialog: 'show_dialog',\n    open_dir_in_explorer: 'open_dir_in_explorer',\n    open_dev_tools: 'open_dev_tools',\n    set_power_save_blocker: 'set_power_save_blocker',\n\n    player_status: 'player_status',\n    change_tray: 'change_tray',\n    quit_update: 'quit_update',\n    update_check: 'update_check',\n    update_download_update: 'update_download_update',\n    update_available: 'update_available',\n    update_error: 'update_error',\n    update_progress: 'update_progress',\n    update_downloaded: 'update_downloaded',\n    update_not_available: 'update_not_available',\n    set_ignore_mouse_events: 'set_ignore_mouse_events',\n    set_window_size: 'set_window_size',\n\n    handle_request: 'handle_request',\n    cancel_request: 'cancel_request',\n\n\n    restart_window: 'restart_window',\n\n    // lang_s2t: 'lang_s2t',\n\n    handle_kw_decode_lyric: 'handle_kw_decode_lyric',\n    handle_tx_decode_lyric: 'handle_tx_decode_lyric',\n    get_lyric_info: 'get_lyric_info',\n    set_lyric_info: 'set_lyric_info',\n    set_config: 'set_config',\n    set_hot_key_config: 'set_hot_key_config',\n    on_config_change: 'on_config_change',\n    key_down: 'key_down',\n    quit: 'quit',\n    min_toggle: 'min_toggle',\n    hide_toggle: 'hide_toggle',\n\n    get_other_source: 'get_other_source',\n    save_other_source: 'save_other_source',\n    clear_other_source: 'clear_other_source',\n    get_other_source_count: 'get_other_source_count',\n    get_data: 'get_data',\n    save_data: 'save_data',\n    get_sound_effect_eq_preset: 'get_sound_effect_eq_preset',\n    save_sound_effect_eq_preset: 'save_sound_effect_eq_preset',\n    get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset',\n    save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset',\n    // get_sound_effect_pitch_shifter_preset: 'get_sound_effect_pitch_shifter_preset',\n    // save_sound_effect_pitch_shifter_preset: 'save_sound_effect_pitch_shifter_preset',\n    get_hot_key: 'get_hot_key',\n\n    import_user_api: 'import_user_api',\n    remove_user_api: 'remove_user_api',\n    set_user_api: 'set_user_api',\n    get_user_api_list: 'get_user_api_list',\n    request_user_api: 'request_user_api',\n    request_user_api_cancel: 'request_user_api_cancel',\n    get_user_api_status: 'get_user_api_status',\n    user_api_status: 'user_api_status',\n    user_api_show_update_alert: 'user_api_show_update_alert',\n    user_api_set_allow_update_alert: 'user_api_set_allow_update_alert',\n\n    get_palyer_lyric: 'get_lyric',\n    // save_lyric: 'save_lyric',\n    // clear_lyric: 'clear_lyric',\n    get_lyric_raw: 'get_lyric_raw',\n    save_lyric_raw: 'save_lyric_raw',\n    clear_lyric_raw: 'clear_lyric_raw',\n    get_lyric_raw_count: 'get_lyric_raw_count',\n    get_lyric_edited: 'get_lyric_edited',\n    save_lyric_edited: 'save_lyric_edited',\n    remove_lyric_edited: 'remove_lyric_edited',\n    clear_lyric_edited: 'clear_lyric_edited',\n    get_lyric_edited_count: 'get_lyric_edited_count',\n    get_music_url: 'get_music_url',\n    save_music_url: 'save_music_url',\n    clear_music_url: 'clear_music_url',\n    get_music_url_count: 'get_music_url_count',\n\n    open_api_action: 'open_api_action',\n    sync_action: 'sync_action',\n    sync_get_server_devices: 'sync_get_server_devices',\n    sync_remove_server_device: 'sync_remove_server_device',\n\n    process_new_desktop_lyric_client: 'process_new_desktop_lyric_client',\n\n    player_action_set_buttons: 'player_action_set_buttons',\n    // player_action_set_thumbnail_clip: 'player_action_set_thumbnail_clip',\n    player_action_on_button_click: 'player_action_on_button_click',\n\n    get_themes: 'get_themes',\n    save_theme: 'save_theme',\n    remove_theme: 'remove_theme',\n\n    download_list_get: 'download_list_get',\n    download_list_add: 'download_list_add',\n    download_list_update: 'download_list_update',\n    download_list_remove: 'download_list_remove',\n    download_list_clear: 'download_list_clear',\n  },\n  winLyric: {\n    close: 'close',\n    set_config: 'set_config',\n    get_config: 'get_config',\n    on_config_change: 'on_config_change',\n    main_window_inited: 'main_window_inited',\n    set_win_bounds: 'set_win_bounds',\n    set_win_resizeable: 'set_win_resizeable',\n    key_down: 'key_down',\n    request_main_window_channel: 'request_main_window_channel',\n    provide_main_window_channel: 'provide_main_window_channel',\n  },\n  hotKey: {\n    enable: 'enable',\n    status: 'status',\n    set_config: 'set_config',\n  },\n}\n\n\nfor (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {\n  let eventNames = modules[moduleName]\n  for (const eventName of Object.keys(eventNames) as Array<keyof typeof eventNames>) {\n    eventNames[eventName] = `${moduleName}_${eventName as string}` as never\n  }\n}\n\n// for (const moduleName of Object.keys(modules) as Array<keyof typeof modules>) {\n//   let eventNames = modules[moduleName]\n//   for (const eventName of Object.keys(eventNames)) {\n//     eventNames[eventName] = `${moduleName}_${eventName}`\n//   }\n// }\n\n\nexport const CMMON_EVENT_NAME = modules.common\nexport const PLAYER_EVENT_NAME = modules.player\nexport const DISLIKE_EVENT_NAME = modules.dislike\nexport const WIN_MAIN_RENDERER_EVENT_NAME = modules.winMain\nexport const WIN_LYRIC_RENDERER_EVENT_NAME = modules.winLyric\nexport const HOTKEY_RENDERER_EVENT_NAME = modules.hotKey\n"
  },
  {
    "path": "src/common/mainIpc.ts",
    "content": "import { ipcMain } from 'electron'\n\nexport function mainOn(name: string, listener: LX.IpcMainEventListener): void\nexport function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void\nexport function mainOn<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {\n  ipcMain.on(name, (event, params) => {\n    listener({ event, params })\n  })\n}\n\nexport function mainOnce(name: string, listener: LX.IpcMainEventListener): void\nexport function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void\nexport function mainOnce<T>(name: string, listener: LX.IpcMainEventListenerParams<T>): void {\n  ipcMain.once(name, (event, params) => {\n    listener({ event, params })\n  })\n}\n\nexport const mainOff = (name: string, listener: (...args: any[]) => void) => {\n  ipcMain.removeListener(name, listener)\n}\n\nexport const mainOffAll = (name: string) => {\n  ipcMain.removeAllListeners(name)\n}\n\nexport function mainHandle(name: string, listener: LX.IpcMainInvokeEventListener): void\nexport function mainHandle<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void\nexport function mainHandle<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void\nexport function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void\nexport function mainHandle<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {\n  ipcMain.handle(name, async(event, params) => {\n    return listener({ event, params })\n  })\n}\n\nexport function mainHandleOnce(name: string, listener: LX.IpcMainInvokeEventListener): void\nexport function mainHandleOnce<T>(name: string, listener: LX.IpcMainInvokeEventListenerParams<T>): void\nexport function mainHandleOnce<V>(name: string, listener: LX.IpcMainInvokeEventListenerValue<V>): void\nexport function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void\nexport function mainHandleOnce<T, V>(name: string, listener: LX.IpcMainInvokeEventListenerParamsValue<T, V>): void {\n  ipcMain.handleOnce(name, async(event, params) => {\n    return listener({ event, params })\n  })\n}\nexport const mainHandleRemove = (name: string) => {\n  ipcMain.removeHandler(name)\n}\n\nexport function mainSend(window: Electron.BrowserWindow, name: string): void\nexport function mainSend<T>(window: Electron.BrowserWindow, name: string, params: T): void\nexport function mainSend<T>(window: Electron.BrowserWindow, name: string, params?: T): void {\n  window.webContents.send(name, params)\n}\n"
  },
  {
    "path": "src/common/rendererIpc.ts",
    "content": "import { ipcRenderer } from 'electron'\n\nexport function rendererSend(name: string): void\nexport function rendererSend<T>(name: string, params: T): void\nexport function rendererSend<T>(name: string, params?: T): void {\n  ipcRenderer.send(name, params)\n}\n\nexport function rendererSendSync(name: string): void\nexport function rendererSendSync<T>(name: string, params: T): void\nexport function rendererSendSync<T>(name: string, params?: T): void {\n  ipcRenderer.sendSync(name, params)\n}\n\nexport async function rendererInvoke(name: string): Promise<void>\nexport async function rendererInvoke<V>(name: string): Promise<V>\nexport async function rendererInvoke<T>(name: string, params: T): Promise<void>\nexport async function rendererInvoke<T, V>(name: string, params: T): Promise<V>\nexport async function rendererInvoke <T, V>(name: string, params?: T): Promise<V> {\n  return ipcRenderer.invoke(name, params)\n}\n\nexport function rendererOn(name: string, listener: LX.IpcRendererEventListener): void\nexport function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void\nexport function rendererOn<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {\n  ipcRenderer.on(name, (event, params) => {\n    listener({ event, params })\n  })\n}\n\nexport function rendererOnce(name: string, listener: LX.IpcRendererEventListener): void\nexport function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void\nexport function rendererOnce<T>(name: string, listener: LX.IpcRendererEventListenerParams<T>): void {\n  ipcRenderer.once(name, (event, params) => {\n    listener({ event, params })\n  })\n}\n\nexport const rendererOff = (name: string, listener: (...args: any[]) => any) => {\n  ipcRenderer.removeListener(name, listener)\n}\n\nexport const rendererOffAll = (name: string) => {\n  ipcRenderer.removeAllListeners(name)\n}\n"
  },
  {
    "path": "src/common/theme/colorUtils.js",
    "content": "/* eslint-disable */\n// https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js)#micro-functions-version-4\n\n/**\n * Blend color (Lighten or Darken)\n * @param {number} p 混合百分比 范围 0.0 - 1.0\n * @param {string} c0 rgb(a) color1\n * @param {string} c1 rgb(a) color2\n * @returns color\n */\nexports.RGB_Linear_Blend=(p,c0,c1)=>{\n\tvar 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+\")\"):\")\";\n\treturn\"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;\n}\n\n/**\n * Blend color (Lighten or Darken)\n * @param {number} p 混合百分比 范围 0.0 - 1.0\n * @param {string} c0 rgb(a) color1\n * @param {string} c1 rgb(a) color2\n * @returns color\n */\nexports.RGB_Log_Blend=(p,c0,c1)=>{\n\tvar 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+\")\"):\")\";\n\treturn\"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;\n}\n\n\n/**\n * Shade color (Lighten or Darken)\n * @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色，正为白色\n * @param {string} c0 rgb(a) color\n * @returns color\n */\nexports.RGB_Linear_Shade=(p,c0)=>{\n\tvar 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;\n\treturn\"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:\")\");\n}\n\n\n/**\n * Shade color (Lighten or Darken)\n * @param {number} p Shade 百分比范围为 -1.0 - 1.0 负为黑色，正为白色\n * @param {string} c0 rgb(a) color\n * @returns color\n */\nexports.RGB_Log_Shade=(p,c0)=>{\n\tvar 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;\n\treturn\"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:\")\");\n}\n\n\n/**\n * 修改透明度\n * @param {number} p 透明度 -1.0 - 1.0\n * @param {string} color\n * @returns color\n */\nexports.RGB_Alpha_Shade = (p, color) => {\n  var i = parseInt\n  var n = p < 0\n  var [r, g, b, a] = color.split(\",\")\n  r = r[3] == 'a' ? r.slice(5) : r.slice(4)\n  if (a) {\n    a = parseFloat(a)\n    a = a - (n ? (1 - a) * p : a * p)\n    a = n ? Math.max(0, a) : Math.min(1, a)\n  } else {\n    a = 1 - p\n    a = Math.min(1, a)\n  }\n  return `rgba(${i(r)}, ${i(g)}, ${i(b)}, ${a.toFixed(2)})`\n}\n"
  },
  {
    "path": "src/common/theme/createThemes.js",
    "content": "//! 更新默认主题配置后，需要执行 npm run build:theme 重新构建index.json\n\nconst fs = require('fs')\nconst path = require('path')\nconst { createThemeColors } = require('./utils')\n\nconst defaultThemes = [\n  {\n    id: 'green',\n    name: '绿意盎然',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(77, 175, 124)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#4baed5',\n      '--color-badge-tertiary': '#e7aa36',\n    },\n  },\n  {\n    id: 'blue',\n    name: '蓝田生玉',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(52, 152, 219)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#5cbf9b',\n      '--color-badge-tertiary': '#5cbf9b',\n    },\n  },\n  {\n    id: 'blue_plus',\n    name: '蛋雅深蓝',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(77, 131, 175)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-600)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': 'rgba(66.6, 150.7, 171, 1)',\n      '--color-badge-tertiary': 'rgba(54, 196, 231, 1)',\n    },\n  },\n  {\n    id: 'orange',\n    name: '橙黄橘绿',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(245, 171, 53)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#9ed458',\n      '--color-badge-tertiary': '#9ed458',\n    },\n  },\n  {\n    id: 'red',\n    name: '热情似火',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(214, 69, 65)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#dfbb6b',\n      '--color-badge-tertiary': '#dfbb6b',\n    },\n  },\n  {\n    id: 'pink',\n    name: '粉装玉琢',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(241, 130, 141)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#f5b684',\n      '--color-badge-tertiary': '#f5b684',\n    },\n  },\n  {\n    id: 'purple',\n    name: '重斤球紫',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(155, 89, 182)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#e5a39f',\n      '--color-badge-tertiary': '#e5a39f',\n    },\n  },\n  {\n    id: 'grey',\n    name: '灰常美丽',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(108, 122, 137)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#b19b9f',\n      '--color-badge-tertiary': '#b19b9f',\n    },\n  },\n  {\n    id: 'ming',\n    name: '青出于黑',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(51, 110, 123)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#6376a2',\n      '--color-badge-tertiary': '#6376a2',\n    },\n  },\n  {\n    id: 'blue2',\n    name: '清热板蓝',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(79, 98, 208)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'var(--color-primary-light-600-alpha-700)',\n      '--color-main-background': 'rgba(255, 255, 255, 1)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'none',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#b080db',\n      '--color-badge-tertiary': '#b080db',\n    },\n  },\n  {\n    id: 'black',\n    name: '黑灯瞎火',\n    isDark: true,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(150, 150, 150)',\n      font: 'rgb(229, 229, 229)',\n      '--color-app-background': 'rgba(0, 0, 0, 0)',\n      '--color-main-background': 'rgba(19, 19, 19, 0.9)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'url(./theme_images/landingMoon.png)',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary-dark-200)',\n      '--color-badge-secondary': 'var(--color-primary)',\n      '--color-badge-tertiary': 'var(--color-primary-dark-300)',\n    },\n  },\n  {\n    id: 'mid_autumn',\n    name: '月里嫦娥',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(74, 55, 82)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'rgba(255, 255, 255, 0)',\n      '--color-main-background': 'rgba(255, 255, 255, 0.9)',\n      '--color-nav-font': 'var(--color-primary-light-600)',\n      '--background-image': 'url(./theme_images/jqbg.jpg)',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': '#af9479',\n      '--color-badge-tertiary': '#af9479',\n    },\n  },\n  {\n    id: 'naruto',\n    name: '木叶之村',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(87, 144, 167)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'rgba(255, 255, 255, 0.15)',\n      '--color-main-background': 'rgba(255, 255, 255, 0.8)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'url(./theme_images/myzcbg.jpg)',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': 'var(--color-primary)',\n      '--color-badge-secondary': 'var(--color-primary-light-100)',\n      '--color-badge-tertiary': 'var(--color-primary-light-100)',\n    },\n  },\n  {\n    id: 'china_ink',\n    name: '近墨者黑',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgba(47, 47, 47, 1)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'rgba(255, 255, 255, 0)',\n      '--color-main-background': 'rgba(255, 255, 255, 0.8)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'url(./theme_images/china_ink.jpg)',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n\n      '--color-btn-hide': 'rgba(183, 212, 208, 1)',\n      '--color-btn-min': 'rgba(200, 214, 183, 1)',\n      '--color-btn-close': 'rgba(218, 195, 188, 1)',\n\n      '--color-badge-primary': 'rgba(137, 70, 70, 1)',\n      '--color-badge-secondary': 'rgba(67, 139, 65, 1)',\n      '--color-badge-tertiary': 'rgba(132, 135, 65, 1)',\n    },\n  },\n  {\n    id: 'happy_new_year',\n    name: '新年快乐',\n    isDark: false,\n    isDarkFont: false,\n    config: {\n      primary: 'rgb(192, 57, 43)',\n      font: 'rgb(33, 33, 33)',\n      '--color-app-background': 'rgba(255, 255, 255, 0.15)',\n      '--color-main-background': 'rgba(255, 255, 255, 0.8)',\n      '--color-nav-font': 'var(--color-primary)',\n      '--background-image': 'url(./theme_images/xnkl.png)',\n      '--background-image-position': 'center',\n      '--background-image-size': 'cover',\n\n      '--color-btn-hide': '#3bc2b2',\n      '--color-btn-min': '#85c43b',\n      '--color-btn-close': '#fab4a0',\n\n      '--color-badge-primary': '#7fb575',\n      '--color-badge-secondary': '#dfbb6b',\n      '--color-badge-tertiary': 'var(--color-primary-light-100)',\n    },\n  },\n]\n\nconst themes = defaultThemes.map(({ config: { primary, font, ...extInfo }, ...themeInfo }) => {\n  return {\n    ...themeInfo,\n    isCustom: false,\n    config: {\n      themeColors: createThemeColors(primary, font, themeInfo.isDark),\n      extInfo,\n    },\n  }\n})\n\nfs.writeFileSync(path.join(__dirname, 'index.json'), JSON.stringify(themes, null, 2))\n\n"
  },
  {
    "path": "src/common/theme/index.json",
    "content": "[\n  {\n    \"id\": \"green\",\n    \"name\": \"绿意盎然\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(77, 175, 124)\",\n        \"--color-primary-dark-100\": \"rgb(69,158,112)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(69, 158, 112, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(77, 175, 124, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(69, 158, 112, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(77, 175, 124, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(69, 158, 112, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(77, 175, 124, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(69, 158, 112, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(77, 175, 124, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(69, 158, 112, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(77, 175, 124, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(69, 158, 112, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(77, 175, 124, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(69, 158, 112, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(77, 175, 124, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(69, 158, 112, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(77, 175, 124, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(69, 158, 112, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(77, 175, 124, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(62,142,101)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(62, 142, 101, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(62, 142, 101, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(62, 142, 101, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(62, 142, 101, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(62, 142, 101, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(62, 142, 101, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(62, 142, 101, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(62, 142, 101, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(62, 142, 101, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(56,128,91)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(56, 128, 91, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(56, 128, 91, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(56, 128, 91, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(56, 128, 91, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(56, 128, 91, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(56, 128, 91, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(56, 128, 91, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(56, 128, 91, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(56, 128, 91, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(50,115,82)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(50, 115, 82, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(50, 115, 82, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(50, 115, 82, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(50, 115, 82, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(50, 115, 82, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(50, 115, 82, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(50, 115, 82, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(50, 115, 82, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(50, 115, 82, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(45,104,74)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(45, 104, 74, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(45, 104, 74, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(45, 104, 74, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(45, 104, 74, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(45, 104, 74, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(45, 104, 74, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(45, 104, 74, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(45, 104, 74, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(45, 104, 74, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(41,94,67)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(41, 94, 67, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(41, 94, 67, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(41, 94, 67, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(41, 94, 67, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(41, 94, 67, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(41, 94, 67, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(41, 94, 67, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(41, 94, 67, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(41, 94, 67, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(37,85,60)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(37, 85, 60, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(37, 85, 60, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(37, 85, 60, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(37, 85, 60, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(37, 85, 60, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(37, 85, 60, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(37, 85, 60, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(37, 85, 60, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(37, 85, 60, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(33,77,54)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(33, 77, 54, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(33, 77, 54, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(33, 77, 54, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(33, 77, 54, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(33, 77, 54, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(33, 77, 54, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(33, 77, 54, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(33, 77, 54, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(33, 77, 54, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(30,69,49)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(30, 69, 49, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(30, 69, 49, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(30, 69, 49, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(30, 69, 49, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(30, 69, 49, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(30, 69, 49, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(30, 69, 49, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(30, 69, 49, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(30, 69, 49, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(27,62,44)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(27, 62, 44, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(27, 62, 44, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(27, 62, 44, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(27, 62, 44, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(27, 62, 44, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(27, 62, 44, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(27, 62, 44, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(27, 62, 44, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(27, 62, 44, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(113,191,150)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(113, 191, 150, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(113, 191, 150, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(113, 191, 150, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(113, 191, 150, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(113, 191, 150, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(113, 191, 150, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(113, 191, 150, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(113, 191, 150, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(113, 191, 150, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(141,204,171)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(141, 204, 171, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(141, 204, 171, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(141, 204, 171, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(141, 204, 171, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(141, 204, 171, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(141, 204, 171, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(141, 204, 171, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(141, 204, 171, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(141, 204, 171, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(164,214,188)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(164, 214, 188, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(164, 214, 188, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(164, 214, 188, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(164, 214, 188, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(164, 214, 188, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(164, 214, 188, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(164, 214, 188, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(164, 214, 188, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(164, 214, 188, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(182,222,201)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(182, 222, 201, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(182, 222, 201, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(182, 222, 201, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(182, 222, 201, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(182, 222, 201, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(182, 222, 201, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(182, 222, 201, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(182, 222, 201, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(182, 222, 201, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(197,229,212)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(197, 229, 212, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(197, 229, 212, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(197, 229, 212, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(197, 229, 212, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(197, 229, 212, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(197, 229, 212, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(197, 229, 212, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(197, 229, 212, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(197, 229, 212, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(209,234,221)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(209, 234, 221, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(209, 234, 221, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(209, 234, 221, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(209, 234, 221, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(209, 234, 221, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(209, 234, 221, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(209, 234, 221, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(209, 234, 221, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(209, 234, 221, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(218,238,228)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(218, 238, 228, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(218, 238, 228, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(218, 238, 228, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(218, 238, 228, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(218, 238, 228, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(218, 238, 228, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(218, 238, 228, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(218, 238, 228, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(218, 238, 228, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(225,241,233)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(225, 241, 233, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(225, 241, 233, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(225, 241, 233, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(225, 241, 233, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(225, 241, 233, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(225, 241, 233, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(225, 241, 233, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(225, 241, 233, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(225, 241, 233, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(231,244,237)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(231, 244, 237, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(231, 244, 237, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(231, 244, 237, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(231, 244, 237, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(231, 244, 237, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(231, 244, 237, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(231, 244, 237, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(231, 244, 237, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(231, 244, 237, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(77, 175, 124)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#4baed5\",\n        \"--color-badge-tertiary\": \"#e7aa36\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue\",\n    \"name\": \"蓝田生玉\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(52, 152, 219)\",\n        \"--color-primary-dark-100\": \"rgb(47,137,197)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(47, 137, 197, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(52, 152, 219, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(47, 137, 197, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(52, 152, 219, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(47, 137, 197, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(52, 152, 219, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(47, 137, 197, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(52, 152, 219, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(47, 137, 197, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(52, 152, 219, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(47, 137, 197, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(52, 152, 219, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(47, 137, 197, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(52, 152, 219, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(47, 137, 197, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(52, 152, 219, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(47, 137, 197, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(52, 152, 219, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(42,123,177)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(42, 123, 177, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(42, 123, 177, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(42, 123, 177, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(42, 123, 177, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(42, 123, 177, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(42, 123, 177, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(42, 123, 177, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(42, 123, 177, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(42, 123, 177, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(38,111,159)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(38, 111, 159, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(38, 111, 159, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(38, 111, 159, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(38, 111, 159, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(38, 111, 159, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(38, 111, 159, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(38, 111, 159, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(38, 111, 159, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(38, 111, 159, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(34,100,143)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(34, 100, 143, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(34, 100, 143, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(34, 100, 143, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(34, 100, 143, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(34, 100, 143, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(34, 100, 143, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(34, 100, 143, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(34, 100, 143, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(34, 100, 143, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(31,90,129)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(31, 90, 129, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(31, 90, 129, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(31, 90, 129, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(31, 90, 129, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(31, 90, 129, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(31, 90, 129, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(31, 90, 129, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(31, 90, 129, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(31, 90, 129, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(28,81,116)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(28, 81, 116, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(28, 81, 116, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(28, 81, 116, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(28, 81, 116, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(28, 81, 116, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(28, 81, 116, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(28, 81, 116, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(28, 81, 116, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(28, 81, 116, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(25,73,104)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(25, 73, 104, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(25, 73, 104, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(25, 73, 104, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(25, 73, 104, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(25, 73, 104, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(25, 73, 104, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(25, 73, 104, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(25, 73, 104, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(25, 73, 104, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(23,66,94)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(23, 66, 94, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(23, 66, 94, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(23, 66, 94, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(23, 66, 94, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(23, 66, 94, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(23, 66, 94, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(23, 66, 94, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(23, 66, 94, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(23, 66, 94, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(21,59,85)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(21, 59, 85, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(21, 59, 85, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(21, 59, 85, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(21, 59, 85, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(21, 59, 85, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(21, 59, 85, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(21, 59, 85, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(21, 59, 85, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(21, 59, 85, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(19,53,77)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(19, 53, 77, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(19, 53, 77, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(19, 53, 77, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(19, 53, 77, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(19, 53, 77, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(19, 53, 77, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(19, 53, 77, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(19, 53, 77, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(19, 53, 77, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(93,173,226)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(93, 173, 226, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(93, 173, 226, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(93, 173, 226, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(93, 173, 226, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(93, 173, 226, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(93, 173, 226, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(93, 173, 226, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(93, 173, 226, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(93, 173, 226, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(125,189,232)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(125, 189, 232, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(125, 189, 232, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(125, 189, 232, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(125, 189, 232, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(125, 189, 232, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(125, 189, 232, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(125, 189, 232, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(125, 189, 232, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(125, 189, 232, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(151,202,237)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(151, 202, 237, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(151, 202, 237, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(151, 202, 237, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(151, 202, 237, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(151, 202, 237, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(151, 202, 237, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(151, 202, 237, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(151, 202, 237, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(151, 202, 237, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(172,213,241)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(172, 213, 241, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(172, 213, 241, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(172, 213, 241, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(172, 213, 241, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(172, 213, 241, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(172, 213, 241, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(172, 213, 241, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(172, 213, 241, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(172, 213, 241, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(189,221,244)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(189, 221, 244, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(189, 221, 244, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(189, 221, 244, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(189, 221, 244, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(189, 221, 244, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(189, 221, 244, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(189, 221, 244, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(189, 221, 244, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(189, 221, 244, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(202,228,246)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(202, 228, 246, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(202, 228, 246, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(202, 228, 246, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(202, 228, 246, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(202, 228, 246, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(202, 228, 246, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(202, 228, 246, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(202, 228, 246, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(202, 228, 246, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(213,233,248)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(213, 233, 248, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(213, 233, 248, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(213, 233, 248, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(213, 233, 248, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(213, 233, 248, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(213, 233, 248, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(213, 233, 248, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(213, 233, 248, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(213, 233, 248, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(221,237,249)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(221, 237, 249, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(221, 237, 249, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(221, 237, 249, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(221, 237, 249, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(221, 237, 249, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(221, 237, 249, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(221, 237, 249, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(221, 237, 249, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(221, 237, 249, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(228,241,250)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(228, 241, 250, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(228, 241, 250, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(228, 241, 250, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(228, 241, 250, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(228, 241, 250, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(228, 241, 250, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(228, 241, 250, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(228, 241, 250, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(228, 241, 250, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(52, 152, 219)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#5cbf9b\",\n        \"--color-badge-tertiary\": \"#5cbf9b\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue_plus\",\n    \"name\": \"蛋雅深蓝\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(77, 131, 175)\",\n        \"--color-primary-dark-100\": \"rgb(69,118,158)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(69, 118, 158, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(77, 131, 175, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(69, 118, 158, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(77, 131, 175, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(69, 118, 158, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(77, 131, 175, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(69, 118, 158, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(77, 131, 175, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(69, 118, 158, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(77, 131, 175, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(69, 118, 158, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(77, 131, 175, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(69, 118, 158, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(77, 131, 175, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(69, 118, 158, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(77, 131, 175, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(69, 118, 158, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(77, 131, 175, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(62,106,142)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(62, 106, 142, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(62, 106, 142, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(62, 106, 142, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(62, 106, 142, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(62, 106, 142, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(62, 106, 142, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(62, 106, 142, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(62, 106, 142, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(62, 106, 142, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(56,95,128)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(56, 95, 128, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(56, 95, 128, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(56, 95, 128, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(56, 95, 128, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(56, 95, 128, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(56, 95, 128, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(56, 95, 128, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(56, 95, 128, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(56, 95, 128, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(50,86,115)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(50, 86, 115, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(50, 86, 115, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(50, 86, 115, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(50, 86, 115, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(50, 86, 115, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(50, 86, 115, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(50, 86, 115, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(50, 86, 115, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(50, 86, 115, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(45,77,104)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(45, 77, 104, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(45, 77, 104, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(45, 77, 104, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(45, 77, 104, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(45, 77, 104, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(45, 77, 104, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(45, 77, 104, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(45, 77, 104, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(45, 77, 104, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(41,69,94)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(41, 69, 94, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(41, 69, 94, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(41, 69, 94, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(41, 69, 94, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(41, 69, 94, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(41, 69, 94, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(41, 69, 94, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(41, 69, 94, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(41, 69, 94, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(37,62,85)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(37, 62, 85, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(37, 62, 85, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(37, 62, 85, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(37, 62, 85, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(37, 62, 85, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(37, 62, 85, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(37, 62, 85, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(37, 62, 85, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(37, 62, 85, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(33,56,77)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(33, 56, 77, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(33, 56, 77, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(33, 56, 77, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(33, 56, 77, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(33, 56, 77, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(33, 56, 77, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(33, 56, 77, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(33, 56, 77, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(33, 56, 77, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(30,50,69)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(30, 50, 69, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(30, 50, 69, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(30, 50, 69, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(30, 50, 69, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(30, 50, 69, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(30, 50, 69, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(30, 50, 69, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(30, 50, 69, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(30, 50, 69, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(27,45,62)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(27, 45, 62, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(27, 45, 62, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(27, 45, 62, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(27, 45, 62, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(27, 45, 62, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(27, 45, 62, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(27, 45, 62, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(27, 45, 62, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(27, 45, 62, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(113,156,191)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(113, 156, 191, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(113, 156, 191, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(113, 156, 191, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(113, 156, 191, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(113, 156, 191, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(113, 156, 191, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(113, 156, 191, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(113, 156, 191, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(113, 156, 191, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(141,176,204)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(141, 176, 204, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(141, 176, 204, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(141, 176, 204, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(141, 176, 204, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(141, 176, 204, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(141, 176, 204, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(141, 176, 204, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(141, 176, 204, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(141, 176, 204, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(164,192,214)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(164, 192, 214, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(164, 192, 214, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(164, 192, 214, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(164, 192, 214, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(164, 192, 214, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(164, 192, 214, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(164, 192, 214, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(164, 192, 214, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(164, 192, 214, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(182,205,222)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(182, 205, 222, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(182, 205, 222, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(182, 205, 222, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(182, 205, 222, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(182, 205, 222, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(182, 205, 222, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(182, 205, 222, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(182, 205, 222, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(182, 205, 222, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(197,215,229)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(197, 215, 229, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(197, 215, 229, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(197, 215, 229, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(197, 215, 229, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(197, 215, 229, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(197, 215, 229, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(197, 215, 229, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(197, 215, 229, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(197, 215, 229, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(209,223,234)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(209, 223, 234, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(209, 223, 234, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(209, 223, 234, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(209, 223, 234, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(209, 223, 234, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(209, 223, 234, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(209, 223, 234, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(209, 223, 234, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(209, 223, 234, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(218,229,238)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(218, 229, 238, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(218, 229, 238, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(218, 229, 238, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(218, 229, 238, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(218, 229, 238, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(218, 229, 238, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(218, 229, 238, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(218, 229, 238, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(218, 229, 238, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(225,234,241)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(225, 234, 241, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(225, 234, 241, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(225, 234, 241, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(225, 234, 241, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(225, 234, 241, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(225, 234, 241, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(225, 234, 241, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(225, 234, 241, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(225, 234, 241, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(231,238,244)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(231, 238, 244, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(231, 238, 244, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(231, 238, 244, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(231, 238, 244, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(231, 238, 244, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(231, 238, 244, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(231, 238, 244, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(231, 238, 244, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(231, 238, 244, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(77, 131, 175)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-600)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"rgba(66.6, 150.7, 171, 1)\",\n        \"--color-badge-tertiary\": \"rgba(54, 196, 231, 1)\"\n      }\n    }\n  },\n  {\n    \"id\": \"orange\",\n    \"name\": \"橙黄橘绿\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(245, 171, 53)\",\n        \"--color-primary-dark-100\": \"rgb(221,154,48)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(221, 154, 48, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(245, 171, 53, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(221, 154, 48, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(245, 171, 53, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(221, 154, 48, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(245, 171, 53, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(221, 154, 48, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(245, 171, 53, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(221, 154, 48, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(245, 171, 53, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(221, 154, 48, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(245, 171, 53, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(221, 154, 48, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(245, 171, 53, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(221, 154, 48, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(245, 171, 53, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(221, 154, 48, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(245, 171, 53, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(199,139,43)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(199, 139, 43, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(199, 139, 43, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(199, 139, 43, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(199, 139, 43, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(199, 139, 43, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(199, 139, 43, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(199, 139, 43, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(199, 139, 43, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(199, 139, 43, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(179,125,39)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(179, 125, 39, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(179, 125, 39, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(179, 125, 39, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(179, 125, 39, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(179, 125, 39, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(179, 125, 39, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(179, 125, 39, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(179, 125, 39, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(179, 125, 39, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(161,113,35)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(161, 113, 35, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(161, 113, 35, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(161, 113, 35, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(161, 113, 35, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(161, 113, 35, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(161, 113, 35, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(161, 113, 35, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(161, 113, 35, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(161, 113, 35, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(145,102,32)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(145, 102, 32, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(145, 102, 32, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(145, 102, 32, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(145, 102, 32, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(145, 102, 32, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(145, 102, 32, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(145, 102, 32, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(145, 102, 32, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(145, 102, 32, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(131,92,29)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(131, 92, 29, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(131, 92, 29, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(131, 92, 29, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(131, 92, 29, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(131, 92, 29, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(131, 92, 29, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(131, 92, 29, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(131, 92, 29, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(131, 92, 29, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(118,83,26)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(118, 83, 26, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(118, 83, 26, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(118, 83, 26, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(118, 83, 26, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(118, 83, 26, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(118, 83, 26, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(118, 83, 26, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(118, 83, 26, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(118, 83, 26, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(106,75,23)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(106, 75, 23, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(106, 75, 23, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(106, 75, 23, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(106, 75, 23, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(106, 75, 23, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(106, 75, 23, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(106, 75, 23, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(106, 75, 23, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(106, 75, 23, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(95,68,21)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(95, 68, 21, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(95, 68, 21, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(95, 68, 21, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(95, 68, 21, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(95, 68, 21, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(95, 68, 21, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(95, 68, 21, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(95, 68, 21, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(95, 68, 21, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(86,61,19)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(86, 61, 19, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(86, 61, 19, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(86, 61, 19, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(86, 61, 19, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(86, 61, 19, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(86, 61, 19, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(86, 61, 19, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(86, 61, 19, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(86, 61, 19, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(247,188,93)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(247, 188, 93, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(247, 188, 93, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(247, 188, 93, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(247, 188, 93, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(247, 188, 93, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(247, 188, 93, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(247, 188, 93, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(247, 188, 93, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(247, 188, 93, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(249,201,125)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(249, 201, 125, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(249, 201, 125, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(249, 201, 125, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(249, 201, 125, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(249, 201, 125, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(249, 201, 125, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(249, 201, 125, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(249, 201, 125, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(249, 201, 125, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(250,212,151)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(250, 212, 151, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(250, 212, 151, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(250, 212, 151, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(250, 212, 151, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(250, 212, 151, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(250, 212, 151, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(250, 212, 151, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(250, 212, 151, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(250, 212, 151, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(251,221,172)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(251, 221, 172, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(251, 221, 172, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(251, 221, 172, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(251, 221, 172, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(251, 221, 172, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(251, 221, 172, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(251, 221, 172, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(251, 221, 172, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(251, 221, 172, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(252,228,189)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(252, 228, 189, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(252, 228, 189, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(252, 228, 189, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(252, 228, 189, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(252, 228, 189, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(252, 228, 189, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(252, 228, 189, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(252, 228, 189, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(252, 228, 189, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(253,233,202)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(253, 233, 202, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(253, 233, 202, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(253, 233, 202, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(253, 233, 202, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(253, 233, 202, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(253, 233, 202, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(253, 233, 202, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(253, 233, 202, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(253, 233, 202, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(253,237,213)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(253, 237, 213, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(253, 237, 213, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(253, 237, 213, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(253, 237, 213, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(253, 237, 213, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(253, 237, 213, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(253, 237, 213, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(253, 237, 213, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(253, 237, 213, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(253,241,221)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(253, 241, 221, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(253, 241, 221, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(253, 241, 221, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(253, 241, 221, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(253, 241, 221, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(253, 241, 221, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(253, 241, 221, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(253, 241, 221, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(253, 241, 221, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(253,244,228)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(253, 244, 228, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(253, 244, 228, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(253, 244, 228, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(253, 244, 228, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(253, 244, 228, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(253, 244, 228, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(253, 244, 228, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(253, 244, 228, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(253, 244, 228, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(245, 171, 53)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#9ed458\",\n        \"--color-badge-tertiary\": \"#9ed458\"\n      }\n    }\n  },\n  {\n    \"id\": \"red\",\n    \"name\": \"热情似火\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(214, 69, 65)\",\n        \"--color-primary-dark-100\": \"rgb(193,62,59)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(193, 62, 59, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(214, 69, 65, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(193, 62, 59, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(214, 69, 65, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(193, 62, 59, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(214, 69, 65, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(193, 62, 59, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(214, 69, 65, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(193, 62, 59, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(214, 69, 65, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(193, 62, 59, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(214, 69, 65, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(193, 62, 59, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(214, 69, 65, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(193, 62, 59, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(214, 69, 65, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(193, 62, 59, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(214, 69, 65, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(174,56,53)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(174, 56, 53, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(174, 56, 53, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(174, 56, 53, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(174, 56, 53, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(174, 56, 53, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(174, 56, 53, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(174, 56, 53, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(174, 56, 53, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(174, 56, 53, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(157,50,48)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(157, 50, 48, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(157, 50, 48, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(157, 50, 48, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(157, 50, 48, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(157, 50, 48, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(157, 50, 48, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(157, 50, 48, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(157, 50, 48, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(157, 50, 48, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(141,45,43)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(141, 45, 43, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(141, 45, 43, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(141, 45, 43, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(141, 45, 43, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(141, 45, 43, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(141, 45, 43, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(141, 45, 43, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(141, 45, 43, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(141, 45, 43, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(127,41,39)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(127, 41, 39, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(127, 41, 39, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(127, 41, 39, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(127, 41, 39, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(127, 41, 39, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(127, 41, 39, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(127, 41, 39, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(127, 41, 39, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(127, 41, 39, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(114,37,35)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(114, 37, 35, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(114, 37, 35, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(114, 37, 35, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(114, 37, 35, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(114, 37, 35, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(114, 37, 35, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(114, 37, 35, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(114, 37, 35, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(114, 37, 35, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(103,33,32)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(103, 33, 32, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(103, 33, 32, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(103, 33, 32, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(103, 33, 32, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(103, 33, 32, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(103, 33, 32, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(103, 33, 32, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(103, 33, 32, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(103, 33, 32, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(93,30,29)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(93, 30, 29, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(93, 30, 29, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(93, 30, 29, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(93, 30, 29, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(93, 30, 29, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(93, 30, 29, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(93, 30, 29, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(93, 30, 29, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(93, 30, 29, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(84,27,26)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(84, 27, 26, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(84, 27, 26, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(84, 27, 26, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(84, 27, 26, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(84, 27, 26, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(84, 27, 26, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(84, 27, 26, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(84, 27, 26, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(84, 27, 26, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(76,24,23)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(76, 24, 23, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(76, 24, 23, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(76, 24, 23, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(76, 24, 23, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(76, 24, 23, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(76, 24, 23, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(76, 24, 23, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(76, 24, 23, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(76, 24, 23, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(222,106,103)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(222, 106, 103, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(222, 106, 103, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(222, 106, 103, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(222, 106, 103, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(222, 106, 103, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(222, 106, 103, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(222, 106, 103, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(222, 106, 103, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(222, 106, 103, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(229,136,133)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(229, 136, 133, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(229, 136, 133, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(229, 136, 133, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(229, 136, 133, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(229, 136, 133, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(229, 136, 133, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(229, 136, 133, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(229, 136, 133, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(229, 136, 133, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(234,160,157)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(234, 160, 157, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(234, 160, 157, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(234, 160, 157, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(234, 160, 157, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(234, 160, 157, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(234, 160, 157, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(234, 160, 157, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(234, 160, 157, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(234, 160, 157, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(238,179,177)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(238, 179, 177, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(238, 179, 177, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(238, 179, 177, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(238, 179, 177, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(238, 179, 177, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(238, 179, 177, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(238, 179, 177, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(238, 179, 177, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(238, 179, 177, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(241,194,193)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(241, 194, 193, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(241, 194, 193, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(241, 194, 193, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(241, 194, 193, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(241, 194, 193, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(241, 194, 193, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(241, 194, 193, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(241, 194, 193, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(241, 194, 193, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(244,206,205)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(244, 206, 205, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(244, 206, 205, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(244, 206, 205, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(244, 206, 205, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(244, 206, 205, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(244, 206, 205, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(244, 206, 205, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(244, 206, 205, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(244, 206, 205, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(246,216,215)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(246, 216, 215, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(246, 216, 215, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(246, 216, 215, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(246, 216, 215, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(246, 216, 215, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(246, 216, 215, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(246, 216, 215, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(246, 216, 215, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(246, 216, 215, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(248,224,223)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(248, 224, 223, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(248, 224, 223, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(248, 224, 223, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(248, 224, 223, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(248, 224, 223, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(248, 224, 223, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(248, 224, 223, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(248, 224, 223, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(248, 224, 223, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(249,230,229)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(249, 230, 229, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(249, 230, 229, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(249, 230, 229, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(249, 230, 229, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(249, 230, 229, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(249, 230, 229, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(249, 230, 229, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(249, 230, 229, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(249, 230, 229, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(214, 69, 65)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#dfbb6b\",\n        \"--color-badge-tertiary\": \"#dfbb6b\"\n      }\n    }\n  },\n  {\n    \"id\": \"pink\",\n    \"name\": \"粉装玉琢\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(241, 130, 141)\",\n        \"--color-primary-dark-100\": \"rgb(217,117,127)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(217, 117, 127, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(241, 130, 141, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(217, 117, 127, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(241, 130, 141, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(217, 117, 127, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(241, 130, 141, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(217, 117, 127, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(241, 130, 141, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(217, 117, 127, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(241, 130, 141, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(217, 117, 127, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(241, 130, 141, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(217, 117, 127, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(241, 130, 141, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(217, 117, 127, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(241, 130, 141, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(217, 117, 127, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(241, 130, 141, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(195,105,114)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(195, 105, 114, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(195, 105, 114, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(195, 105, 114, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(195, 105, 114, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(195, 105, 114, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(195, 105, 114, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(195, 105, 114, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(195, 105, 114, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(195, 105, 114, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(176,95,103)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(176, 95, 103, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(176, 95, 103, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(176, 95, 103, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(176, 95, 103, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(176, 95, 103, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(176, 95, 103, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(176, 95, 103, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(176, 95, 103, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(176, 95, 103, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(158,86,93)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(158, 86, 93, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(158, 86, 93, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(158, 86, 93, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(158, 86, 93, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(158, 86, 93, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(158, 86, 93, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(158, 86, 93, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(158, 86, 93, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(158, 86, 93, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(142,77,84)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(142, 77, 84, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(142, 77, 84, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(142, 77, 84, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(142, 77, 84, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(142, 77, 84, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(142, 77, 84, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(142, 77, 84, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(142, 77, 84, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(142, 77, 84, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(128,69,76)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(128, 69, 76, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(128, 69, 76, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(128, 69, 76, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(128, 69, 76, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(128, 69, 76, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(128, 69, 76, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(128, 69, 76, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(128, 69, 76, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(128, 69, 76, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(115,62,68)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(115, 62, 68, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(115, 62, 68, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(115, 62, 68, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(115, 62, 68, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(115, 62, 68, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(115, 62, 68, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(115, 62, 68, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(115, 62, 68, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(115, 62, 68, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(104,56,61)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(104, 56, 61, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(104, 56, 61, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(104, 56, 61, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(104, 56, 61, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(104, 56, 61, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(104, 56, 61, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(104, 56, 61, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(104, 56, 61, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(104, 56, 61, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(94,50,55)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(94, 50, 55, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(94, 50, 55, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(94, 50, 55, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(94, 50, 55, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(94, 50, 55, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(94, 50, 55, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(94, 50, 55, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(94, 50, 55, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(94, 50, 55, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(85,45,50)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(85, 45, 50, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(85, 45, 50, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(85, 45, 50, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(85, 45, 50, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(85, 45, 50, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(85, 45, 50, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(85, 45, 50, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(85, 45, 50, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(85, 45, 50, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(244,155,164)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(244, 155, 164, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(244, 155, 164, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(244, 155, 164, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(244, 155, 164, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(244, 155, 164, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(244, 155, 164, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(244, 155, 164, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(244, 155, 164, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(244, 155, 164, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(246,175,182)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(246, 175, 182, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(246, 175, 182, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(246, 175, 182, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(246, 175, 182, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(246, 175, 182, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(246, 175, 182, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(246, 175, 182, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(246, 175, 182, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(246, 175, 182, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(248,191,197)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(248, 191, 197, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(248, 191, 197, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(248, 191, 197, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(248, 191, 197, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(248, 191, 197, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(248, 191, 197, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(248, 191, 197, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(248, 191, 197, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(248, 191, 197, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(249,204,209)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(249, 204, 209, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(249, 204, 209, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(249, 204, 209, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(249, 204, 209, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(249, 204, 209, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(249, 204, 209, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(249, 204, 209, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(249, 204, 209, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(249, 204, 209, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(250,214,218)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(250, 214, 218, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(250, 214, 218, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(250, 214, 218, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(250, 214, 218, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(250, 214, 218, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(250, 214, 218, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(250, 214, 218, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(250, 214, 218, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(250, 214, 218, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(251,222,225)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(251, 222, 225, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(251, 222, 225, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(251, 222, 225, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(251, 222, 225, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(251, 222, 225, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(251, 222, 225, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(251, 222, 225, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(251, 222, 225, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(251, 222, 225, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(252,229,231)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(252, 229, 231, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(252, 229, 231, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(252, 229, 231, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(252, 229, 231, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(252, 229, 231, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(252, 229, 231, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(252, 229, 231, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(252, 229, 231, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(252, 229, 231, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(253,234,236)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(253, 234, 236, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(253, 234, 236, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(253, 234, 236, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(253, 234, 236, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(253, 234, 236, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(253, 234, 236, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(253, 234, 236, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(253, 234, 236, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(253, 234, 236, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(253,238,240)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(253, 238, 240, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(253, 238, 240, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(253, 238, 240, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(253, 238, 240, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(253, 238, 240, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(253, 238, 240, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(253, 238, 240, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(253, 238, 240, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(253, 238, 240, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(241, 130, 141)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#f5b684\",\n        \"--color-badge-tertiary\": \"#f5b684\"\n      }\n    }\n  },\n  {\n    \"id\": \"purple\",\n    \"name\": \"重斤球紫\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(155, 89, 182)\",\n        \"--color-primary-dark-100\": \"rgb(140,80,164)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(140, 80, 164, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(155, 89, 182, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(140, 80, 164, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(155, 89, 182, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(140, 80, 164, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(155, 89, 182, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(140, 80, 164, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(155, 89, 182, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(140, 80, 164, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(155, 89, 182, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(140, 80, 164, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(155, 89, 182, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(140, 80, 164, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(155, 89, 182, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(140, 80, 164, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(155, 89, 182, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(140, 80, 164, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(155, 89, 182, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(126,72,148)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(126, 72, 148, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(126, 72, 148, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(126, 72, 148, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(126, 72, 148, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(126, 72, 148, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(126, 72, 148, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(126, 72, 148, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(126, 72, 148, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(126, 72, 148, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(113,65,133)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(113, 65, 133, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(113, 65, 133, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(113, 65, 133, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(113, 65, 133, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(113, 65, 133, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(113, 65, 133, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(113, 65, 133, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(113, 65, 133, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(113, 65, 133, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(102,59,120)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(102, 59, 120, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(102, 59, 120, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(102, 59, 120, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(102, 59, 120, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(102, 59, 120, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(102, 59, 120, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(102, 59, 120, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(102, 59, 120, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(102, 59, 120, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(92,53,108)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(92, 53, 108, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(92, 53, 108, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(92, 53, 108, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(92, 53, 108, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(92, 53, 108, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(92, 53, 108, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(92, 53, 108, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(92, 53, 108, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(92, 53, 108, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(83,48,97)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(83, 48, 97, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(83, 48, 97, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(83, 48, 97, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(83, 48, 97, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(83, 48, 97, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(83, 48, 97, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(83, 48, 97, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(83, 48, 97, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(83, 48, 97, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(75,43,87)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(75, 43, 87, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(75, 43, 87, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(75, 43, 87, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(75, 43, 87, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(75, 43, 87, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(75, 43, 87, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(75, 43, 87, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(75, 43, 87, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(75, 43, 87, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(68,39,78)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(68, 39, 78, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(68, 39, 78, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(68, 39, 78, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(68, 39, 78, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(68, 39, 78, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(68, 39, 78, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(68, 39, 78, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(68, 39, 78, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(68, 39, 78, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(61,35,70)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(61, 35, 70, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(61, 35, 70, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(61, 35, 70, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(61, 35, 70, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(61, 35, 70, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(61, 35, 70, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(61, 35, 70, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(61, 35, 70, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(61, 35, 70, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(55,32,63)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(55, 32, 63, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(55, 32, 63, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(55, 32, 63, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(55, 32, 63, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(55, 32, 63, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(55, 32, 63, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(55, 32, 63, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(55, 32, 63, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(55, 32, 63, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(175,122,197)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(175, 122, 197, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(175, 122, 197, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(175, 122, 197, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(175, 122, 197, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(175, 122, 197, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(175, 122, 197, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(175, 122, 197, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(175, 122, 197, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(175, 122, 197, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(191,149,209)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(191, 149, 209, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(191, 149, 209, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(191, 149, 209, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(191, 149, 209, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(191, 149, 209, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(191, 149, 209, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(191, 149, 209, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(191, 149, 209, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(191, 149, 209, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(204,170,218)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(204, 170, 218, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(204, 170, 218, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(204, 170, 218, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(204, 170, 218, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(204, 170, 218, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(204, 170, 218, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(204, 170, 218, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(204, 170, 218, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(204, 170, 218, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(214,187,225)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(214, 187, 225, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(214, 187, 225, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(214, 187, 225, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(214, 187, 225, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(214, 187, 225, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(214, 187, 225, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(214, 187, 225, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(214, 187, 225, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(214, 187, 225, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(222,201,231)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(222, 201, 231, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(222, 201, 231, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(222, 201, 231, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(222, 201, 231, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(222, 201, 231, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(222, 201, 231, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(222, 201, 231, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(222, 201, 231, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(222, 201, 231, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(229,212,236)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(229, 212, 236, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(229, 212, 236, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(229, 212, 236, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(229, 212, 236, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(229, 212, 236, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(229, 212, 236, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(229, 212, 236, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(229, 212, 236, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(229, 212, 236, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(234,221,240)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(234, 221, 240, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(234, 221, 240, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(234, 221, 240, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(234, 221, 240, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(234, 221, 240, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(234, 221, 240, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(234, 221, 240, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(234, 221, 240, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(234, 221, 240, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(238,228,243)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(238, 228, 243, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(238, 228, 243, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(238, 228, 243, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(238, 228, 243, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(238, 228, 243, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(238, 228, 243, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(238, 228, 243, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(238, 228, 243, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(238, 228, 243, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(241,233,245)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(241, 233, 245, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(241, 233, 245, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(241, 233, 245, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(241, 233, 245, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(241, 233, 245, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(241, 233, 245, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(241, 233, 245, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(241, 233, 245, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(241, 233, 245, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(155, 89, 182)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#e5a39f\",\n        \"--color-badge-tertiary\": \"#e5a39f\"\n      }\n    }\n  },\n  {\n    \"id\": \"grey\",\n    \"name\": \"灰常美丽\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(108, 122, 137)\",\n        \"--color-primary-dark-100\": \"rgb(97,110,123)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(97, 110, 123, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(108, 122, 137, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(97, 110, 123, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(108, 122, 137, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(97, 110, 123, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(108, 122, 137, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(97, 110, 123, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(108, 122, 137, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(97, 110, 123, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(108, 122, 137, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(97, 110, 123, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(108, 122, 137, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(97, 110, 123, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(108, 122, 137, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(97, 110, 123, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(108, 122, 137, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(97, 110, 123, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(108, 122, 137, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(87,99,111)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(87, 99, 111, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(87, 99, 111, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(87, 99, 111, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(87, 99, 111, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(87, 99, 111, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(87, 99, 111, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(87, 99, 111, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(87, 99, 111, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(87, 99, 111, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(78,89,100)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(78, 89, 100, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(78, 89, 100, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(78, 89, 100, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(78, 89, 100, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(78, 89, 100, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(78, 89, 100, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(78, 89, 100, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(78, 89, 100, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(78, 89, 100, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(70,80,90)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(70, 80, 90, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(70, 80, 90, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(70, 80, 90, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(70, 80, 90, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(70, 80, 90, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(70, 80, 90, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(70, 80, 90, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(70, 80, 90, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(70, 80, 90, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(63,72,81)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(63, 72, 81, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(63, 72, 81, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(63, 72, 81, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(63, 72, 81, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(63, 72, 81, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(63, 72, 81, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(63, 72, 81, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(63, 72, 81, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(63, 72, 81, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(57,65,73)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(57, 65, 73, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(57, 65, 73, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(57, 65, 73, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(57, 65, 73, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(57, 65, 73, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(57, 65, 73, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(57, 65, 73, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(57, 65, 73, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(57, 65, 73, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(51,59,66)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(51, 59, 66, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(51, 59, 66, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(51, 59, 66, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(51, 59, 66, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(51, 59, 66, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(51, 59, 66, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(51, 59, 66, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(51, 59, 66, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(51, 59, 66, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(46,53,59)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(46, 53, 59, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(46, 53, 59, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(46, 53, 59, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(46, 53, 59, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(46, 53, 59, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(46, 53, 59, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(46, 53, 59, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(46, 53, 59, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(46, 53, 59, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(41,48,53)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(41, 48, 53, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(41, 48, 53, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(41, 48, 53, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(41, 48, 53, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(41, 48, 53, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(41, 48, 53, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(41, 48, 53, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(41, 48, 53, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(41, 48, 53, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(37,43,48)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(37, 43, 48, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(37, 43, 48, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(37, 43, 48, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(37, 43, 48, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(37, 43, 48, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(37, 43, 48, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(37, 43, 48, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(37, 43, 48, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(37, 43, 48, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(137,149,161)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(137, 149, 161, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(137, 149, 161, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(137, 149, 161, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(137, 149, 161, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(137, 149, 161, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(137, 149, 161, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(137, 149, 161, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(137, 149, 161, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(137, 149, 161, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(161,170,180)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(161, 170, 180, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(161, 170, 180, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(161, 170, 180, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(161, 170, 180, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(161, 170, 180, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(161, 170, 180, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(161, 170, 180, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(161, 170, 180, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(161, 170, 180, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(180,187,195)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(180, 187, 195, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(180, 187, 195, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(180, 187, 195, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(180, 187, 195, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(180, 187, 195, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(180, 187, 195, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(180, 187, 195, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(180, 187, 195, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(180, 187, 195, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(195,201,207)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(195, 201, 207, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(195, 201, 207, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(195, 201, 207, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(195, 201, 207, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(195, 201, 207, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(195, 201, 207, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(195, 201, 207, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(195, 201, 207, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(195, 201, 207, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(207,212,217)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(207, 212, 217, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(207, 212, 217, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(207, 212, 217, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(207, 212, 217, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(207, 212, 217, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(207, 212, 217, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(207, 212, 217, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(207, 212, 217, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(207, 212, 217, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(217,221,225)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(217, 221, 225, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(217, 221, 225, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(217, 221, 225, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(217, 221, 225, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(217, 221, 225, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(217, 221, 225, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(217, 221, 225, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(217, 221, 225, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(217, 221, 225, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(225,228,231)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(225, 228, 231, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(225, 228, 231, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(225, 228, 231, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(225, 228, 231, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(225, 228, 231, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(225, 228, 231, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(225, 228, 231, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(225, 228, 231, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(225, 228, 231, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(231,233,236)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(231, 233, 236, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(231, 233, 236, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(231, 233, 236, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(231, 233, 236, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(231, 233, 236, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(231, 233, 236, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(231, 233, 236, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(231, 233, 236, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(231, 233, 236, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(236,237,240)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(236, 237, 240, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(236, 237, 240, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(236, 237, 240, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(236, 237, 240, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(236, 237, 240, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(236, 237, 240, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(236, 237, 240, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(236, 237, 240, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(236, 237, 240, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(108, 122, 137)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#b19b9f\",\n        \"--color-badge-tertiary\": \"#b19b9f\"\n      }\n    }\n  },\n  {\n    \"id\": \"ming\",\n    \"name\": \"青出于黑\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(51, 110, 123)\",\n        \"--color-primary-dark-100\": \"rgb(46,99,111)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(46, 99, 111, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(51, 110, 123, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(46, 99, 111, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(51, 110, 123, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(46, 99, 111, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(51, 110, 123, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(46, 99, 111, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(51, 110, 123, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(46, 99, 111, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(51, 110, 123, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(46, 99, 111, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(51, 110, 123, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(46, 99, 111, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(51, 110, 123, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(46, 99, 111, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(51, 110, 123, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(46, 99, 111, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(51, 110, 123, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(41,89,100)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(41, 89, 100, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(41, 89, 100, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(41, 89, 100, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(41, 89, 100, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(41, 89, 100, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(41, 89, 100, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(41, 89, 100, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(41, 89, 100, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(41, 89, 100, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(37,80,90)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(37, 80, 90, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(37, 80, 90, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(37, 80, 90, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(37, 80, 90, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(37, 80, 90, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(37, 80, 90, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(37, 80, 90, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(37, 80, 90, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(37, 80, 90, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(33,72,81)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(33, 72, 81, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(33, 72, 81, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(33, 72, 81, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(33, 72, 81, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(33, 72, 81, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(33, 72, 81, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(33, 72, 81, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(33, 72, 81, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(33, 72, 81, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(30,65,73)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(30, 65, 73, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(30, 65, 73, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(30, 65, 73, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(30, 65, 73, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(30, 65, 73, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(30, 65, 73, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(30, 65, 73, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(30, 65, 73, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(30, 65, 73, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(27,59,66)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(27, 59, 66, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(27, 59, 66, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(27, 59, 66, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(27, 59, 66, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(27, 59, 66, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(27, 59, 66, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(27, 59, 66, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(27, 59, 66, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(27, 59, 66, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(24,53,59)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(24, 53, 59, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(24, 53, 59, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(24, 53, 59, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(24, 53, 59, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(24, 53, 59, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(24, 53, 59, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(24, 53, 59, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(24, 53, 59, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(24, 53, 59, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(22,48,53)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(22, 48, 53, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(22, 48, 53, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(22, 48, 53, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(22, 48, 53, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(22, 48, 53, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(22, 48, 53, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(22, 48, 53, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(22, 48, 53, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(22, 48, 53, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(20,43,48)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(20, 43, 48, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(20, 43, 48, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(20, 43, 48, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(20, 43, 48, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(20, 43, 48, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(20, 43, 48, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(20, 43, 48, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(20, 43, 48, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(20, 43, 48, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(18,39,43)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(18, 39, 43, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(18, 39, 43, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(18, 39, 43, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(18, 39, 43, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(18, 39, 43, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(18, 39, 43, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(18, 39, 43, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(18, 39, 43, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(18, 39, 43, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(92,139,149)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(92, 139, 149, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(92, 139, 149, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(92, 139, 149, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(92, 139, 149, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(92, 139, 149, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(92, 139, 149, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(92, 139, 149, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(92, 139, 149, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(92, 139, 149, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(125,162,170)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(125, 162, 170, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(125, 162, 170, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(125, 162, 170, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(125, 162, 170, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(125, 162, 170, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(125, 162, 170, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(125, 162, 170, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(125, 162, 170, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(125, 162, 170, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(151,181,187)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(151, 181, 187, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(151, 181, 187, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(151, 181, 187, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(151, 181, 187, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(151, 181, 187, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(151, 181, 187, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(151, 181, 187, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(151, 181, 187, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(151, 181, 187, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(172,196,201)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(172, 196, 201, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(172, 196, 201, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(172, 196, 201, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(172, 196, 201, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(172, 196, 201, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(172, 196, 201, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(172, 196, 201, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(172, 196, 201, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(172, 196, 201, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(189,208,212)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(189, 208, 212, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(189, 208, 212, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(189, 208, 212, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(189, 208, 212, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(189, 208, 212, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(189, 208, 212, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(189, 208, 212, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(189, 208, 212, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(189, 208, 212, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(202,217,221)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(202, 217, 221, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(202, 217, 221, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(202, 217, 221, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(202, 217, 221, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(202, 217, 221, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(202, 217, 221, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(202, 217, 221, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(202, 217, 221, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(202, 217, 221, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(213,225,228)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(213, 225, 228, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(213, 225, 228, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(213, 225, 228, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(213, 225, 228, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(213, 225, 228, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(213, 225, 228, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(213, 225, 228, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(213, 225, 228, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(213, 225, 228, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(221,231,233)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(221, 231, 233, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(221, 231, 233, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(221, 231, 233, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(221, 231, 233, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(221, 231, 233, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(221, 231, 233, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(221, 231, 233, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(221, 231, 233, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(221, 231, 233, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(228,236,237)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(228, 236, 237, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(228, 236, 237, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(228, 236, 237, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(228, 236, 237, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(228, 236, 237, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(228, 236, 237, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(228, 236, 237, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(228, 236, 237, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(228, 236, 237, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(51, 110, 123)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#6376a2\",\n        \"--color-badge-tertiary\": \"#6376a2\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue2\",\n    \"name\": \"清热板蓝\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(79, 98, 208)\",\n        \"--color-primary-dark-100\": \"rgb(71,88,187)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(71, 88, 187, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(79, 98, 208, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(71, 88, 187, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(79, 98, 208, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(71, 88, 187, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(79, 98, 208, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(71, 88, 187, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(79, 98, 208, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(71, 88, 187, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(79, 98, 208, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(71, 88, 187, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(79, 98, 208, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(71, 88, 187, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(79, 98, 208, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(71, 88, 187, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(79, 98, 208, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(71, 88, 187, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(79, 98, 208, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(64,79,168)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(64, 79, 168, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(64, 79, 168, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(64, 79, 168, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(64, 79, 168, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(64, 79, 168, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(64, 79, 168, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(64, 79, 168, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(64, 79, 168, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(64, 79, 168, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(58,71,151)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(58, 71, 151, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(58, 71, 151, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(58, 71, 151, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(58, 71, 151, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(58, 71, 151, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(58, 71, 151, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(58, 71, 151, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(58, 71, 151, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(58, 71, 151, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(52,64,136)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(52, 64, 136, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(52, 64, 136, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(52, 64, 136, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(52, 64, 136, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(52, 64, 136, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(52, 64, 136, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(52, 64, 136, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(52, 64, 136, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(52, 64, 136, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(47,58,122)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(47, 58, 122, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(47, 58, 122, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(47, 58, 122, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(47, 58, 122, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(47, 58, 122, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(47, 58, 122, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(47, 58, 122, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(47, 58, 122, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(47, 58, 122, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(42,52,110)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(42, 52, 110, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(42, 52, 110, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(42, 52, 110, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(42, 52, 110, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(42, 52, 110, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(42, 52, 110, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(42, 52, 110, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(42, 52, 110, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(42, 52, 110, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(38,47,99)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(38, 47, 99, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(38, 47, 99, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(38, 47, 99, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(38, 47, 99, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(38, 47, 99, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(38, 47, 99, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(38, 47, 99, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(38, 47, 99, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(38, 47, 99, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(34,42,89)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(34, 42, 89, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(34, 42, 89, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(34, 42, 89, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(34, 42, 89, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(34, 42, 89, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(34, 42, 89, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(34, 42, 89, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(34, 42, 89, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(34, 42, 89, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(31,38,80)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(31, 38, 80, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(31, 38, 80, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(31, 38, 80, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(31, 38, 80, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(31, 38, 80, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(31, 38, 80, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(31, 38, 80, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(31, 38, 80, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(31, 38, 80, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(28,34,72)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(28, 34, 72, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(28, 34, 72, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(28, 34, 72, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(28, 34, 72, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(28, 34, 72, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(28, 34, 72, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(28, 34, 72, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(28, 34, 72, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(28, 34, 72, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(114,129,217)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(114, 129, 217, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(114, 129, 217, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(114, 129, 217, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(114, 129, 217, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(114, 129, 217, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(114, 129, 217, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(114, 129, 217, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(114, 129, 217, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(114, 129, 217, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(142,154,225)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(142, 154, 225, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(142, 154, 225, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(142, 154, 225, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(142, 154, 225, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(142, 154, 225, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(142, 154, 225, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(142, 154, 225, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(142, 154, 225, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(142, 154, 225, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(165,174,231)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(165, 174, 231, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(165, 174, 231, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(165, 174, 231, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(165, 174, 231, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(165, 174, 231, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(165, 174, 231, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(165, 174, 231, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(165, 174, 231, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(165, 174, 231, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(183,190,236)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(183, 190, 236, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(183, 190, 236, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(183, 190, 236, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(183, 190, 236, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(183, 190, 236, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(183, 190, 236, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(183, 190, 236, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(183, 190, 236, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(183, 190, 236, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(197,203,240)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(197, 203, 240, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(197, 203, 240, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(197, 203, 240, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(197, 203, 240, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(197, 203, 240, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(197, 203, 240, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(197, 203, 240, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(197, 203, 240, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(197, 203, 240, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(209,213,243)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(209, 213, 243, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(209, 213, 243, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(209, 213, 243, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(209, 213, 243, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(209, 213, 243, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(209, 213, 243, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(209, 213, 243, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(209, 213, 243, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(209, 213, 243, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(218,221,245)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(218, 221, 245, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(218, 221, 245, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(218, 221, 245, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(218, 221, 245, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(218, 221, 245, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(218, 221, 245, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(218, 221, 245, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(218, 221, 245, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(218, 221, 245, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(225,228,247)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(225, 228, 247, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(225, 228, 247, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(225, 228, 247, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(225, 228, 247, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(225, 228, 247, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(225, 228, 247, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(225, 228, 247, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(225, 228, 247, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(225, 228, 247, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(231,233,249)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(231, 233, 249, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(231, 233, 249, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(231, 233, 249, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(231, 233, 249, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(231, 233, 249, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(231, 233, 249, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(231, 233, 249, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(231, 233, 249, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(231, 233, 249, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(79, 98, 208)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"var(--color-primary-light-600-alpha-700)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"none\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#b080db\",\n        \"--color-badge-tertiary\": \"#b080db\"\n      }\n    }\n  },\n  {\n    \"id\": \"black\",\n    \"name\": \"黑灯瞎火\",\n    \"isDark\": true,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(150, 150, 150)\",\n        \"--color-primary-dark-100\": \"rgb(171,171,171)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(171, 171, 171, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(150, 150, 150, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(171, 171, 171, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(150, 150, 150, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(171, 171, 171, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(150, 150, 150, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(171, 171, 171, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(150, 150, 150, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(171, 171, 171, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(150, 150, 150, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(171, 171, 171, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(150, 150, 150, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(171, 171, 171, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(150, 150, 150, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(171, 171, 171, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(150, 150, 150, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(171, 171, 171, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(150, 150, 150, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(188,188,188)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(188, 188, 188, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(188, 188, 188, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(188, 188, 188, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(188, 188, 188, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(188, 188, 188, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(188, 188, 188, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(188, 188, 188, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(188, 188, 188, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(188, 188, 188, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(201,201,201)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(201, 201, 201, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(201, 201, 201, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(201, 201, 201, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(201, 201, 201, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(201, 201, 201, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(201, 201, 201, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(201, 201, 201, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(201, 201, 201, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(201, 201, 201, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(212,212,212)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(212, 212, 212, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(212, 212, 212, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(212, 212, 212, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(212, 212, 212, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(212, 212, 212, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(212, 212, 212, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(212, 212, 212, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(212, 212, 212, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(212, 212, 212, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(221,221,221)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(221, 221, 221, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(221, 221, 221, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(221, 221, 221, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(221, 221, 221, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(221, 221, 221, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(221, 221, 221, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(221, 221, 221, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(221, 221, 221, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(221, 221, 221, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(228,228,228)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(228, 228, 228, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(228, 228, 228, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(228, 228, 228, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(228, 228, 228, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(228, 228, 228, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(228, 228, 228, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(228, 228, 228, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(228, 228, 228, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(228, 228, 228, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(233,233,233)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(233, 233, 233, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(233, 233, 233, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(233, 233, 233, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(233, 233, 233, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(233, 233, 233, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(233, 233, 233, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(233, 233, 233, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(233, 233, 233, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(233, 233, 233, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(237,237,237)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(237, 237, 237, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(237, 237, 237, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(237, 237, 237, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(237, 237, 237, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(237, 237, 237, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(237, 237, 237, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(237, 237, 237, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(237, 237, 237, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(237, 237, 237, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(241,241,241)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(241, 241, 241, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(241, 241, 241, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(241, 241, 241, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(241, 241, 241, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(241, 241, 241, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(241, 241, 241, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(241, 241, 241, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(241, 241, 241, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(241, 241, 241, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(244,244,244)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(244, 244, 244, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(244, 244, 244, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(244, 244, 244, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(244, 244, 244, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(244, 244, 244, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(244, 244, 244, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(244, 244, 244, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(244, 244, 244, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(244, 244, 244, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(135,135,135)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(135, 135, 135, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(135, 135, 135, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(135, 135, 135, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(135, 135, 135, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(135, 135, 135, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(135, 135, 135, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(135, 135, 135, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(135, 135, 135, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(135, 135, 135, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(122,122,122)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(122, 122, 122, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(122, 122, 122, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(122, 122, 122, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(122, 122, 122, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(122, 122, 122, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(122, 122, 122, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(122, 122, 122, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(122, 122, 122, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(122, 122, 122, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(110,110,110)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(110, 110, 110, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(110, 110, 110, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(110, 110, 110, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(110, 110, 110, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(110, 110, 110, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(110, 110, 110, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(110, 110, 110, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(110, 110, 110, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(110, 110, 110, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(99,99,99)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(99, 99, 99, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(99, 99, 99, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(99, 99, 99, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(99, 99, 99, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(99, 99, 99, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(99, 99, 99, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(99, 99, 99, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(99, 99, 99, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(99, 99, 99, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(89,89,89)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(89, 89, 89, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(89, 89, 89, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(89, 89, 89, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(89, 89, 89, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(89, 89, 89, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(89, 89, 89, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(89, 89, 89, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(89, 89, 89, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(89, 89, 89, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(80,80,80)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(80, 80, 80, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(80, 80, 80, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(80, 80, 80, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(80, 80, 80, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(80, 80, 80, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(80, 80, 80, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(80, 80, 80, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(80, 80, 80, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(80, 80, 80, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(72,72,72)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(72, 72, 72, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(72, 72, 72, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(72, 72, 72, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(72, 72, 72, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(72, 72, 72, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(72, 72, 72, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(72, 72, 72, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(72, 72, 72, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(72, 72, 72, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(65,65,65)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(65, 65, 65, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(65, 65, 65, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(65, 65, 65, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(65, 65, 65, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(65, 65, 65, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(65, 65, 65, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(65, 65, 65, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(65, 65, 65, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(65, 65, 65, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(59,59,59)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(59, 59, 59, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(59, 59, 59, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(59, 59, 59, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(59, 59, 59, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(59, 59, 59, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(59, 59, 59, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(59, 59, 59, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(59, 59, 59, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(59, 59, 59, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(38,38,38)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(38, 38, 38, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(38, 38, 38, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(38, 38, 38, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(38, 38, 38, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(38, 38, 38, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(38, 38, 38, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(38, 38, 38, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(38, 38, 38, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(38, 38, 38, 0.10)\",\n        \"--color-theme\": \"rgb(59,59,59)\",\n        \"--color-1000\": \"rgb(229, 229, 229)\",\n        \"--color-950\": \"rgb(218,218,218)\",\n        \"--color-900\": \"rgb(207,207,207)\",\n        \"--color-850\": \"rgb(197,197,197)\",\n        \"--color-800\": \"rgb(187,187,187)\",\n        \"--color-750\": \"rgb(178,178,178)\",\n        \"--color-700\": \"rgb(169,169,169)\",\n        \"--color-650\": \"rgb(161,161,161)\",\n        \"--color-600\": \"rgb(153,153,153)\",\n        \"--color-550\": \"rgb(145,145,145)\",\n        \"--color-500\": \"rgb(138,138,138)\",\n        \"--color-450\": \"rgb(131,131,131)\",\n        \"--color-400\": \"rgb(124,124,124)\",\n        \"--color-350\": \"rgb(118,118,118)\",\n        \"--color-300\": \"rgb(112,112,112)\",\n        \"--color-250\": \"rgb(106,106,106)\",\n        \"--color-200\": \"rgb(101,101,101)\",\n        \"--color-150\": \"rgb(96,96,96)\",\n        \"--color-100\": \"rgb(91,91,91)\",\n        \"--color-050\": \"rgb(86,86,86)\",\n        \"--color-000\": \"rgb(82,82,82)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"rgba(0, 0, 0, 0)\",\n        \"--color-main-background\": \"rgba(19, 19, 19, 0.9)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"url(./theme_images/landingMoon.png)\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary-dark-200)\",\n        \"--color-badge-secondary\": \"var(--color-primary)\",\n        \"--color-badge-tertiary\": \"var(--color-primary-dark-300)\"\n      }\n    }\n  },\n  {\n    \"id\": \"mid_autumn\",\n    \"name\": \"月里嫦娥\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(74, 55, 82)\",\n        \"--color-primary-dark-100\": \"rgb(67,50,74)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(67, 50, 74, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(74, 55, 82, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(67, 50, 74, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(74, 55, 82, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(67, 50, 74, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(74, 55, 82, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(67, 50, 74, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(74, 55, 82, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(67, 50, 74, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(74, 55, 82, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(67, 50, 74, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(74, 55, 82, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(67, 50, 74, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(74, 55, 82, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(67, 50, 74, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(74, 55, 82, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(67, 50, 74, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(74, 55, 82, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(60,45,67)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(60, 45, 67, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(60, 45, 67, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(60, 45, 67, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(60, 45, 67, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(60, 45, 67, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(60, 45, 67, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(60, 45, 67, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(60, 45, 67, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(60, 45, 67, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(54,41,60)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(54, 41, 60, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(54, 41, 60, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(54, 41, 60, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(54, 41, 60, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(54, 41, 60, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(54, 41, 60, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(54, 41, 60, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(54, 41, 60, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(54, 41, 60, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(49,37,54)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(49, 37, 54, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(49, 37, 54, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(49, 37, 54, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(49, 37, 54, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(49, 37, 54, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(49, 37, 54, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(49, 37, 54, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(49, 37, 54, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(49, 37, 54, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(44,33,49)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(44, 33, 49, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(44, 33, 49, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(44, 33, 49, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(44, 33, 49, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(44, 33, 49, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(44, 33, 49, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(44, 33, 49, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(44, 33, 49, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(44, 33, 49, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(40,30,44)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(40, 30, 44, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(40, 30, 44, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(40, 30, 44, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(40, 30, 44, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(40, 30, 44, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(40, 30, 44, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(40, 30, 44, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(40, 30, 44, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(40, 30, 44, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(36,27,40)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(36, 27, 40, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(36, 27, 40, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(36, 27, 40, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(36, 27, 40, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(36, 27, 40, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(36, 27, 40, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(36, 27, 40, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(36, 27, 40, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(36, 27, 40, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(32,24,36)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(32, 24, 36, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(32, 24, 36, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(32, 24, 36, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(32, 24, 36, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(32, 24, 36, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(32, 24, 36, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(32, 24, 36, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(32, 24, 36, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(32, 24, 36, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(29,22,32)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(29, 22, 32, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(29, 22, 32, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(29, 22, 32, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(29, 22, 32, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(29, 22, 32, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(29, 22, 32, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(29, 22, 32, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(29, 22, 32, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(29, 22, 32, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(26,20,29)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(26, 20, 29, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(26, 20, 29, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(26, 20, 29, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(26, 20, 29, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(26, 20, 29, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(26, 20, 29, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(26, 20, 29, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(26, 20, 29, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(26, 20, 29, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(110,95,117)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(110, 95, 117, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(110, 95, 117, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(110, 95, 117, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(110, 95, 117, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(110, 95, 117, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(110, 95, 117, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(110, 95, 117, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(110, 95, 117, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(110, 95, 117, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(139,127,145)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(139, 127, 145, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(139, 127, 145, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(139, 127, 145, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(139, 127, 145, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(139, 127, 145, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(139, 127, 145, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(139, 127, 145, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(139, 127, 145, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(139, 127, 145, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(162,153,167)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(162, 153, 167, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(162, 153, 167, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(162, 153, 167, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(162, 153, 167, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(162, 153, 167, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(162, 153, 167, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(162, 153, 167, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(162, 153, 167, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(162, 153, 167, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(181,173,185)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(181, 173, 185, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(181, 173, 185, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(181, 173, 185, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(181, 173, 185, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(181, 173, 185, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(181, 173, 185, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(181, 173, 185, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(181, 173, 185, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(181, 173, 185, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(196,189,199)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(196, 189, 199, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(196, 189, 199, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(196, 189, 199, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(196, 189, 199, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(196, 189, 199, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(196, 189, 199, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(196, 189, 199, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(196, 189, 199, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(196, 189, 199, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(208,202,210)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(208, 202, 210, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(208, 202, 210, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(208, 202, 210, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(208, 202, 210, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(208, 202, 210, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(208, 202, 210, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(208, 202, 210, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(208, 202, 210, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(208, 202, 210, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(217,213,219)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(217, 213, 219, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(217, 213, 219, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(217, 213, 219, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(217, 213, 219, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(217, 213, 219, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(217, 213, 219, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(217, 213, 219, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(217, 213, 219, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(217, 213, 219, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(225,221,226)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(225, 221, 226, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(225, 221, 226, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(225, 221, 226, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(225, 221, 226, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(225, 221, 226, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(225, 221, 226, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(225, 221, 226, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(225, 221, 226, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(225, 221, 226, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(231,228,232)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(231, 228, 232, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(231, 228, 232, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(231, 228, 232, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(231, 228, 232, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(231, 228, 232, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(231, 228, 232, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(231, 228, 232, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(231, 228, 232, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(231, 228, 232, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(74, 55, 82)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"rgba(255, 255, 255, 0)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 0.9)\",\n        \"--color-nav-font\": \"var(--color-primary-light-600)\",\n        \"--background-image\": \"url(./theme_images/jqbg.jpg)\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"#af9479\",\n        \"--color-badge-tertiary\": \"#af9479\"\n      }\n    }\n  },\n  {\n    \"id\": \"naruto\",\n    \"name\": \"木叶之村\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(87, 144, 167)\",\n        \"--color-primary-dark-100\": \"rgb(78,130,150)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(78, 130, 150, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(87, 144, 167, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(78, 130, 150, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(87, 144, 167, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(78, 130, 150, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(87, 144, 167, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(78, 130, 150, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(87, 144, 167, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(78, 130, 150, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(87, 144, 167, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(78, 130, 150, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(87, 144, 167, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(78, 130, 150, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(87, 144, 167, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(78, 130, 150, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(87, 144, 167, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(78, 130, 150, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(87, 144, 167, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(70,117,135)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(70, 117, 135, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(70, 117, 135, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(70, 117, 135, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(70, 117, 135, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(70, 117, 135, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(70, 117, 135, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(70, 117, 135, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(70, 117, 135, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(70, 117, 135, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(63,105,122)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(63, 105, 122, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(63, 105, 122, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(63, 105, 122, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(63, 105, 122, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(63, 105, 122, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(63, 105, 122, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(63, 105, 122, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(63, 105, 122, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(63, 105, 122, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(57,95,110)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(57, 95, 110, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(57, 95, 110, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(57, 95, 110, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(57, 95, 110, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(57, 95, 110, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(57, 95, 110, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(57, 95, 110, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(57, 95, 110, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(57, 95, 110, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(51,86,99)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(51, 86, 99, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(51, 86, 99, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(51, 86, 99, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(51, 86, 99, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(51, 86, 99, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(51, 86, 99, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(51, 86, 99, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(51, 86, 99, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(51, 86, 99, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(46,77,89)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(46, 77, 89, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(46, 77, 89, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(46, 77, 89, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(46, 77, 89, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(46, 77, 89, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(46, 77, 89, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(46, 77, 89, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(46, 77, 89, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(46, 77, 89, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(41,69,80)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(41, 69, 80, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(41, 69, 80, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(41, 69, 80, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(41, 69, 80, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(41, 69, 80, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(41, 69, 80, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(41, 69, 80, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(41, 69, 80, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(41, 69, 80, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(37,62,72)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(37, 62, 72, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(37, 62, 72, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(37, 62, 72, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(37, 62, 72, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(37, 62, 72, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(37, 62, 72, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(37, 62, 72, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(37, 62, 72, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(37, 62, 72, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(33,56,65)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(33, 56, 65, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(33, 56, 65, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(33, 56, 65, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(33, 56, 65, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(33, 56, 65, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(33, 56, 65, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(33, 56, 65, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(33, 56, 65, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(33, 56, 65, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(30,50,59)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(30, 50, 59, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(30, 50, 59, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(30, 50, 59, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(30, 50, 59, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(30, 50, 59, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(30, 50, 59, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(30, 50, 59, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(30, 50, 59, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(30, 50, 59, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(121,166,185)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(121, 166, 185, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(121, 166, 185, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(121, 166, 185, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(121, 166, 185, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(121, 166, 185, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(121, 166, 185, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(121, 166, 185, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(121, 166, 185, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(121, 166, 185, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(148,184,199)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(148, 184, 199, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(148, 184, 199, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(148, 184, 199, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(148, 184, 199, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(148, 184, 199, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(148, 184, 199, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(148, 184, 199, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(148, 184, 199, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(148, 184, 199, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(169,198,210)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(169, 198, 210, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(169, 198, 210, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(169, 198, 210, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(169, 198, 210, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(169, 198, 210, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(169, 198, 210, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(169, 198, 210, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(169, 198, 210, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(169, 198, 210, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(186,209,219)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(186, 209, 219, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(186, 209, 219, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(186, 209, 219, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(186, 209, 219, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(186, 209, 219, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(186, 209, 219, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(186, 209, 219, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(186, 209, 219, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(186, 209, 219, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(200,218,226)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(200, 218, 226, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(200, 218, 226, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(200, 218, 226, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(200, 218, 226, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(200, 218, 226, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(200, 218, 226, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(200, 218, 226, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(200, 218, 226, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(200, 218, 226, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(211,225,232)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(211, 225, 232, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(211, 225, 232, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(211, 225, 232, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(211, 225, 232, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(211, 225, 232, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(211, 225, 232, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(211, 225, 232, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(211, 225, 232, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(211, 225, 232, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(220,231,237)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(220, 231, 237, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(220, 231, 237, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(220, 231, 237, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(220, 231, 237, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(220, 231, 237, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(220, 231, 237, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(220, 231, 237, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(220, 231, 237, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(220, 231, 237, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(227,236,241)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(227, 236, 241, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(227, 236, 241, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(227, 236, 241, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(227, 236, 241, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(227, 236, 241, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(227, 236, 241, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(227, 236, 241, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(227, 236, 241, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(227, 236, 241, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(233,240,244)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(233, 240, 244, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(233, 240, 244, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(233, 240, 244, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(233, 240, 244, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(233, 240, 244, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(233, 240, 244, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(233, 240, 244, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(233, 240, 244, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(233, 240, 244, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(87, 144, 167)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"rgba(255, 255, 255, 0.15)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"url(./theme_images/myzcbg.jpg)\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"var(--color-primary)\",\n        \"--color-badge-secondary\": \"var(--color-primary-light-100)\",\n        \"--color-badge-tertiary\": \"var(--color-primary-light-100)\"\n      }\n    }\n  },\n  {\n    \"id\": \"china_ink\",\n    \"name\": \"近墨者黑\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgba(47, 47, 47, 1)\",\n        \"--color-primary-dark-100\": \"rgba(42,42,42, 1)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(42, 42, 42, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(47, 47, 47, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(42, 42, 42, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(47, 47, 47, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(42, 42, 42, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(47, 47, 47, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(42, 42, 42, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(47, 47, 47, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(42, 42, 42, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(47, 47, 47, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(42, 42, 42, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(47, 47, 47, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(42, 42, 42, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(47, 47, 47, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(42, 42, 42, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(47, 47, 47, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(42, 42, 42, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(47, 47, 47, 0.10)\",\n        \"--color-primary-dark-200\": \"rgba(38,38,38, 1)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(38, 38, 38, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(38, 38, 38, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(38, 38, 38, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(38, 38, 38, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(38, 38, 38, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(38, 38, 38, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(38, 38, 38, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(38, 38, 38, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(38, 38, 38, 0.10)\",\n        \"--color-primary-dark-300\": \"rgba(34,34,34, 1)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(34, 34, 34, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(34, 34, 34, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(34, 34, 34, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(34, 34, 34, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(34, 34, 34, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(34, 34, 34, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(34, 34, 34, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(34, 34, 34, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(34, 34, 34, 0.10)\",\n        \"--color-primary-dark-400\": \"rgba(31,31,31, 1)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(31, 31, 31, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(31, 31, 31, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(31, 31, 31, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(31, 31, 31, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(31, 31, 31, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(31, 31, 31, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(31, 31, 31, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(31, 31, 31, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(31, 31, 31, 0.10)\",\n        \"--color-primary-dark-500\": \"rgba(28,28,28, 1)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(28, 28, 28, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(28, 28, 28, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(28, 28, 28, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(28, 28, 28, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(28, 28, 28, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(28, 28, 28, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(28, 28, 28, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(28, 28, 28, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(28, 28, 28, 0.10)\",\n        \"--color-primary-dark-600\": \"rgba(25,25,25, 1)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(25, 25, 25, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(25, 25, 25, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(25, 25, 25, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(25, 25, 25, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(25, 25, 25, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(25, 25, 25, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(25, 25, 25, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(25, 25, 25, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(25, 25, 25, 0.10)\",\n        \"--color-primary-dark-700\": \"rgba(23,23,23, 1)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(23, 23, 23, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(23, 23, 23, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(23, 23, 23, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(23, 23, 23, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(23, 23, 23, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(23, 23, 23, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(23, 23, 23, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(23, 23, 23, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(23, 23, 23, 0.10)\",\n        \"--color-primary-dark-800\": \"rgba(21,21,21, 1)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(21, 21, 21, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(21, 21, 21, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(21, 21, 21, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(21, 21, 21, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(21, 21, 21, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(21, 21, 21, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(21, 21, 21, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(21, 21, 21, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(21, 21, 21, 0.10)\",\n        \"--color-primary-dark-900\": \"rgba(19,19,19, 1)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(19, 19, 19, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(19, 19, 19, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(19, 19, 19, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(19, 19, 19, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(19, 19, 19, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(19, 19, 19, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(19, 19, 19, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(19, 19, 19, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(19, 19, 19, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgba(17,17,17, 1)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(17, 17, 17, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(17, 17, 17, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(17, 17, 17, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(17, 17, 17, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(17, 17, 17, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(17, 17, 17, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(17, 17, 17, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(17, 17, 17, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(17, 17, 17, 0.10)\",\n        \"--color-primary-light-100\": \"rgba(89,89,89, 1)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(89, 89, 89, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(89, 89, 89, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(89, 89, 89, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(89, 89, 89, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(89, 89, 89, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(89, 89, 89, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(89, 89, 89, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(89, 89, 89, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(89, 89, 89, 0.10)\",\n        \"--color-primary-light-200\": \"rgba(122,122,122, 1)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(122, 122, 122, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(122, 122, 122, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(122, 122, 122, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(122, 122, 122, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(122, 122, 122, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(122, 122, 122, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(122, 122, 122, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(122, 122, 122, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(122, 122, 122, 0.10)\",\n        \"--color-primary-light-300\": \"rgba(149,149,149, 1)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(149, 149, 149, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(149, 149, 149, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(149, 149, 149, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(149, 149, 149, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(149, 149, 149, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(149, 149, 149, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(149, 149, 149, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(149, 149, 149, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(149, 149, 149, 0.10)\",\n        \"--color-primary-light-400\": \"rgba(170,170,170, 1)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(170, 170, 170, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(170, 170, 170, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(170, 170, 170, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(170, 170, 170, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(170, 170, 170, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(170, 170, 170, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(170, 170, 170, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(170, 170, 170, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(170, 170, 170, 0.10)\",\n        \"--color-primary-light-500\": \"rgba(187,187,187, 1)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(187, 187, 187, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(187, 187, 187, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(187, 187, 187, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(187, 187, 187, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(187, 187, 187, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(187, 187, 187, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(187, 187, 187, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(187, 187, 187, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(187, 187, 187, 0.10)\",\n        \"--color-primary-light-600\": \"rgba(201,201,201, 1)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(201, 201, 201, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(201, 201, 201, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(201, 201, 201, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(201, 201, 201, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(201, 201, 201, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(201, 201, 201, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(201, 201, 201, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(201, 201, 201, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(201, 201, 201, 0.10)\",\n        \"--color-primary-light-700\": \"rgba(212,212,212, 1)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(212, 212, 212, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(212, 212, 212, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(212, 212, 212, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(212, 212, 212, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(212, 212, 212, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(212, 212, 212, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(212, 212, 212, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(212, 212, 212, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(212, 212, 212, 0.10)\",\n        \"--color-primary-light-800\": \"rgba(221,221,221, 1)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(221, 221, 221, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(221, 221, 221, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(221, 221, 221, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(221, 221, 221, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(221, 221, 221, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(221, 221, 221, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(221, 221, 221, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(221, 221, 221, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(221, 221, 221, 0.10)\",\n        \"--color-primary-light-900\": \"rgba(228,228,228, 1)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(228, 228, 228, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(228, 228, 228, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(228, 228, 228, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(228, 228, 228, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(228, 228, 228, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(228, 228, 228, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(228, 228, 228, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(228, 228, 228, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(228, 228, 228, 0.10)\",\n        \"--color-primary-light-1000\": \"rgba(255,255,255, 1)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgba(47, 47, 47, 1)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"rgba(255, 255, 255, 0)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"url(./theme_images/china_ink.jpg)\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"rgba(183, 212, 208, 1)\",\n        \"--color-btn-min\": \"rgba(200, 214, 183, 1)\",\n        \"--color-btn-close\": \"rgba(218, 195, 188, 1)\",\n        \"--color-badge-primary\": \"rgba(137, 70, 70, 1)\",\n        \"--color-badge-secondary\": \"rgba(67, 139, 65, 1)\",\n        \"--color-badge-tertiary\": \"rgba(132, 135, 65, 1)\"\n      }\n    }\n  },\n  {\n    \"id\": \"happy_new_year\",\n    \"name\": \"新年快乐\",\n    \"isDark\": false,\n    \"isDarkFont\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"--color-primary\": \"rgb(192, 57, 43)\",\n        \"--color-primary-dark-100\": \"rgb(173,51,39)\",\n        \"--color-primary-dark-100-alpha-100\": \"rgba(173, 51, 39, 0.90)\",\n        \"--color-primary-alpha-100\": \"rgba(192, 57, 43, 0.90)\",\n        \"--color-primary-dark-100-alpha-200\": \"rgba(173, 51, 39, 0.80)\",\n        \"--color-primary-alpha-200\": \"rgba(192, 57, 43, 0.80)\",\n        \"--color-primary-dark-100-alpha-300\": \"rgba(173, 51, 39, 0.70)\",\n        \"--color-primary-alpha-300\": \"rgba(192, 57, 43, 0.70)\",\n        \"--color-primary-dark-100-alpha-400\": \"rgba(173, 51, 39, 0.60)\",\n        \"--color-primary-alpha-400\": \"rgba(192, 57, 43, 0.60)\",\n        \"--color-primary-dark-100-alpha-500\": \"rgba(173, 51, 39, 0.50)\",\n        \"--color-primary-alpha-500\": \"rgba(192, 57, 43, 0.50)\",\n        \"--color-primary-dark-100-alpha-600\": \"rgba(173, 51, 39, 0.40)\",\n        \"--color-primary-alpha-600\": \"rgba(192, 57, 43, 0.40)\",\n        \"--color-primary-dark-100-alpha-700\": \"rgba(173, 51, 39, 0.30)\",\n        \"--color-primary-alpha-700\": \"rgba(192, 57, 43, 0.30)\",\n        \"--color-primary-dark-100-alpha-800\": \"rgba(173, 51, 39, 0.20)\",\n        \"--color-primary-alpha-800\": \"rgba(192, 57, 43, 0.20)\",\n        \"--color-primary-dark-100-alpha-900\": \"rgba(173, 51, 39, 0.10)\",\n        \"--color-primary-alpha-900\": \"rgba(192, 57, 43, 0.10)\",\n        \"--color-primary-dark-200\": \"rgb(156,46,35)\",\n        \"--color-primary-dark-200-alpha-100\": \"rgba(156, 46, 35, 0.90)\",\n        \"--color-primary-dark-200-alpha-200\": \"rgba(156, 46, 35, 0.80)\",\n        \"--color-primary-dark-200-alpha-300\": \"rgba(156, 46, 35, 0.70)\",\n        \"--color-primary-dark-200-alpha-400\": \"rgba(156, 46, 35, 0.60)\",\n        \"--color-primary-dark-200-alpha-500\": \"rgba(156, 46, 35, 0.50)\",\n        \"--color-primary-dark-200-alpha-600\": \"rgba(156, 46, 35, 0.40)\",\n        \"--color-primary-dark-200-alpha-700\": \"rgba(156, 46, 35, 0.30)\",\n        \"--color-primary-dark-200-alpha-800\": \"rgba(156, 46, 35, 0.20)\",\n        \"--color-primary-dark-200-alpha-900\": \"rgba(156, 46, 35, 0.10)\",\n        \"--color-primary-dark-300\": \"rgb(140,41,32)\",\n        \"--color-primary-dark-300-alpha-100\": \"rgba(140, 41, 32, 0.90)\",\n        \"--color-primary-dark-300-alpha-200\": \"rgba(140, 41, 32, 0.80)\",\n        \"--color-primary-dark-300-alpha-300\": \"rgba(140, 41, 32, 0.70)\",\n        \"--color-primary-dark-300-alpha-400\": \"rgba(140, 41, 32, 0.60)\",\n        \"--color-primary-dark-300-alpha-500\": \"rgba(140, 41, 32, 0.50)\",\n        \"--color-primary-dark-300-alpha-600\": \"rgba(140, 41, 32, 0.40)\",\n        \"--color-primary-dark-300-alpha-700\": \"rgba(140, 41, 32, 0.30)\",\n        \"--color-primary-dark-300-alpha-800\": \"rgba(140, 41, 32, 0.20)\",\n        \"--color-primary-dark-300-alpha-900\": \"rgba(140, 41, 32, 0.10)\",\n        \"--color-primary-dark-400\": \"rgb(126,37,29)\",\n        \"--color-primary-dark-400-alpha-100\": \"rgba(126, 37, 29, 0.90)\",\n        \"--color-primary-dark-400-alpha-200\": \"rgba(126, 37, 29, 0.80)\",\n        \"--color-primary-dark-400-alpha-300\": \"rgba(126, 37, 29, 0.70)\",\n        \"--color-primary-dark-400-alpha-400\": \"rgba(126, 37, 29, 0.60)\",\n        \"--color-primary-dark-400-alpha-500\": \"rgba(126, 37, 29, 0.50)\",\n        \"--color-primary-dark-400-alpha-600\": \"rgba(126, 37, 29, 0.40)\",\n        \"--color-primary-dark-400-alpha-700\": \"rgba(126, 37, 29, 0.30)\",\n        \"--color-primary-dark-400-alpha-800\": \"rgba(126, 37, 29, 0.20)\",\n        \"--color-primary-dark-400-alpha-900\": \"rgba(126, 37, 29, 0.10)\",\n        \"--color-primary-dark-500\": \"rgb(113,33,26)\",\n        \"--color-primary-dark-500-alpha-100\": \"rgba(113, 33, 26, 0.90)\",\n        \"--color-primary-dark-500-alpha-200\": \"rgba(113, 33, 26, 0.80)\",\n        \"--color-primary-dark-500-alpha-300\": \"rgba(113, 33, 26, 0.70)\",\n        \"--color-primary-dark-500-alpha-400\": \"rgba(113, 33, 26, 0.60)\",\n        \"--color-primary-dark-500-alpha-500\": \"rgba(113, 33, 26, 0.50)\",\n        \"--color-primary-dark-500-alpha-600\": \"rgba(113, 33, 26, 0.40)\",\n        \"--color-primary-dark-500-alpha-700\": \"rgba(113, 33, 26, 0.30)\",\n        \"--color-primary-dark-500-alpha-800\": \"rgba(113, 33, 26, 0.20)\",\n        \"--color-primary-dark-500-alpha-900\": \"rgba(113, 33, 26, 0.10)\",\n        \"--color-primary-dark-600\": \"rgb(102,30,23)\",\n        \"--color-primary-dark-600-alpha-100\": \"rgba(102, 30, 23, 0.90)\",\n        \"--color-primary-dark-600-alpha-200\": \"rgba(102, 30, 23, 0.80)\",\n        \"--color-primary-dark-600-alpha-300\": \"rgba(102, 30, 23, 0.70)\",\n        \"--color-primary-dark-600-alpha-400\": \"rgba(102, 30, 23, 0.60)\",\n        \"--color-primary-dark-600-alpha-500\": \"rgba(102, 30, 23, 0.50)\",\n        \"--color-primary-dark-600-alpha-600\": \"rgba(102, 30, 23, 0.40)\",\n        \"--color-primary-dark-600-alpha-700\": \"rgba(102, 30, 23, 0.30)\",\n        \"--color-primary-dark-600-alpha-800\": \"rgba(102, 30, 23, 0.20)\",\n        \"--color-primary-dark-600-alpha-900\": \"rgba(102, 30, 23, 0.10)\",\n        \"--color-primary-dark-700\": \"rgb(92,27,21)\",\n        \"--color-primary-dark-700-alpha-100\": \"rgba(92, 27, 21, 0.90)\",\n        \"--color-primary-dark-700-alpha-200\": \"rgba(92, 27, 21, 0.80)\",\n        \"--color-primary-dark-700-alpha-300\": \"rgba(92, 27, 21, 0.70)\",\n        \"--color-primary-dark-700-alpha-400\": \"rgba(92, 27, 21, 0.60)\",\n        \"--color-primary-dark-700-alpha-500\": \"rgba(92, 27, 21, 0.50)\",\n        \"--color-primary-dark-700-alpha-600\": \"rgba(92, 27, 21, 0.40)\",\n        \"--color-primary-dark-700-alpha-700\": \"rgba(92, 27, 21, 0.30)\",\n        \"--color-primary-dark-700-alpha-800\": \"rgba(92, 27, 21, 0.20)\",\n        \"--color-primary-dark-700-alpha-900\": \"rgba(92, 27, 21, 0.10)\",\n        \"--color-primary-dark-800\": \"rgb(83,24,19)\",\n        \"--color-primary-dark-800-alpha-100\": \"rgba(83, 24, 19, 0.90)\",\n        \"--color-primary-dark-800-alpha-200\": \"rgba(83, 24, 19, 0.80)\",\n        \"--color-primary-dark-800-alpha-300\": \"rgba(83, 24, 19, 0.70)\",\n        \"--color-primary-dark-800-alpha-400\": \"rgba(83, 24, 19, 0.60)\",\n        \"--color-primary-dark-800-alpha-500\": \"rgba(83, 24, 19, 0.50)\",\n        \"--color-primary-dark-800-alpha-600\": \"rgba(83, 24, 19, 0.40)\",\n        \"--color-primary-dark-800-alpha-700\": \"rgba(83, 24, 19, 0.30)\",\n        \"--color-primary-dark-800-alpha-800\": \"rgba(83, 24, 19, 0.20)\",\n        \"--color-primary-dark-800-alpha-900\": \"rgba(83, 24, 19, 0.10)\",\n        \"--color-primary-dark-900\": \"rgb(75,22,17)\",\n        \"--color-primary-dark-900-alpha-100\": \"rgba(75, 22, 17, 0.90)\",\n        \"--color-primary-dark-900-alpha-200\": \"rgba(75, 22, 17, 0.80)\",\n        \"--color-primary-dark-900-alpha-300\": \"rgba(75, 22, 17, 0.70)\",\n        \"--color-primary-dark-900-alpha-400\": \"rgba(75, 22, 17, 0.60)\",\n        \"--color-primary-dark-900-alpha-500\": \"rgba(75, 22, 17, 0.50)\",\n        \"--color-primary-dark-900-alpha-600\": \"rgba(75, 22, 17, 0.40)\",\n        \"--color-primary-dark-900-alpha-700\": \"rgba(75, 22, 17, 0.30)\",\n        \"--color-primary-dark-900-alpha-800\": \"rgba(75, 22, 17, 0.20)\",\n        \"--color-primary-dark-900-alpha-900\": \"rgba(75, 22, 17, 0.10)\",\n        \"--color-primary-dark-1000\": \"rgb(68,20,15)\",\n        \"--color-primary-dark-1000-alpha-100\": \"rgba(68, 20, 15, 0.90)\",\n        \"--color-primary-dark-1000-alpha-200\": \"rgba(68, 20, 15, 0.80)\",\n        \"--color-primary-dark-1000-alpha-300\": \"rgba(68, 20, 15, 0.70)\",\n        \"--color-primary-dark-1000-alpha-400\": \"rgba(68, 20, 15, 0.60)\",\n        \"--color-primary-dark-1000-alpha-500\": \"rgba(68, 20, 15, 0.50)\",\n        \"--color-primary-dark-1000-alpha-600\": \"rgba(68, 20, 15, 0.40)\",\n        \"--color-primary-dark-1000-alpha-700\": \"rgba(68, 20, 15, 0.30)\",\n        \"--color-primary-dark-1000-alpha-800\": \"rgba(68, 20, 15, 0.20)\",\n        \"--color-primary-dark-1000-alpha-900\": \"rgba(68, 20, 15, 0.10)\",\n        \"--color-primary-light-100\": \"rgb(205,97,85)\",\n        \"--color-primary-light-100-alpha-100\": \"rgba(205, 97, 85, 0.90)\",\n        \"--color-primary-light-100-alpha-200\": \"rgba(205, 97, 85, 0.80)\",\n        \"--color-primary-light-100-alpha-300\": \"rgba(205, 97, 85, 0.70)\",\n        \"--color-primary-light-100-alpha-400\": \"rgba(205, 97, 85, 0.60)\",\n        \"--color-primary-light-100-alpha-500\": \"rgba(205, 97, 85, 0.50)\",\n        \"--color-primary-light-100-alpha-600\": \"rgba(205, 97, 85, 0.40)\",\n        \"--color-primary-light-100-alpha-700\": \"rgba(205, 97, 85, 0.30)\",\n        \"--color-primary-light-100-alpha-800\": \"rgba(205, 97, 85, 0.20)\",\n        \"--color-primary-light-100-alpha-900\": \"rgba(205, 97, 85, 0.10)\",\n        \"--color-primary-light-200\": \"rgb(215,129,119)\",\n        \"--color-primary-light-200-alpha-100\": \"rgba(215, 129, 119, 0.90)\",\n        \"--color-primary-light-200-alpha-200\": \"rgba(215, 129, 119, 0.80)\",\n        \"--color-primary-light-200-alpha-300\": \"rgba(215, 129, 119, 0.70)\",\n        \"--color-primary-light-200-alpha-400\": \"rgba(215, 129, 119, 0.60)\",\n        \"--color-primary-light-200-alpha-500\": \"rgba(215, 129, 119, 0.50)\",\n        \"--color-primary-light-200-alpha-600\": \"rgba(215, 129, 119, 0.40)\",\n        \"--color-primary-light-200-alpha-700\": \"rgba(215, 129, 119, 0.30)\",\n        \"--color-primary-light-200-alpha-800\": \"rgba(215, 129, 119, 0.20)\",\n        \"--color-primary-light-200-alpha-900\": \"rgba(215, 129, 119, 0.10)\",\n        \"--color-primary-light-300\": \"rgb(223,154,146)\",\n        \"--color-primary-light-300-alpha-100\": \"rgba(223, 154, 146, 0.90)\",\n        \"--color-primary-light-300-alpha-200\": \"rgba(223, 154, 146, 0.80)\",\n        \"--color-primary-light-300-alpha-300\": \"rgba(223, 154, 146, 0.70)\",\n        \"--color-primary-light-300-alpha-400\": \"rgba(223, 154, 146, 0.60)\",\n        \"--color-primary-light-300-alpha-500\": \"rgba(223, 154, 146, 0.50)\",\n        \"--color-primary-light-300-alpha-600\": \"rgba(223, 154, 146, 0.40)\",\n        \"--color-primary-light-300-alpha-700\": \"rgba(223, 154, 146, 0.30)\",\n        \"--color-primary-light-300-alpha-800\": \"rgba(223, 154, 146, 0.20)\",\n        \"--color-primary-light-300-alpha-900\": \"rgba(223, 154, 146, 0.10)\",\n        \"--color-primary-light-400\": \"rgb(229,174,168)\",\n        \"--color-primary-light-400-alpha-100\": \"rgba(229, 174, 168, 0.90)\",\n        \"--color-primary-light-400-alpha-200\": \"rgba(229, 174, 168, 0.80)\",\n        \"--color-primary-light-400-alpha-300\": \"rgba(229, 174, 168, 0.70)\",\n        \"--color-primary-light-400-alpha-400\": \"rgba(229, 174, 168, 0.60)\",\n        \"--color-primary-light-400-alpha-500\": \"rgba(229, 174, 168, 0.50)\",\n        \"--color-primary-light-400-alpha-600\": \"rgba(229, 174, 168, 0.40)\",\n        \"--color-primary-light-400-alpha-700\": \"rgba(229, 174, 168, 0.30)\",\n        \"--color-primary-light-400-alpha-800\": \"rgba(229, 174, 168, 0.20)\",\n        \"--color-primary-light-400-alpha-900\": \"rgba(229, 174, 168, 0.10)\",\n        \"--color-primary-light-500\": \"rgb(234,190,185)\",\n        \"--color-primary-light-500-alpha-100\": \"rgba(234, 190, 185, 0.90)\",\n        \"--color-primary-light-500-alpha-200\": \"rgba(234, 190, 185, 0.80)\",\n        \"--color-primary-light-500-alpha-300\": \"rgba(234, 190, 185, 0.70)\",\n        \"--color-primary-light-500-alpha-400\": \"rgba(234, 190, 185, 0.60)\",\n        \"--color-primary-light-500-alpha-500\": \"rgba(234, 190, 185, 0.50)\",\n        \"--color-primary-light-500-alpha-600\": \"rgba(234, 190, 185, 0.40)\",\n        \"--color-primary-light-500-alpha-700\": \"rgba(234, 190, 185, 0.30)\",\n        \"--color-primary-light-500-alpha-800\": \"rgba(234, 190, 185, 0.20)\",\n        \"--color-primary-light-500-alpha-900\": \"rgba(234, 190, 185, 0.10)\",\n        \"--color-primary-light-600\": \"rgb(238,203,199)\",\n        \"--color-primary-light-600-alpha-100\": \"rgba(238, 203, 199, 0.90)\",\n        \"--color-primary-light-600-alpha-200\": \"rgba(238, 203, 199, 0.80)\",\n        \"--color-primary-light-600-alpha-300\": \"rgba(238, 203, 199, 0.70)\",\n        \"--color-primary-light-600-alpha-400\": \"rgba(238, 203, 199, 0.60)\",\n        \"--color-primary-light-600-alpha-500\": \"rgba(238, 203, 199, 0.50)\",\n        \"--color-primary-light-600-alpha-600\": \"rgba(238, 203, 199, 0.40)\",\n        \"--color-primary-light-600-alpha-700\": \"rgba(238, 203, 199, 0.30)\",\n        \"--color-primary-light-600-alpha-800\": \"rgba(238, 203, 199, 0.20)\",\n        \"--color-primary-light-600-alpha-900\": \"rgba(238, 203, 199, 0.10)\",\n        \"--color-primary-light-700\": \"rgb(241,213,210)\",\n        \"--color-primary-light-700-alpha-100\": \"rgba(241, 213, 210, 0.90)\",\n        \"--color-primary-light-700-alpha-200\": \"rgba(241, 213, 210, 0.80)\",\n        \"--color-primary-light-700-alpha-300\": \"rgba(241, 213, 210, 0.70)\",\n        \"--color-primary-light-700-alpha-400\": \"rgba(241, 213, 210, 0.60)\",\n        \"--color-primary-light-700-alpha-500\": \"rgba(241, 213, 210, 0.50)\",\n        \"--color-primary-light-700-alpha-600\": \"rgba(241, 213, 210, 0.40)\",\n        \"--color-primary-light-700-alpha-700\": \"rgba(241, 213, 210, 0.30)\",\n        \"--color-primary-light-700-alpha-800\": \"rgba(241, 213, 210, 0.20)\",\n        \"--color-primary-light-700-alpha-900\": \"rgba(241, 213, 210, 0.10)\",\n        \"--color-primary-light-800\": \"rgb(244,221,219)\",\n        \"--color-primary-light-800-alpha-100\": \"rgba(244, 221, 219, 0.90)\",\n        \"--color-primary-light-800-alpha-200\": \"rgba(244, 221, 219, 0.80)\",\n        \"--color-primary-light-800-alpha-300\": \"rgba(244, 221, 219, 0.70)\",\n        \"--color-primary-light-800-alpha-400\": \"rgba(244, 221, 219, 0.60)\",\n        \"--color-primary-light-800-alpha-500\": \"rgba(244, 221, 219, 0.50)\",\n        \"--color-primary-light-800-alpha-600\": \"rgba(244, 221, 219, 0.40)\",\n        \"--color-primary-light-800-alpha-700\": \"rgba(244, 221, 219, 0.30)\",\n        \"--color-primary-light-800-alpha-800\": \"rgba(244, 221, 219, 0.20)\",\n        \"--color-primary-light-800-alpha-900\": \"rgba(244, 221, 219, 0.10)\",\n        \"--color-primary-light-900\": \"rgb(246,228,226)\",\n        \"--color-primary-light-900-alpha-100\": \"rgba(246, 228, 226, 0.90)\",\n        \"--color-primary-light-900-alpha-200\": \"rgba(246, 228, 226, 0.80)\",\n        \"--color-primary-light-900-alpha-300\": \"rgba(246, 228, 226, 0.70)\",\n        \"--color-primary-light-900-alpha-400\": \"rgba(246, 228, 226, 0.60)\",\n        \"--color-primary-light-900-alpha-500\": \"rgba(246, 228, 226, 0.50)\",\n        \"--color-primary-light-900-alpha-600\": \"rgba(246, 228, 226, 0.40)\",\n        \"--color-primary-light-900-alpha-700\": \"rgba(246, 228, 226, 0.30)\",\n        \"--color-primary-light-900-alpha-800\": \"rgba(246, 228, 226, 0.20)\",\n        \"--color-primary-light-900-alpha-900\": \"rgba(246, 228, 226, 0.10)\",\n        \"--color-primary-light-1000\": \"rgb(255,255,255)\",\n        \"--color-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"--color-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"--color-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"--color-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"--color-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"--color-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"--color-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"--color-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"--color-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"--color-theme\": \"rgb(192, 57, 43)\",\n        \"--color-1000\": \"rgb(33, 33, 33)\",\n        \"--color-950\": \"rgb(44,44,44)\",\n        \"--color-900\": \"rgb(55,55,55)\",\n        \"--color-850\": \"rgb(66,66,66)\",\n        \"--color-800\": \"rgb(77,77,77)\",\n        \"--color-750\": \"rgb(89,89,89)\",\n        \"--color-700\": \"rgb(100,100,100)\",\n        \"--color-650\": \"rgb(111,111,111)\",\n        \"--color-600\": \"rgb(122,122,122)\",\n        \"--color-550\": \"rgb(133,133,133)\",\n        \"--color-500\": \"rgb(144,144,144)\",\n        \"--color-450\": \"rgb(155,155,155)\",\n        \"--color-400\": \"rgb(166,166,166)\",\n        \"--color-350\": \"rgb(177,177,177)\",\n        \"--color-300\": \"rgb(188,188,188)\",\n        \"--color-250\": \"rgb(200,200,200)\",\n        \"--color-200\": \"rgb(211,211,211)\",\n        \"--color-150\": \"rgb(222,222,222)\",\n        \"--color-100\": \"rgb(233,233,233)\",\n        \"--color-050\": \"rgb(244,244,244)\",\n        \"--color-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"--color-app-background\": \"rgba(255, 255, 255, 0.15)\",\n        \"--color-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"--color-nav-font\": \"var(--color-primary)\",\n        \"--background-image\": \"url(./theme_images/xnkl.png)\",\n        \"--background-image-position\": \"center\",\n        \"--background-image-size\": \"cover\",\n        \"--color-btn-hide\": \"#3bc2b2\",\n        \"--color-btn-min\": \"#85c43b\",\n        \"--color-btn-close\": \"#fab4a0\",\n        \"--color-badge-primary\": \"#7fb575\",\n        \"--color-badge-secondary\": \"#dfbb6b\",\n        \"--color-badge-tertiary\": \"var(--color-primary-light-100)\"\n      }\n    }\n  }\n]"
  },
  {
    "path": "src/common/theme/utils.js",
    "content": "const { RGB_Linear_Shade, RGB_Alpha_Shade } = require('./colorUtils')\n\nexports.createThemeColors = (rgbaColor, fontRgbaColor, isDark, isDarkFont) => {\n  const colors = {\n    '--color-primary': rgbaColor,\n  }\n\n  let preColor = rgbaColor\n  for (let i = 1; i < 11; i += 1) {\n    preColor = RGB_Linear_Shade(isDark ? 0.2 : -0.1, preColor)\n    colors[`--color-primary-dark-${i * 100}`] = preColor\n    for (let j = 1; j < 10; j += 1) {\n      colors[`--color-primary-dark-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)\n      colors[`--color-primary-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, rgbaColor)\n    }\n  }\n  preColor = rgbaColor\n  for (let i = 1; i < 10; i += 1) {\n    preColor = RGB_Linear_Shade(isDark ? -0.1 : 0.2, preColor)\n    colors[`--color-primary-light-${i * 100}`] = preColor\n    for (let j = 1; j < 10; j += 1) {\n      colors[`--color-primary-light-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)\n    }\n  }\n  preColor = RGB_Linear_Shade(isDark ? -0.35 : 1, preColor)\n  colors[`--color-primary-light-${1000}`] = preColor\n  for (let j = 1; j < 10; j += 1) {\n    colors[`--color-primary-light-${1000}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)\n  }\n\n  colors['--color-theme'] = isDark ? colors['--color-primary-light-900'] : rgbaColor\n\n  return { ...colors, ...createFontColors(fontRgbaColor, isDark, isDarkFont) }\n}\n\nconst createFontColors = (rgbaColor, isDark, isDarkFont) => {\n  // rgb(238, 238, 238)\n  // let prec = 'rgb(255, 255, 255)'\n  rgbaColor ??= isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)'\n  if (isDark) return createFontDarkColors(rgbaColor, isDarkFont)\n\n  let colors = {\n    '--color-1000': rgbaColor,\n  }\n  let step = (isDarkFont ? 0.02 : 0.05) * (isDark ? -1 : 1)\n  for (let i = 1; i < 21; i += 1) {\n    colors[`--color-${String(1000 - 50 * i).padStart(3, '0')}`] = RGB_Linear_Shade(step * i, rgbaColor)\n  }\n  // console.log(colors)\n  return colors\n}\n\nconst createFontDarkColors = (rgbaColor, isDarkFont) => {\n  // rgb(238, 238, 238)\n  // let prec = 'rgb(255, 255, 255)'\n\n  let colors = {\n    '--color-1000': rgbaColor,\n  }\n  const step = isDarkFont ? -0.015 : -0.05\n  let preColor = rgbaColor\n  for (let i = 1; i < 21; i += 1) {\n    preColor = RGB_Linear_Shade(step, preColor)\n    colors[`--color-${String(1000 - 50 * i).padStart(3, '0')}`] = preColor\n  }\n\n  // console.log(colors)\n  return colors\n}\n\n// console.log(createFontColors('rgb(33, 33, 33)', false))\n// console.log(createFontColors('rgb(255, 255, 255)', true))\n\n// console.log(createFontDarkColors('rgb(255, 255, 255)'))\n\n"
  },
  {
    "path": "src/common/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"typeRoots\": [\n      \"./types\"\n    ],\n    \"paths\": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */\n      \"@common/*\": [\"common/*\"],\n    },\n  },\n}\n"
  },
  {
    "path": "src/common/types/app_setting.d.ts",
    "content": "import type { I18n } from '../../lang/i18n'\n\ndeclare global {\n\n  declare namespace LX {\n    type AddMusicLocationType = 'top' | 'bottom'\n\n    interface AppSetting {\n      version: string\n\n      /**\n       * 窗口大小id\n       */\n      'common.windowSizeId': number\n\n      /**\n       * 窗口大小id\n       */\n      'common.fontSize': number\n\n      /**\n       * 是否以全屏启动\n       */\n      'common.startInFullscreen': boolean\n\n      /**\n       * 语言id\n       */\n      'common.langId': I18n['locale'] | null\n\n      /**\n       * api id\n       */\n      'common.apiSource': string\n\n      /**\n       * 音源名称类型，原名、别名\n       */\n      'common.sourceNameType': 'alias' | 'real'\n\n      /**\n       * 显示的字体\n       */\n      'common.font': string\n\n      /**\n       * 是否启用动画\n       */\n      'common.isShowAnimation': boolean\n\n      /**\n       * 是否启用随机弹窗动画\n       */\n      'common.randomAnimate': boolean\n\n      /**\n       * 是否同意软件协议\n       */\n      'common.isAgreePact': boolean\n\n      /**\n       * 控制按钮位置，左边、右边\n       */\n      'common.controlBtnPosition': 'left' | 'right'\n\n      /**\n       * 播放栏进度条样式\n       */\n      'common.playBarProgressStyle': 'mini' | 'full' | 'middle'\n\n      /**\n       * 启用透明窗口\n       */\n      'common.transparentWindow': boolean\n\n      /**\n       * 尝试自动更新\n       */\n      'common.tryAutoUpdate': boolean\n\n      /**\n       * 更新版本后是否显示变更日志\n       */\n      'common.showChangeLog': boolean\n\n      /**\n       * 启动时自动播放歌曲\n       */\n      'player.startupAutoPlay': boolean\n\n      /**\n       * 切歌模式\n       */\n      'player.togglePlayMethod': 'listLoop' | 'random' | 'list' | 'singleLoop' | 'none'\n\n      /**\n       * 优先播放的音质\n       */\n      'player.playQuality': LX.Quality\n\n      /**\n       * 是否显示任务栏进度条\n       */\n      'player.isShowTaskProgess': boolean\n\n\n      /**\n       * 是否将歌词显示在状态栏\n       */\n      'player.isShowStatusBarLyric': boolean\n\n      /**\n       * 音量大小\n       */\n      'player.volume': number\n\n      /**\n       * 播放歌曲时是否阻止电脑休眠\n       */\n      'player.powerSaveBlocker': boolean\n\n      /**\n       * 是否静音\n       */\n      'player.isMute': boolean\n\n      /**\n       * 播放速率\n       */\n      'player.playbackRate': number\n\n      /**\n       * 是否自动调整音频的音高以补偿对播放速率设置所做的更改\n       */\n      'player.preservesPitch': boolean\n\n      /**\n       * 使用设备能处理的最大声道数输出音频\n       */\n      'player.isMaxOutputChannelCount': boolean\n\n      /**\n       * 音频输出设备id\n       */\n      'player.mediaDeviceId': string\n\n      /**\n       * 是否在音频输出设备更改时暂停播放\n       */\n      'player.isMediaDeviceRemovedStopPlay': boolean\n\n      /**\n       * 是否显示歌词翻译\n       */\n      'player.isShowLyricTranslation': boolean\n\n      /**\n       * 是否显示歌词罗马音\n       */\n      'player.isShowLyricRoma': boolean\n\n      /**\n       * 是否调换翻译歌词与罗马音歌词位置\n       */\n      'player.isSwapLyricTranslationAndRoma': boolean\n\n      /**\n       * 是否将歌词从简体转换为繁体\n       */\n      'player.isS2t': boolean\n\n      /**\n       * 是否播放卡拉OK歌词\n       */\n      'player.isPlayLxlrc': boolean\n\n      /**\n       * 启动软件时是否恢复上次播放进度\n       */\n      'player.isSavePlayTime': boolean\n\n      /**\n       * 是否启用音频可视化\n       */\n      'player.audioVisualization': boolean\n\n      /**\n       * 定时暂停播放-是否等待歌曲播放完毕再暂停\n       */\n      'player.waitPlayEndStop': boolean\n\n      /**\n       * 定时暂停播放-倒计时时间\n       */\n      'player.waitPlayEndStopTime': string\n\n      /**\n       * 环境音效文件名\n       */\n      'player.soundEffect.convolution.fileName': string | null\n\n      /**\n       * 环境音效原始输出增益\n       */\n      'player.soundEffect.convolution.mainGain': number\n\n      /**\n       * 环境音效输出增益\n       */\n      'player.soundEffect.convolution.sendGain': number\n\n      /**\n       * 均衡器 31hz 值\n       */\n      'player.soundEffect.biquadFilter.hz31': number\n\n      /**\n       * 均衡器 62hz 值\n       */\n      'player.soundEffect.biquadFilter.hz62': number\n\n      /**\n       * 均衡器 125hz 值\n       */\n      'player.soundEffect.biquadFilter.hz125': number\n\n      /**\n       * 均衡器 250hz 值\n       */\n      'player.soundEffect.biquadFilter.hz250': number\n\n      /**\n       * 均衡器 500hz 值\n       */\n      'player.soundEffect.biquadFilter.hz500': number\n\n      /**\n       * 均衡器 1000hz 值\n       */\n      'player.soundEffect.biquadFilter.hz1000': number\n\n      /**\n       * 均衡器 2000hz 值\n       */\n      'player.soundEffect.biquadFilter.hz2000': number\n\n      /**\n       * 均衡器 4000hz 值\n       */\n      'player.soundEffect.biquadFilter.hz4000': number\n\n      /**\n       * 均衡器 8000hz 值\n       */\n      'player.soundEffect.biquadFilter.hz8000': number\n\n      /**\n       * 均衡器 16000hz 值\n       */\n      'player.soundEffect.biquadFilter.hz16000': number\n\n      /**\n       * 3D立体环绕是否启用\n       */\n      'player.soundEffect.panner.enable': boolean\n\n      /**\n       * 3D立体环绕声音距离\n       */\n      'player.soundEffect.panner.soundR': number\n\n      /**\n       * 3D立体环绕速度\n       */\n      'player.soundEffect.panner.speed': number\n\n      /**\n       * 升降声调\n       */\n      'player.soundEffect.pitchShifter.playbackRate': number\n\n      /**\n       * 是否启用音频加载失败时自动切歌\n       */\n      'player.autoSkipOnError': boolean\n\n      /**\n       * 点击相同列表内的歌曲切歌时是否清空已播放列表（随机模式下列表内所有歌曲会重新参与随机）\n       */\n      'player.isAutoCleanPlayedList': boolean\n\n      /**\n       * 播放详情页-是否缩放当前播放的歌词行\n       */\n      'playDetail.isZoomActiveLrc': boolean\n\n      /**\n       * 播放详情页-是否允许通过歌词调整播放进度\n       */\n      'playDetail.isShowLyricProgressSetting': boolean\n\n      /**\n       * 播放详情页-歌词字体大小\n       */\n      'playDetail.style.fontSize': number\n\n      /**\n       * 播放详情页-歌词对齐方式\n       */\n      'playDetail.style.align': 'center' | 'left' | 'right'\n\n      /**\n       * 播放详情页-是否延迟桌面歌词滚动\n       */\n      'playDetail.isDelayScroll': boolean\n\n\n      /**\n       * 是否启用桌面歌词\n       */\n      'desktopLyric.enable': boolean\n\n      /**\n       * 是否锁定桌面歌词\n       */\n      'desktopLyric.isLock': boolean\n\n      /**\n       * 是在置顶桌面\n       */\n      'desktopLyric.isAlwaysOnTop': boolean\n\n      /**\n       * 是否自动刷新歌词置顶\n       */\n      'desktopLyric.isAlwaysOnTopLoop': boolean\n\n      /**\n       * 是否将歌词进程显示在任务栏\n       */\n      'desktopLyric.isShowTaskbar': boolean\n\n      /**\n       * 是否启用音频可视化\n       */\n      'desktopLyric.audioVisualization': boolean\n\n      /**\n       * 是否在全屏时隐藏歌词\n       */\n      'desktopLyric.fullscreenHide': boolean\n\n      /**\n       * 是否在暂停时隐藏歌词\n       */\n      'desktopLyric.pauseHide': boolean\n\n      /**\n       * 桌面歌词窗口宽度\n       */\n      'desktopLyric.width': number\n\n      /**\n       * 桌面歌词窗口高度\n       */\n      'desktopLyric.height': number\n\n      /**\n       * 桌面歌词窗口x坐标\n       */\n      'desktopLyric.x': number | null\n\n      /**\n       * 桌面歌词窗口y坐标\n       */\n      'desktopLyric.y': number | null\n\n      /**\n       * 是否允许桌面歌词窗口拖出主屏幕之外\n       */\n      'desktopLyric.isLockScreen': boolean\n\n      /**\n       * 是否延迟桌面歌词滚动\n       */\n      'desktopLyric.isDelayScroll': boolean\n\n      /**\n       * 歌词滚动位置\n       */\n      'desktopLyric.scrollAlign': 'top' | 'center'\n\n      /**\n       * 是否在鼠标划过桌面歌词窗口时降低歌词透明度\n       */\n      'desktopLyric.isHoverHide': boolean\n\n      /**\n       * 歌词方向\n       */\n      'desktopLyric.direction': 'horizontal' | 'vertical'\n\n      /**\n       * 歌词对齐方式\n       */\n      'desktopLyric.style.align': 'center' | 'left' | 'right'\n\n      /**\n       * 桌面歌词字体\n       */\n      'desktopLyric.style.font': string\n\n      /**\n       * 桌面歌词字体大小\n       */\n      'desktopLyric.style.fontSize': number\n\n      /**\n       * 歌词间距大小\n       */\n      'desktopLyric.style.lineGap': number\n\n      /**\n       * 桌面歌词未播放字体颜色\n       */\n      'desktopLyric.style.lyricUnplayColor': string\n\n      /**\n       * 桌面歌词已播放字体颜色\n       */\n      'desktopLyric.style.lyricPlayedColor': string\n\n      /**\n       * 桌面歌词字体阴影颜色\n       */\n      'desktopLyric.style.lyricShadowColor': string\n\n      /**\n       * 桌面歌词加粗字体\n       */\n      // 'desktopLyric.style.fontWeight': boolean\n\n      /**\n       * 桌面歌词字体透明度\n       */\n      'desktopLyric.style.opacity': number\n\n      /**\n       * 桌面歌词是否允许换行\n       */\n      'desktopLyric.style.ellipsis': boolean\n\n      /**\n       * 是否缩放当前正在播放的桌面歌词\n       */\n      'desktopLyric.style.isZoomActiveLrc': boolean\n\n      /**\n       * 是否加粗逐字歌词字体\n       */\n      'desktopLyric.style.isFontWeightFont': boolean\n\n      /**\n       * 是否加粗逐行歌词字体\n       */\n      'desktopLyric.style.isFontWeightLine': boolean\n\n      /**\n       * 是否加粗翻译、罗马音字体\n       */\n      'desktopLyric.style.isFontWeightExtended': boolean\n\n      /**\n       * 是否启用双击列表里的歌曲时自动切换到当前列表播放（仅对歌单、排行榜有效）\n       */\n      'list.isClickPlayList': boolean\n\n      /**\n       * 是否显示歌曲来源（仅对我的列表有效）\n       */\n      'list.isShowSource': boolean\n\n      /**\n       * 是否自动恢复列表滚动位置（仅对我的列表有效）\n       */\n      'list.isSaveScrollLocation': boolean\n\n      /**\n       * 添加歌曲到我的列表时的方式\n       */\n      'list.addMusicLocationType': LX.AddMusicLocationType\n\n      /**\n       * 是否显示列表操作按钮列\n       */\n      'list.actionButtonsVisible': boolean\n\n      /**\n       * 是否启用下载功能\n       */\n      'download.enable': boolean\n\n      /**\n       * 按列表名分组保存\n       */\n      'download.isSavePathGroupByListName': boolean\n\n      /**\n       * 下载路径\n       */\n      'download.savePath': string\n\n      /**\n       * 文件命名方式\n       */\n      'download.fileName': '歌名 - 歌手' | '歌手 - 歌名' | '歌名'\n\n      /**\n       * 最大并发下载数\n       */\n      'download.maxDownloadNum': number\n\n      /**\n       * 存在同名文件时跳过下载\n       */\n      'download.skipExistFile': boolean\n\n      /**\n       * 是否下载lrc文件\n       */\n      'download.isDownloadLrc': boolean\n\n      /**\n       * 是否在下载 lx 歌词\n       */\n      'download.isDownloadLxLrc': boolean\n\n      /**\n       * 是否下载翻译歌词文件\n       */\n      'download.isDownloadTLrc': boolean\n\n      /**\n       * 是否下载罗马音歌词文件\n       */\n      'download.isDownloadRLrc': boolean\n\n      /**\n       * 保存lrc时的文本编码格式\n       */\n      'download.lrcFormat': 'utf8' | 'gbk'\n\n      /**\n       * 是否在音频文件中嵌入歌曲封面\n       */\n      'download.isEmbedPic': boolean\n\n      /**\n       * 是否在音频文件中嵌入 lx 歌词\n       */\n      'download.isEmbedLyricLx': boolean\n\n      /**\n       * 是否在音频文件中嵌入歌词\n       */\n      'download.isEmbedLyric': boolean\n\n      /**\n       * 是否在音频文件中嵌入翻译歌词\n       */\n      'download.isEmbedLyricT': boolean\n\n      /**\n       * 是否在音频文件中嵌入罗马音歌词\n       */\n      'download.isEmbedLyricR': boolean\n\n      /**\n       * 歌曲源不可用时，是否启用换源下载\n       */\n      'download.isUseOtherSource': boolean\n\n      /**\n       * 主题id\n       */\n      'theme.id': string\n\n      /**\n       * 亮色主题id\n       */\n      'theme.lightId': string\n\n      /**\n       * 暗色主题id\n       */\n      'theme.darkId': string\n\n      /**\n       * 是否显示热门搜索\n       */\n      'search.isShowHotSearch': boolean\n\n      /**\n       * 是否显示搜索历史\n       */\n      'search.isShowHistorySearch': boolean\n\n      /**\n       * 软件启动时是否自动聚焦搜索框\n       */\n      'search.isFocusSearchBox': boolean\n\n      /**\n       * 是否启用代理\n       */\n      'network.proxy.enable': boolean\n\n      /**\n       * 代理服务器地址\n       */\n      'network.proxy.host': string\n\n      /**\n       * 代理服务器端口号\n       */\n      'network.proxy.port': string\n\n      /**\n       * 是否启用托盘\n       */\n      'tray.enable': boolean\n\n      /**\n       * 是否关闭时是否最小化到托盘\n       */\n      // 'tray.isToTray': boolean\n\n      /**\n       * 托盘主题id\n       */\n      'tray.themeId': number\n\n      /**\n       * 同步服务模式\n       */\n      'sync.mode': 'server' | 'client'\n\n      /**\n       * 是否启用同步服务\n       */\n      'sync.enable': boolean\n\n      /**\n       * 同步服务端口号\n       */\n      'sync.server.port': '23332' | string\n\n      /**\n       * 最大备份快照数\n       */\n      'sync.server.maxSsnapshotNum': number\n\n      /**\n       * 同步服务地址\n       */\n      'sync.client.host': string\n\n\n      /**\n       * 是否启用开放API服务\n       */\n      'openAPI.enable': boolean\n\n      /**\n       * API服务端口号\n       */\n      'openAPI.port': '23330' | string\n\n      /**\n       * 是否绑定到局域网\n       */\n      'openAPI.bindLan': boolean\n\n      /**\n       * 是否在离开搜索界面时自动清空搜索框\n       */\n      'odc.isAutoClearSearchInput': boolean\n\n      /**\n       * 是否在离开搜索界面时自动清空搜索结果列表\n       */\n      'odc.isAutoClearSearchList': boolean\n    }\n  }\n\n}\n"
  },
  {
    "path": "src/common/types/common.d.ts",
    "content": "// import './app_setting'\n\ndeclare namespace LX {\n  interface CmdParams {\n    /**\n     * 搜索，启动软件时自动在搜索框搜索指定的内容，例如：-search=\"突然的自我 - 伍佰\"\n     */\n    search?: string\n\n    /**\n     * 禁用硬件加速启动\n     */\n    dha?: boolean\n\n    /**\n     * 以非透明模式启动\n     */\n    dt?: boolean\n\n    /**\n     * 禁用硬件媒体密钥处理\n     */\n    dhmkh?: boolean\n\n    /**\n     * 设置代理服务器，代理应用的所有流量，例：-proxy-server=\"127.0.0.1:1081\"（不支持设置账号密码，v1.17.0起新增）。注：应用内“设置-网络-代理设置”仅代理接口请求的流量，优先级更高\n     */\n    'proxy-server'?: string\n\n    /**\n     * 以分号分隔的主机列表绕过代理服务器，例：-proxy-bypass-list=\"<local>;*.google.com;*foo.com;1.2.3.4:5678\"（与-proxy-server一起使用才有效，v1.17.0起新增）。注：此设置对应用内接口请求无效\n     */\n    'proxy-bypass-list'?: string\n\n    /**\n     * 启动时播放指定列表的音乐\n     */\n    play?: string\n\n    /**\n     * 启动后最小化到系统托盘\n     */\n    hidden?: boolean\n\n    [key: string]: boolean | number | string\n  }\n\n  type OnlineSource = 'kw' | 'kg' | 'tx' | 'wy' | 'mg'\n  type Source = OnlineSource | 'local'\n  type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | '192k' | 'ape' | 'wav'\n\n  type QualityList = Partial<Record<LX.Source, LX.Quality[]>>\n\n  interface EnvParams {\n    deeplink?: string | null\n    cmdParams: CmdParams\n    workAreaSize?: Electron.Size\n  }\n\n  interface HotKey {\n    name: string\n    action: string\n    type: keyof typeof keyName\n  }\n\n  interface HotKeyDownInfo {\n    type: 'local' | 'global'\n    key: string\n  }\n\n  interface HotKeyConfig {\n    enable: boolean\n    keys: Record<string, HotKey>\n  }\n  interface HotKeyConfigAll {\n    local: HotKeyConfig\n    global: HotKeyConfig\n  }\n  interface RegisterKeyInfo {\n    key: string\n    info: HotKey\n  }\n  type HotKeyState = Map<string, {\n    status: boolean\n    info: HotKey\n  }>\n  interface HotKeyActionWrap<T, D> {\n    action: T\n    data: D\n    source?: string\n  }\n  type HotKeyActions = HotKeyActionWrap<'config', HotKeyConfigAll>\n  | HotKeyActionWrap<'enable', boolean>\n  | HotKeyActionWrap<'register', RegisterKeyInfo>\n  | HotKeyActionWrap<'unregister', string>\n\n  interface HotKeyEvent {\n    type: string\n    key: string\n  }\n\n  interface TaskBarButtonFlags {\n    empty: boolean\n    collect: boolean\n    play: boolean\n    next: boolean\n    prev: boolean\n  }\n\n  type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle'\n  interface VersionInfo {\n    version: string\n    desc: string\n  }\n}\n"
  },
  {
    "path": "src/common/types/config_files.d.ts",
    "content": "declare namespace LX {\n  namespace ConfigFile {\n    interface MyListInfoPart {\n      type: 'playListPart_v2'\n      data: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/common/types/desktop_lyric.d.ts",
    "content": "declare namespace LX {\n  namespace DesktopLyric {\n    interface Config {\n      'desktopLyric.enable': LX.AppSetting['desktopLyric.enable']\n      'desktopLyric.isLock': LX.AppSetting['desktopLyric.isLock']\n      'desktopLyric.isAlwaysOnTop': LX.AppSetting['desktopLyric.isAlwaysOnTop']\n      'desktopLyric.isAlwaysOnTopLoop': LX.AppSetting['desktopLyric.isAlwaysOnTopLoop']\n      'desktopLyric.isShowTaskbar': LX.AppSetting['desktopLyric.isShowTaskbar']\n      'desktopLyric.pauseHide': LX.AppSetting['desktopLyric.pauseHide']\n      'desktopLyric.audioVisualization': LX.AppSetting['desktopLyric.audioVisualization']\n      'desktopLyric.width': LX.AppSetting['desktopLyric.width']\n      'desktopLyric.height': LX.AppSetting['desktopLyric.height']\n      'desktopLyric.x': LX.AppSetting['desktopLyric.x']\n      'desktopLyric.y': LX.AppSetting['desktopLyric.y']\n      'desktopLyric.isLockScreen': LX.AppSetting['desktopLyric.isLockScreen']\n      'desktopLyric.isDelayScroll': LX.AppSetting['desktopLyric.isDelayScroll']\n      'desktopLyric.scrollAlign': LX.AppSetting['desktopLyric.scrollAlign']\n      'desktopLyric.isHoverHide': LX.AppSetting['desktopLyric.isHoverHide']\n      'desktopLyric.direction': LX.AppSetting['desktopLyric.direction']\n      'desktopLyric.style.align': LX.AppSetting['desktopLyric.style.align']\n      'desktopLyric.style.font': LX.AppSetting['desktopLyric.style.font']\n      'desktopLyric.style.fontSize': LX.AppSetting['desktopLyric.style.fontSize']\n      'desktopLyric.style.lineGap': LX.AppSetting['desktopLyric.style.lineGap']\n      'desktopLyric.style.lyricUnplayColor': LX.AppSetting['desktopLyric.style.lyricUnplayColor']\n      'desktopLyric.style.lyricPlayedColor': LX.AppSetting['desktopLyric.style.lyricPlayedColor']\n      'desktopLyric.style.lyricShadowColor': LX.AppSetting['desktopLyric.style.lyricShadowColor']\n      // 'desktopLyric.style.fontWeight': LX.AppSetting['desktopLyric.style.fontWeight']\n      'desktopLyric.style.opacity': LX.AppSetting['desktopLyric.style.opacity']\n      'desktopLyric.style.ellipsis': LX.AppSetting['desktopLyric.style.ellipsis']\n      'desktopLyric.style.isFontWeightFont': LX.AppSetting['desktopLyric.style.isFontWeightFont']\n      'desktopLyric.style.isFontWeightLine': LX.AppSetting['desktopLyric.style.isFontWeightLine']\n      'desktopLyric.style.isFontWeightExtended': LX.AppSetting['desktopLyric.style.isFontWeightExtended']\n      'desktopLyric.style.isZoomActiveLrc': LX.AppSetting['desktopLyric.style.isZoomActiveLrc']\n      'common.langId': LX.AppSetting['common.langId']\n      'player.isShowLyricTranslation': LX.AppSetting['player.isShowLyricTranslation']\n      'player.isShowLyricRoma': LX.AppSetting['player.isShowLyricRoma']\n      'player.isSwapLyricTranslationAndRoma': LX.AppSetting['player.isSwapLyricTranslationAndRoma']\n      'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc']\n      'player.playbackRate': LX.AppSetting['player.playbackRate']\n    }\n\n    type WinMainActions = 'get_info' | 'get_status' | 'get_analyser_data_array'\n\n    interface LyricActionBase <A> {\n      action: A\n    }\n    interface LyricActionData<A, D> extends LyricActionBase<A> {\n      data: D\n    }\n    type LyricAction<A, D = undefined> = D extends undefined ? LyricActionBase<A> : LyricActionData<A, D>\n\n    type LyricActions = LyricAction<'set_info', {\n      id: string | null\n      singer: string\n      name: string\n      album: string\n      lrc: string | null\n      tlrc: string | null\n      rlrc: string | null\n      lxlrc: string | null\n      // pic: string | null\n      isPlay: boolean\n      line: number\n      played_time: number\n    }>\n    | LyricAction<'set_status', {\n      isPlay: boolean\n      line: number\n      played_time: number\n    }>\n    | LyricAction<'set_lyric', {\n      lrc: string | null\n      tlrc: string | null\n      rlrc: string | null\n      lxlrc: string | null\n    }>\n    | LyricAction<'set_offset', number>\n    | LyricAction<'set_playbackRate', number>\n    | LyricAction<'set_play', number>\n    | LyricAction<'set_pause'>\n    | LyricAction<'set_stop'>\n    | LyricAction<'send_analyser_data_array', Uint8Array>\n\n\n    interface NewBounds {\n      x: number\n      y: number\n      w: number\n      h: number\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/dislike_list.d.ts",
    "content": "\n\ndeclare namespace LX {\n  namespace Dislike {\n    // interface ListItemMusicText {\n    //   id?: string\n    //   // type: 'music'\n    //   name: string | null\n    //   singer: string | null\n    // }\n    // interface ListItemMusic {\n    //   id?: number\n    //   type: 'musicId'\n    //   musicId: string\n    //   meta: LX.Music.MusicInfo\n    // }\n    // type ListItem = ListItemMusicText\n    // type ListItem = string\n    // type ListItem = ListItemMusic | ListItemMusicText\n\n    interface DislikeMusicInfo {\n      name: string\n      singer: string\n    }\n\n    type DislikeRules = string\n\n    interface DislikeInfo {\n      // musicIds: Set<string>\n      names: Set<string>\n      musicNames: Set<string>\n      singerNames: Set<string>\n      // list: LX.Dislike.ListItem[]\n      rules: DislikeRules\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/dislike_list_sync.d.ts",
    "content": "declare namespace LX {\n\n  namespace Sync {\n    namespace Dislike {\n      interface ListInfo {\n        lastSyncDate?: number\n        snapshotKey: string\n      }\n\n      interface SyncActionBase <A> {\n        action: A\n      }\n      interface SyncActionData<A, D> extends SyncActionBase<A> {\n        data: D\n      }\n      type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>\n      type ActionList = SyncAction<'dislike_data_overwrite', LX.Dislike.DislikeRules>\n      | SyncAction<'dislike_music_add', LX.Dislike.DislikeMusicInfo[]>\n      | SyncAction<'dislike_music_clear'>\n\n      type SyncMode = 'merge_local_remote'\n      | 'merge_remote_local'\n      | 'overwrite_local_remote'\n      | 'overwrite_remote_local'\n      // | 'none'\n      | 'cancel'\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/common/types/download_list.d.ts",
    "content": "import { type Message } from '@root/lang'\n\n// interface DownloadList {\n\n// }\n\n\ndeclare global {\n  namespace LX {\n    namespace Download {\n      type DownloadTaskStatus = 'run'\n      | 'waiting'\n      | 'pause'\n      | 'error'\n      | 'completed'\n\n      type FileExt = 'mp3' | 'flac' | 'wav' | 'ape'\n\n      interface ProgressInfo {\n        progress: number\n        speed: string\n        downloaded: number\n        total: number\n        writeQueue: number\n      }\n\n      interface DownloadTaskActionBase <A> {\n        action: A\n      }\n      interface DownloadTaskActionData<A, D> extends DownloadTaskActionBase<A> {\n        data: D\n      }\n      type DownloadTaskAction<A, D = undefined> = D extends undefined ? DownloadTaskActionBase<A> : DownloadTaskActionData<A, D>\n\n      type DownloadTaskActions = DownloadTaskAction<'start'>\n      | DownloadTaskAction<'complete'>\n      | DownloadTaskAction<'refreshUrl'>\n      | DownloadTaskAction<'statusText', string>\n      | DownloadTaskAction<'progress', ProgressInfo>\n      | DownloadTaskAction<'error', {\n        error?: keyof Message\n        message?: string\n      }>\n\n      interface ListItem {\n        id: string\n        isComplate: boolean\n        status: DownloadTaskStatus\n        statusText: string\n        downloaded: number\n        total: number\n        progress: number\n        speed: string\n        writeQueue: number\n        metadata: {\n          musicInfo: LX.Music.MusicInfoOnline\n          url: string | null\n          quality: LX.Quality\n          ext: FileExt\n          fileName: string\n          filePath: string\n          listId?: string\n        }\n      }\n\n      interface saveDownloadMusicInfo {\n        list: ListItem[]\n        addMusicLocationType: LX.AddMusicLocationType\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/ipc_main.d.ts",
    "content": "declare namespace LX {\n  interface IpcMainEvent {\n    event: Electron.IpcMainEvent\n  }\n  interface IpcMainEventParams<T> {\n    event: Electron.IpcMainEvent\n    params: T\n  }\n  type IpcMainEventListener = (params: LX.IpcMainEvent) => void\n  type IpcMainEventListenerParams<T> = (params: LX.IpcMainEventParams<T>) => void\n\n  interface IpcMainInvokeEvent {\n    event: Electron.IpcMainInvokeEvent\n  }\n  interface IpcMainInvokeEventParams<T> {\n    event: Electron.IpcMainInvokeEvent\n    params: T\n  }\n\n  type IpcMainInvokeEventListener = (params: LX.IpcMainInvokeEvent) => Promise<void>\n  type IpcMainInvokeEventListenerParams<T> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<void>\n  type IpcMainInvokeEventListenerValue<V> = (params: LX.IpcMainInvokeEvent) => Promise<V>\n  type IpcMainInvokeEventListenerParamsValue<T, V> = (params: LX.IpcMainInvokeEventParams<T>) => Promise<V>\n}\n"
  },
  {
    "path": "src/common/types/ipc_renderer.d.ts",
    "content": "declare namespace LX {\n  interface IpcRendererEvent {\n    event: Electron.IpcRendererEvent\n  }\n  interface IpcRendererEventParams<T> {\n    event: Electron.IpcRendererEvent\n    params: T\n  }\n  type IpcRendererEventListener = (params: LX.IpcRendererEvent) => any\n  type IpcRendererEventListenerParams<T> = (params: LX.IpcRendererEventParams<T>) => any\n}\n"
  },
  {
    "path": "src/common/types/list.d.ts",
    "content": "declare namespace LX {\n  namespace List {\n    interface UserListInfo {\n      id: string\n      name: string\n      // list: LX.Music.MusicInfo[]\n      source?: LX.OnlineSource\n      sourceListId?: string\n      // position?: number\n      locationUpdateTime: number | null\n    }\n\n    interface MyDefaultListInfo {\n      id: 'default'\n      name: 'list__name_default'\n      // name: '试听列表'\n      // list: LX.Music.MusicInfo[]\n    }\n\n    interface MyLoveListInfo {\n      id: 'love'\n      name: 'list__name_love'\n      // name: '我的收藏'\n      // list: LX.Music.MusicInfo[]\n    }\n\n    interface MyTempListInfo {\n      id: 'temp'\n      name: '临时列表'\n      // list: LX.Music.MusicInfo[]\n      // TODO: save default lists info\n      meta: {\n        id?: string\n      }\n    }\n\n    type MyListInfo = MyDefaultListInfo | MyLoveListInfo | UserListInfo\n\n    interface MyAllList {\n      defaultList: MyDefaultListInfo\n      loveList: MyLoveListInfo\n      userList: UserListInfo[]\n      tempList: MyTempListInfo\n    }\n\n\n    type SearchHistoryList = string[]\n    type ListPositionInfo = Record<string, number>\n    type ListUpdateInfo = Record<string, {\n      updateTime: number\n      isAutoUpdate: boolean\n    }>\n\n    type ListSaveType = 'myList' | 'downloadList'\n    type ListSaveInfo = {\n      type: 'myList'\n      data: Partial<MyAllList>\n    } | {\n      type: 'downloadList'\n      data: LX.Download.ListItem[]\n    }\n\n\n    type ListActionDataOverwrite = MakeOptional<LX.List.ListDataFull, 'tempList'>\n    interface ListActionAdd {\n      position: number\n      listInfos: UserListInfo[]\n    }\n    type ListActionRemove = string[]\n    type ListActionUpdate = UserListInfo[]\n    interface ListActionUpdatePosition {\n      /**\n       * 列表id\n       */\n      ids: string[]\n      /**\n       * 位置\n       */\n      position: number\n    }\n\n    interface ListActionMusicAdd {\n      id: string\n      musicInfos: LX.Music.MusicInfo[]\n      addMusicLocationType: LX.AddMusicLocationType\n    }\n\n    interface ListActionMusicMove {\n      fromId: string\n      toId: string\n      musicInfos: LX.Music.MusicInfo[]\n      addMusicLocationType: LX.AddMusicLocationType\n    }\n\n    interface ListActionCheckMusicExistList {\n      listId: string\n      musicInfoId: string\n    }\n\n    interface ListActionMusicRemove {\n      listId: string\n      ids: string[]\n    }\n\n    type ListActionMusicUpdate = Array<{\n      id: string\n      musicInfo: LX.Music.MusicInfo\n    }>\n\n    interface ListActionMusicUpdatePosition {\n      listId: string\n      position: number\n      ids: string[]\n    }\n\n    interface ListActionMusicOverwrite {\n      listId: string\n      musicInfos: LX.Music.MusicInfo[]\n    }\n\n    type ListActionMusicClear = string[]\n\n    interface MyDefaultListInfoFull extends MyDefaultListInfo {\n      list: LX.Music.MusicInfo[]\n    }\n    interface MyLoveListInfoFull extends MyLoveListInfo {\n      list: LX.Music.MusicInfo[]\n    }\n    interface UserListInfoFull extends UserListInfo {\n      list: LX.Music.MusicInfo[]\n    }\n    interface MyTempListInfoFull extends MyTempListInfo {\n      list: LX.Music.MusicInfo[]\n    }\n\n    interface ListDataFull {\n      defaultList: LX.Music.MusicInfo[]\n      loveList: LX.Music.MusicInfo[]\n      userList: UserListInfoFull[]\n      tempList: LX.Music.MusicInfo[]\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/list_sync.d.ts",
    "content": "declare namespace LX {\n\n  namespace Sync {\n    namespace List {\n      interface ListInfo {\n        lastSyncDate?: number\n        snapshotKey: string\n      }\n\n      type ActionList = LX.Sync.SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>\n      | SyncAction<'list_create', LX.List.ListActionAdd>\n      | SyncAction<'list_remove', LX.List.ListActionRemove>\n      | SyncAction<'list_update', LX.List.ListActionUpdate>\n      | SyncAction<'list_update_position', LX.List.ListActionUpdatePosition>\n      | SyncAction<'list_music_add', LX.List.ListActionMusicAdd>\n      | SyncAction<'list_music_move', LX.List.ListActionMusicMove>\n      | SyncAction<'list_music_remove', LX.List.ListActionMusicRemove>\n      | SyncAction<'list_music_update', LX.List.ListActionMusicUpdate>\n      | SyncAction<'list_music_update_position', LX.List.ListActionMusicUpdatePosition>\n      | SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>\n      | SyncAction<'list_music_clear', LX.List.ListActionMusicClear>\n\n      type ListData = Omit<LX.List.ListDataFull, 'tempList'>\n      type SyncMode = 'merge_local_remote'\n      | 'merge_remote_local'\n      | 'overwrite_local_remote'\n      | 'overwrite_remote_local'\n      | 'overwrite_local_remote_full'\n      | 'overwrite_remote_local_full'\n      // | 'none'\n      | 'cancel'\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/music.d.ts",
    "content": "declare namespace LX {\n  namespace Music {\n    interface MusicQualityType { // {\"type\": \"128k\", size: \"3.56M\"}\n      type: LX.Quality\n      size: string | null\n    }\n    interface MusicQualityTypeKg { // {\"type\": \"128k\", size: \"3.56M\"}\n      type: LX.Quality\n      size: string | null\n      hash: string\n    }\n    type _MusicQualityType = Partial<Record<Quality, {\n      size: string | null\n    }>>\n    type _MusicQualityTypeKg = Partial<Record<Quality, {\n      size: string | null\n      hash: string\n    }>>\n\n\n    interface MusicInfoMetaBase {\n      songId: string | number // 歌曲ID，mg源为copyrightId，local为文件路径\n      albumName: string // 歌曲专辑名称\n      picUrl?: string | null // 歌曲图片链接\n      toggleMusicInfo?: MusicInfoOnline | null\n    }\n\n    interface MusicInfoMeta_online extends MusicInfoMetaBase {\n      qualitys: MusicQualityType[]\n      _qualitys: _MusicQualityType\n      albumId?: string | number // 歌曲专辑ID\n    }\n\n    interface MusicInfoMeta_local extends MusicInfoMetaBase {\n      filePath: string\n      ext: string\n    }\n\n\n    interface MusicInfoBase<S = LX.Source> {\n      id: string\n      name: string // 歌曲名\n      singer: string // 艺术家名\n      source: S // 源\n      interval: string | null // 格式化后的歌曲时长，例：03:55\n      meta: MusicInfoMetaBase\n    }\n\n    interface MusicInfoLocal extends MusicInfoBase<'local'> {\n      meta: MusicInfoMeta_local\n    }\n\n    interface MusicInfo_online_common extends MusicInfoBase<'kw' | 'wy'> {\n      meta: MusicInfoMeta_online\n    }\n\n    interface MusicInfoMeta_kg extends MusicInfoMeta_online {\n      qualitys: MusicQualityTypeKg[]\n      _qualitys: _MusicQualityTypeKg\n      hash: string // 歌曲hash\n    }\n    interface MusicInfo_kg extends MusicInfoBase<'kg'> {\n      meta: MusicInfoMeta_kg\n    }\n\n    interface MusicInfoMeta_tx extends MusicInfoMeta_online {\n      strMediaMid: string // 歌曲strMediaMid\n      id?: number // 歌曲songId\n      albumMid?: string // 歌曲albumMid\n    }\n    interface MusicInfo_tx extends MusicInfoBase<'tx'> {\n      meta: MusicInfoMeta_tx\n    }\n\n    interface MusicInfoMeta_mg extends MusicInfoMeta_online {\n      copyrightId: string // 歌曲copyrightId\n      lrcUrl?: string // 歌曲lrcUrl\n      mrcUrl?: string // 歌曲mrcUrl\n      trcUrl?: string // 歌曲trcUrl\n    }\n    interface MusicInfo_mg extends MusicInfoBase<'mg'> {\n      meta: MusicInfoMeta_mg\n    }\n\n    type MusicInfoOnline = MusicInfo_online_common | MusicInfo_kg | MusicInfo_tx | MusicInfo_mg\n    type MusicInfo = MusicInfoOnline | MusicInfoLocal\n\n    interface LyricInfo {\n      // 歌曲歌词\n      lyric: string\n      // 翻译歌词\n      tlyric?: string | null\n      // 罗马音歌词\n      rlyric?: string | null\n      // 逐字歌词\n      lxlyric?: string | null\n    }\n\n    interface LyricInfoSave {\n      id: string\n      lyrics: LyricInfo\n    }\n\n    interface MusicFileMeta {\n      title: string\n      artist: string | null\n      album: string | null\n      APIC: string | null\n      lyrics: string | null\n    }\n\n    interface MusicUrlInfo {\n      id: string\n      url: string\n    }\n\n    interface MusicInfoOtherSourceSave {\n      id: string\n      list: MusicInfoOnline[]\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/common/types/music_metadata.d.ts",
    "content": "import {\n  type IAudioMetadata as iAudioMetadata,\n} from 'music-metadata'\n\ndeclare global {\n  namespace LX {\n    namespace MusicMetadataModule {\n      type IAudioMetadata = iAudioMetadata\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/open_api.d.ts",
    "content": "declare namespace LX {\n  namespace OpenAPI {\n    interface Status {\n      status: boolean\n      message: string\n      address: string\n    }\n    interface EnableServer {\n      enable: boolean\n      port: string\n      bindLan: boolean\n    }\n\n    interface ActionBase <A> {\n      action: A\n    }\n    interface ActionData<A, D> extends ActionBase<A> {\n      data: D\n    }\n    type Action<A, D = undefined> = D extends undefined ? ActionBase<A> : ActionData<A, D>\n\n    type Actions = Action<'status'>\n    | Action<'enable', EnableServer>\n\n  }\n}\n"
  },
  {
    "path": "src/common/types/player.d.ts",
    "content": "declare namespace LX {\n  namespace Player {\n    interface ProgressBarOptions {\n      progress: number\n      mode?: Electron.ProgressBarOptions['mode']\n    }\n\n    type StatusButtonActions = 'unCollect'\n    | 'collect'\n    | 'prev'\n    | 'pause'\n    | 'play'\n    | 'next'\n    | 'seek'\n    | 'volume'\n    | 'mute'\n\n    interface LyricInfo extends LX.Music.LyricInfo {\n      rawlrcInfo: LX.Music.LyricInfo\n    }\n\n    interface Status {\n      status: 'playing' | 'paused' | 'error' | 'stoped'\n      name: string\n      singer: string\n      albumName: string\n      picUrl: string\n      progress: number\n      duration: number\n      playbackRate: number\n      lyricLineText: string\n      lyricLineAllText: string\n      lyric: string\n      tlyric: string\n      rlyric: string\n      lxlyric: string\n      collect: boolean\n      volume: number\n      mute: boolean\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/shims_vue.d.ts",
    "content": "// declare module '*.vue' {\n//   import { App } from 'vue'\n//   export default App.Component\n// }\n\ndeclare module '*.vue' {\n  import { type Component } from 'vue'\n  const component: Component\n  export default component\n}\n"
  },
  {
    "path": "src/common/types/sound_effect.d.ts",
    "content": "declare namespace LX {\n  namespace SoundEffect {\n    interface EQPreset {\n      id: string\n      name: string\n      hz31: number\n      hz62: number\n      hz125: number\n      hz250: number\n      hz500: number\n      hz1000: number\n      hz2000: number\n      hz4000: number\n      hz8000: number\n      hz16000: number\n    }\n    interface ConvolutionPreset {\n      id: string\n      name: string\n      source: string\n      mainGain: number\n      sendGain: number\n    }\n    // interface PitchShifterPreset {\n    //   id: string\n    //   name: string\n    //   playbackRate: number\n    // }\n  }\n}\n"
  },
  {
    "path": "src/common/types/sync.d.ts",
    "content": "declare namespace LX {\n  namespace Sync {\n\n    interface EnableServer {\n      enable: boolean\n      port: string\n    }\n    interface EnableClient {\n      enable: boolean\n      host: string\n      authCode?: string\n    }\n\n    interface SyncActionBase <A> {\n      action: A\n    }\n    interface SyncActionData<A, D> extends SyncActionBase<A> {\n      data: D\n    }\n    type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>\n\n\n    interface ModeTypes {\n      list: LX.Sync.List.SyncMode\n      dislike: LX.Sync.Dislike.SyncMode\n    }\n\n    type ModeType = { [K in keyof ModeTypes]: { type: K, mode: ModeTypes[K] } }[keyof ModeTypes]\n\n    type SyncMainWindowActions = SyncAction<'select_mode', { deviceName: string, type: keyof ModeTypes }>\n    | SyncAction<'close_select_mode'>\n    | SyncAction<'client_status', ClientStatus>\n    | SyncAction<'server_status', ServerStatus>\n\n    type SyncServiceActions = SyncAction<'select_mode', ModeType>\n    | SyncAction<'get_server_status'>\n    | SyncAction<'get_client_status'>\n    | SyncAction<'generate_code'>\n    | SyncAction<'enable_server', EnableServer>\n    | SyncAction<'enable_client', EnableClient>\n\n    type ServerDevices = ServerKeyInfo[]\n\n    interface ServerStatus {\n      status: boolean\n      message: string\n      address: string[]\n      code: string\n      devices: ServerKeyInfo[]\n    }\n\n    interface ClientStatus {\n      status: boolean\n      message: string\n      address: string[]\n    }\n\n    interface ClientKeyInfo {\n      clientId: string\n      key: string\n      serverName: string\n    }\n\n    interface ServerKeyInfo {\n      clientId: string\n      key: string\n      deviceName: string\n      lastConnectDate?: number\n      isMobile: boolean\n    }\n\n    interface ListConfig {\n      skipSnapshot: boolean\n    }\n    interface DislikeConfig {\n      skipSnapshot: boolean\n    }\n    type ServerType = 'desktop-app' | 'server'\n    interface EnabledFeatures {\n      list?: false | ListConfig\n      dislike?: false | DislikeConfig\n    }\n    type SupportedFeatures = Partial<{ [k in keyof EnabledFeatures]: number }>\n  }\n}\n"
  },
  {
    "path": "src/common/types/theme.d.ts",
    "content": "declare namespace LX {\n\n  interface ThemeColors {\n    '--color-000': string\n    '--color-050': string\n    '--color-100': string\n    '--color-150': string\n    '--color-200': string\n    '--color-250': string\n    '--color-300': string\n    '--color-350': string\n    '--color-400': string\n    '--color-450': string\n    '--color-500': string\n    '--color-550': string\n    '--color-600': string\n    '--color-650': string\n    '--color-700': string\n    '--color-750': string\n    '--color-800': string\n    '--color-850': string\n    '--color-900': string\n    '--color-950': string\n    '--color-1000': string\n\n\n    '--color-theme': string\n\n    '--color-primary': string\n    '--color-primary-alpha-100': string\n    '--color-primary-alpha-200': string\n    '--color-primary-alpha-300': string\n    '--color-primary-alpha-400': string\n    '--color-primary-alpha-500': string\n    '--color-primary-alpha-600': string\n    '--color-primary-alpha-700': string\n    '--color-primary-alpha-800': string\n    '--color-primary-alpha-900': string\n\n    '--color-primary-dark-100': string\n    '--color-primary-dark-100-alpha-100': string\n    '--color-primary-dark-100-alpha-200': string\n    '--color-primary-dark-100-alpha-300': string\n    '--color-primary-dark-100-alpha-400': string\n    '--color-primary-dark-100-alpha-500': string\n    '--color-primary-dark-100-alpha-600': string\n    '--color-primary-dark-100-alpha-700': string\n    '--color-primary-dark-100-alpha-800': string\n    '--color-primary-dark-100-alpha-900': string\n\n    '--color-primary-dark-200': string\n    '--color-primary-dark-200-alpha-100': string\n    '--color-primary-dark-200-alpha-200': string\n    '--color-primary-dark-200-alpha-300': string\n    '--color-primary-dark-200-alpha-400': string\n    '--color-primary-dark-200-alpha-500': string\n    '--color-primary-dark-200-alpha-600': string\n    '--color-primary-dark-200-alpha-700': string\n    '--color-primary-dark-200-alpha-800': string\n    '--color-primary-dark-200-alpha-900': string\n\n    '--color-primary-dark-300': string\n    '--color-primary-dark-300-alpha-100': string\n    '--color-primary-dark-300-alpha-200': string\n    '--color-primary-dark-300-alpha-300': string\n    '--color-primary-dark-300-alpha-400': string\n    '--color-primary-dark-300-alpha-500': string\n    '--color-primary-dark-300-alpha-600': string\n    '--color-primary-dark-300-alpha-700': string\n    '--color-primary-dark-300-alpha-800': string\n    '--color-primary-dark-300-alpha-900': string\n\n    '--color-primary-dark-400': string\n    '--color-primary-dark-400-alpha-100': string\n    '--color-primary-dark-400-alpha-200': string\n    '--color-primary-dark-400-alpha-300': string\n    '--color-primary-dark-400-alpha-400': string\n    '--color-primary-dark-400-alpha-500': string\n    '--color-primary-dark-400-alpha-600': string\n    '--color-primary-dark-400-alpha-700': string\n    '--color-primary-dark-400-alpha-800': string\n    '--color-primary-dark-400-alpha-900': string\n\n    '--color-primary-dark-500': string\n    '--color-primary-dark-500-alpha-100': string\n    '--color-primary-dark-500-alpha-200': string\n    '--color-primary-dark-500-alpha-300': string\n    '--color-primary-dark-500-alpha-400': string\n    '--color-primary-dark-500-alpha-500': string\n    '--color-primary-dark-500-alpha-600': string\n    '--color-primary-dark-500-alpha-700': string\n    '--color-primary-dark-500-alpha-800': string\n    '--color-primary-dark-500-alpha-900': string\n\n    '--color-primary-dark-600': string\n    '--color-primary-dark-600-alpha-100': string\n    '--color-primary-dark-600-alpha-200': string\n    '--color-primary-dark-600-alpha-300': string\n    '--color-primary-dark-600-alpha-400': string\n    '--color-primary-dark-600-alpha-500': string\n    '--color-primary-dark-600-alpha-600': string\n    '--color-primary-dark-600-alpha-700': string\n    '--color-primary-dark-600-alpha-800': string\n    '--color-primary-dark-600-alpha-900': string\n\n    '--color-primary-dark-700': string\n    '--color-primary-dark-700-alpha-100': string\n    '--color-primary-dark-700-alpha-200': string\n    '--color-primary-dark-700-alpha-300': string\n    '--color-primary-dark-700-alpha-400': string\n    '--color-primary-dark-700-alpha-500': string\n    '--color-primary-dark-700-alpha-600': string\n    '--color-primary-dark-700-alpha-700': string\n    '--color-primary-dark-700-alpha-800': string\n    '--color-primary-dark-700-alpha-900': string\n\n    '--color-primary-dark-800': string\n    '--color-primary-dark-800-alpha-100': string\n    '--color-primary-dark-800-alpha-200': string\n    '--color-primary-dark-800-alpha-300': string\n    '--color-primary-dark-800-alpha-400': string\n    '--color-primary-dark-800-alpha-500': string\n    '--color-primary-dark-800-alpha-600': string\n    '--color-primary-dark-800-alpha-700': string\n    '--color-primary-dark-800-alpha-800': string\n    '--color-primary-dark-800-alpha-900': string\n\n    '--color-primary-dark-900': string\n    '--color-primary-dark-900-alpha-100': string\n    '--color-primary-dark-900-alpha-200': string\n    '--color-primary-dark-900-alpha-300': string\n    '--color-primary-dark-900-alpha-400': string\n    '--color-primary-dark-900-alpha-500': string\n    '--color-primary-dark-900-alpha-600': string\n    '--color-primary-dark-900-alpha-700': string\n    '--color-primary-dark-900-alpha-800': string\n    '--color-primary-dark-900-alpha-900': string\n\n    '--color-primary-dark-1000': string\n    '--color-primary-dark-1000-alpha-100': string\n    '--color-primary-dark-1000-alpha-200': string\n    '--color-primary-dark-1000-alpha-300': string\n    '--color-primary-dark-1000-alpha-400': string\n    '--color-primary-dark-1000-alpha-500': string\n    '--color-primary-dark-1000-alpha-600': string\n    '--color-primary-dark-1000-alpha-700': string\n    '--color-primary-dark-1000-alpha-800': string\n    '--color-primary-dark-1000-alpha-900': string\n\n    '--color-primary-light-100': string\n    '--color-primary-light-100-alpha-100': string\n    '--color-primary-light-100-alpha-200': string\n    '--color-primary-light-100-alpha-300': string\n    '--color-primary-light-100-alpha-400': string\n    '--color-primary-light-100-alpha-500': string\n    '--color-primary-light-100-alpha-600': string\n    '--color-primary-light-100-alpha-700': string\n    '--color-primary-light-100-alpha-800': string\n    '--color-primary-light-100-alpha-900': string\n\n    '--color-primary-light-200': string\n    '--color-primary-light-200-alpha-100': string\n    '--color-primary-light-200-alpha-200': string\n    '--color-primary-light-200-alpha-300': string\n    '--color-primary-light-200-alpha-400': string\n    '--color-primary-light-200-alpha-500': string\n    '--color-primary-light-200-alpha-600': string\n    '--color-primary-light-200-alpha-700': string\n    '--color-primary-light-200-alpha-800': string\n    '--color-primary-light-200-alpha-900': string\n\n    '--color-primary-light-300': string\n    '--color-primary-light-300-alpha-100': string\n    '--color-primary-light-300-alpha-200': string\n    '--color-primary-light-300-alpha-300': string\n    '--color-primary-light-300-alpha-400': string\n    '--color-primary-light-300-alpha-500': string\n    '--color-primary-light-300-alpha-600': string\n    '--color-primary-light-300-alpha-700': string\n    '--color-primary-light-300-alpha-800': string\n    '--color-primary-light-300-alpha-900': string\n\n    '--color-primary-light-400': string\n    '--color-primary-light-400-alpha-100': string\n    '--color-primary-light-400-alpha-200': string\n    '--color-primary-light-400-alpha-300': string\n    '--color-primary-light-400-alpha-400': string\n    '--color-primary-light-400-alpha-500': string\n    '--color-primary-light-400-alpha-600': string\n    '--color-primary-light-400-alpha-700': string\n    '--color-primary-light-400-alpha-800': string\n    '--color-primary-light-400-alpha-900': string\n\n    '--color-primary-light-500': string\n    '--color-primary-light-500-alpha-100': string\n    '--color-primary-light-500-alpha-200': string\n    '--color-primary-light-500-alpha-300': string\n    '--color-primary-light-500-alpha-400': string\n    '--color-primary-light-500-alpha-500': string\n    '--color-primary-light-500-alpha-600': string\n    '--color-primary-light-500-alpha-700': string\n    '--color-primary-light-500-alpha-800': string\n    '--color-primary-light-500-alpha-900': string\n\n    '--color-primary-light-600': string\n    '--color-primary-light-600-alpha-100': string\n    '--color-primary-light-600-alpha-200': string\n    '--color-primary-light-600-alpha-300': string\n    '--color-primary-light-600-alpha-400': string\n    '--color-primary-light-600-alpha-500': string\n    '--color-primary-light-600-alpha-600': string\n    '--color-primary-light-600-alpha-700': string\n    '--color-primary-light-600-alpha-800': string\n    '--color-primary-light-600-alpha-900': string\n\n    '--color-primary-light-700': string\n    '--color-primary-light-700-alpha-100': string\n    '--color-primary-light-700-alpha-200': string\n    '--color-primary-light-700-alpha-300': string\n    '--color-primary-light-700-alpha-400': string\n    '--color-primary-light-700-alpha-500': string\n    '--color-primary-light-700-alpha-600': string\n    '--color-primary-light-700-alpha-700': string\n    '--color-primary-light-700-alpha-800': string\n    '--color-primary-light-700-alpha-900': string\n\n    '--color-primary-light-800': string\n    '--color-primary-light-800-alpha-100': string\n    '--color-primary-light-800-alpha-200': string\n    '--color-primary-light-800-alpha-300': string\n    '--color-primary-light-800-alpha-400': string\n    '--color-primary-light-800-alpha-500': string\n    '--color-primary-light-800-alpha-600': string\n    '--color-primary-light-800-alpha-700': string\n    '--color-primary-light-800-alpha-800': string\n    '--color-primary-light-800-alpha-900': string\n\n    '--color-primary-light-900': string\n    '--color-primary-light-900-alpha-100': string\n    '--color-primary-light-900-alpha-200': string\n    '--color-primary-light-900-alpha-300': string\n    '--color-primary-light-900-alpha-400': string\n    '--color-primary-light-900-alpha-500': string\n    '--color-primary-light-900-alpha-600': string\n    '--color-primary-light-900-alpha-700': string\n    '--color-primary-light-900-alpha-800': string\n    '--color-primary-light-900-alpha-900': string\n\n    '--color-primary-light-1000': string\n    '--color-primary-light-1000-alpha-100': string\n    '--color-primary-light-1000-alpha-200': string\n    '--color-primary-light-1000-alpha-300': string\n    '--color-primary-light-1000-alpha-400': string\n    '--color-primary-light-1000-alpha-500': string\n    '--color-primary-light-1000-alpha-600': string\n    '--color-primary-light-1000-alpha-700': string\n    '--color-primary-light-1000-alpha-800': string\n    '--color-primary-light-1000-alpha-900': string\n  }\n\n  interface Theme {\n    id: string\n    name: string\n    isDark: boolean\n    isDarkFont: boolean\n    isCustom: boolean\n    config: {\n      themeColors: ThemeColors\n      extInfo: {\n        '--color-app-background': string\n        '--color-main-background': string\n        '--color-nav-font': string\n        '--background-image': string\n        '--background-image-position': string\n        '--background-image-size': string\n\n        // 关闭按钮颜色\n        '--color-btn-hide': string\n        '--color-btn-min': string\n        '--color-btn-close': string\n\n        // 徽章颜色\n        '--color-badge-primary': string\n        '--color-badge-secondary': string\n        '--color-badge-tertiary': string\n\n      }\n    }\n  }\n\n  interface ThemeInfo {\n    themes: LX.Theme[]\n    userThemes: LX.Theme[]\n    dataPath: string\n  }\n\n  interface ThemeSetting {\n    shouldUseDarkColors: boolean\n    theme: {\n      id: string\n      name: string\n      isDark: boolean\n      colors: Record<string, string>\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/types/user_api.d.ts",
    "content": "declare namespace LX {\n  namespace UserApi {\n    type UserApiSourceInfoType = 'music'\n    type UserApiSourceInfoActions = 'musicUrl' | 'lyric' | 'pic'\n\n    interface UserApiSourceInfo {\n      name: string\n      type: UserApiSourceInfoType\n      actions: UserApiSourceInfoActions[]\n      qualitys: LX.Quality[]\n    }\n\n    type UserApiSources = Record<LX.Source, UserApiSourceInfo>\n\n\n    interface UserApiInfoFull {\n      id: string\n      name: string\n      description: string\n      script: string\n      allowShowUpdateAlert: boolean\n      author?: string\n      homepage?: string\n      version?: string\n      sources?: UserApiSources\n    }\n\n    type UserApiInfo = Omit<UserApiInfoFull, 'script'>\n\n    interface UserApiStatus {\n      status: boolean\n      message?: string\n      apiInfo?: UserApiInfo\n    }\n\n    interface UserApiUpdateInfo {\n      name: string\n      description: string\n      log: string\n      updateUrl?: string\n    }\n\n    interface UserApiRequestParams {\n      requestKey: string\n      data: any\n    }\n    type UserApiRequestCancelParams = string\n    type UserApiSetApiParams = string\n\n    interface UserApiSetAllowUpdateAlertParams {\n      id: string\n      enable: boolean\n    }\n\n    interface ImportUserApi {\n      apiInfo: UserApiInfo\n      apiList: UserApiInfo[]\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/common/types/utils.d.ts",
    "content": "type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> & Partial<Pick<Type, Key>>\n\ntype DeepPartial<T> = {\n  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];\n}\n\ntype Modify<T, R> = Omit<T, keyof R> & R\n\n// type UndefinedOrNever = undefined\ntype Actions<T extends { action: string, data?: any }> = {\n  [U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined\n}\n\ntype WarpPromiseValue<T> = T extends ((...args: infer P) => Promise<infer R>)\n  ? ((...args: P) => Promise<R>)\n  : T extends ((...args: infer P2) => infer R2)\n    ? ((...args: P2) => Promise<R2>)\n    : Promise<T>\n\ntype WarpPromiseRecord<T extends Record<string, any>> = {\n  [K in keyof T]: WarpPromiseValue<T[K]>\n}\n"
  },
  {
    "path": "src/common/utils/common.ts",
    "content": "// 非业务工具方法\n\n/**\n * 获取两个数之间的随机整数，大于等于min，小于max\n * @param {*} min\n * @param {*} max\n */\nexport const getRandom = (min: number, max: number): number => Math.floor(Math.random() * (max - min)) + min\n\n\nexport const sizeFormate = (size: number): string => {\n  // https://gist.github.com/thomseddon/3511330\n  if (!size) return '0 B'\n  let units = ['B', 'KB', 'MB', 'GB', 'TB']\n  let number = Math.floor(Math.log(size) / Math.log(1024))\n  return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`\n}\n\n/**\n * 将字符串、时间戳等格式转成时间对象\n * @param date 时间\n * @returns 时间对象或空字符串\n */\nexport const toDateObj = (date: any): Date | '' => {\n  // console.log(date)\n  if (!date) return ''\n  switch (typeof date) {\n    case 'string':\n      if (!date.includes('T')) date = date.split('.')[0].replace(/-/g, '/')\n    // eslint-disable-next-line no-fallthrough\n    case 'number':\n      date = new Date(date)\n    // eslint-disable-next-line no-fallthrough\n    case 'object':\n      break\n    default: return ''\n  }\n  return date\n}\n\nconst numFix = (n: number): string => n < 10 ? (`0${n}`) : n.toString()\n/**\n * 时间格式化\n * @param _date 时间\n * @param format Y-M-D h:m:s Y年 M月 D日 h时 m分 s秒\n */\nexport const dateFormat = (_date: any, format = 'Y-M-D h:m:s') => {\n  // console.log(date)\n  const date = toDateObj(_date)\n  if (!date) return ''\n  return format\n    .replace('Y', date.getFullYear().toString())\n    .replace('M', numFix(date.getMonth() + 1))\n    .replace('D', numFix(date.getDate()))\n    .replace('h', numFix(date.getHours()))\n    .replace('m', numFix(date.getMinutes()))\n    .replace('s', numFix(date.getSeconds()))\n}\n\n\nexport const formatPlayTime = (time: number) => {\n  let m = Math.trunc(time / 60)\n  let s = Math.trunc(time % 60)\n  return m == 0 && s == 0 ? '--/--' : numFix(m) + ':' + numFix(s)\n}\n\nexport const formatPlayTime2 = (time: number) => {\n  let m = Math.trunc(time / 60)\n  let s = Math.trunc(time % 60)\n  return numFix(m) + ':' + numFix(s)\n}\n\n\nexport const isUrl = (path: string) => /https?:\\/\\//.test(path)\n\n// 解析URL参数为对象\nexport const parseUrlParams = (str: string): Record<string, string> => {\n  const params: Record<string, string> = {}\n  if (typeof str !== 'string') return params\n  const paramsArr = str.split('&')\n  for (const param of paramsArr) {\n    let [key, value] = param.split('=')\n    params[key] = value\n  }\n  return params\n}\n\n/**\n * 生成节流函数\n * @param fn 回调\n * @param delay 延迟\n * @returns\n */\nexport function throttle<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {\n  let timer: NodeJS.Timeout | null = null\n  let _args: Args\n  return (...args: Args) => {\n    _args = args\n    if (timer) return\n    timer = setTimeout(() => {\n      timer = null\n      void fn(..._args)\n    }, delay)\n  }\n}\n\n/**\n * 生成防抖函数\n * @param fn 回调\n * @param delay 延迟\n * @returns\n */\nexport function debounce<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {\n  let timer: NodeJS.Timeout | null = null\n  let _args: Args\n  return (...args: Args) => {\n    _args = args\n    if (timer) clearTimeout(timer)\n    timer = setTimeout(() => {\n      timer = null\n      void fn(..._args)\n    }, delay)\n  }\n}\n\nconst fileNameRxp = /[\\\\/:*?#\"<>|]/g\nexport const filterFileName = (name: string): string => name.replace(fileNameRxp, '')\n\n\n// https://blog.csdn.net/xcxy2015/article/details/77164126#comments\n/**\n *\n * @param a\n * @param b\n */\nexport const similar = (a: string, b: string) => {\n  if (!a || !b) return 0\n  if (a.length > b.length) { // 保证 a <= b\n    let t = b\n    b = a\n    a = t\n  }\n  let al = a.length\n  let bl = b.length\n  let mp = [] // 一个表\n  let i, j, ai, lt, tmp // ai：字符串a的第i个字符。 lt：左上角的值。 tmp：暂存新的值。\n  for (i = 0; i <= bl; i++) mp[i] = i\n  for (i = 1; i <= al; i++) {\n    ai = a.charAt(i - 1)\n    lt = mp[0]\n    mp[0] = mp[0] + 1\n    for (j = 1; j <= bl; j++) {\n      tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))\n      lt = mp[j]\n      mp[j] = tmp\n    }\n  }\n  return 1 - (mp[bl] / bl)\n}\n\n/**\n * 排序字符串\n * @param arr\n * @param data\n */\nexport const sortInsert = <T>(arr: Array<{ num: number, data: T }>, data: { num: number, data: T }) => {\n  let key = data.num\n  let left = 0\n  let right = arr.length - 1\n\n  while (left <= right) {\n    let middle = Math.trunc((left + right) / 2)\n    if (key == arr[middle].num) {\n      left = middle\n      break\n    } else if (key < arr[middle].num) {\n      right = middle - 1\n    } else {\n      left = middle + 1\n    }\n  }\n  while (left > 0) {\n    if (arr[left - 1].num != key) break\n    left--\n  }\n\n  arr.splice(left, 0, data)\n}\n\nexport const encodePath = (path: string) => {\n  return encodeURI(path.replaceAll('\\\\', '/'))\n}\n\n\nexport const arrPush = <T>(list: T[], newList: T[]) => {\n  for (let i = 0; i * 1000 < newList.length; i++) {\n    list.push(...newList.slice(i * 1000, (i + 1) * 1000))\n  }\n  return list\n}\n\nexport const arrUnshift = <T>(list: T[], newList: T[]) => {\n  for (let i = 0; i * 1000 < newList.length; i++) {\n    list.splice(i * 1000, 0, ...newList.slice(i * 1000, (i + 1) * 1000))\n  }\n  return list\n}\n\nexport const arrPushByPosition = <T>(list: T[], newList: T[], position: number) => {\n  for (let i = 0; i * 1000 < newList.length; i++) {\n    list.splice(position + i * 1000, 0, ...newList.slice(i * 1000, (i + 1) * 1000))\n  }\n  return list\n}\n\n\n// https://stackoverflow.com/a/2450976\nexport const arrShuffle = <T>(array: T[]) => {\n  let currentIndex = array.length\n  let randomIndex\n\n  // While there remain elements to shuffle.\n  while (currentIndex != 0) {\n    // Pick a remaining element.\n    randomIndex = Math.floor(Math.random() * currentIndex)\n    currentIndex--;\n\n    // And swap it with the current element.\n    [array[currentIndex], array[randomIndex]] = [\n      array[randomIndex], array[currentIndex]]\n  }\n\n  return array\n}\n"
  },
  {
    "path": "src/common/utils/download/Downloader.ts",
    "content": "import fs from 'fs'\nimport path from 'path'\nimport { EventEmitter } from 'events'\nimport { performance } from 'perf_hooks'\nimport { STATUS } from './util'\nimport type http from 'http'\nimport { request, type Options as RequestOptions } from './request'\n\nexport interface Options {\n  forceResume: boolean\n  timeout: number\n  requestOptions: RequestOptions\n}\n\nconst defaultChunkInfo = {\n  path: '',\n  startByte: '0',\n  endByte: '',\n}\n\nconst defaultRequestOptions: Options['requestOptions'] = {\n  method: 'get',\n  headers: {},\n}\nconst defaultOptions: Options = {\n  forceResume: true,\n  timeout: 20_000,\n  requestOptions: { ...defaultRequestOptions },\n}\n\nclass Task extends EventEmitter {\n  resumeLastChunk: Buffer | null\n  downloadUrl: string\n  chunkInfo: { path: string, startByte: string, endByte: string }\n  status: typeof STATUS[keyof typeof STATUS]\n  options: Options\n  requestOptions: Options['requestOptions']\n  ws: fs.WriteStream | null = null\n  progress = { total: 0, downloaded: 0, speed: 0, progress: 0 }\n  statsEstimate = { time: 0, bytes: 0, prevBytes: 0 }\n  requestInstance: http.ClientRequest | null = null\n  maxRedirectNum = 2\n  private redirectNum = 0\n  private dataWriteQueueLength = 0\n  private closeWaiting = false\n  private timeout: null | NodeJS.Timeout = null\n\n\n  constructor(url: string, savePath: string, filename: string, options: Partial<Options> = {}) {\n    super()\n\n    this.resumeLastChunk = null\n    this.downloadUrl = url\n    this.chunkInfo = Object.assign({}, defaultChunkInfo, {\n      path: path.join(savePath, filename),\n      startByte: '0',\n    })\n    // if (!this.chunkInfo.endByte) this.chunkInfo.endByte = ''\n\n    this.options = Object.assign({}, defaultOptions, options)\n    this.requestOptions = Object.assign({}, defaultRequestOptions, this.options.requestOptions || {})\n    this.requestOptions.headers = this.requestOptions.headers ? { ...this.requestOptions.headers } : {}\n\n    this.status = STATUS.idle\n  }\n\n  async __init() {\n    const { path, startByte, endByte } = this.chunkInfo\n    this.redirectNum = 0\n    this.progress.downloaded = 0\n    this.progress.progress = 0\n    this.progress.speed = 0\n    this.dataWriteQueueLength = 0\n    this.closeWaiting = false\n    this.__clearTimeout()\n    this.__startTimeout()\n    if (startByte) this.requestOptions.headers!.range = `bytes=${startByte}-${endByte}`\n\n    if (!path) return\n    return new Promise<void>((resolve, reject) => {\n      fs.stat(path, (errStat, stats) => {\n        if (errStat) {\n          // console.log(errStat.code)\n          if (errStat.code !== 'ENOENT') {\n            this.__handleError(errStat)\n            reject(errStat)\n            return\n          }\n        } else if (stats.size >= 10) {\n          fs.open(path, 'r', (errOpen, fd) => {\n            if (errOpen) {\n              this.__handleError(errOpen)\n              reject(errOpen)\n              return\n            }\n            fs.read(fd, Buffer.alloc(10), 0, 10, stats.size - 10, (errRead, bytesRead, buffer) => {\n              if (errRead) {\n                this.__handleError(errRead)\n                reject(errRead)\n                return\n              }\n              fs.close(fd, errClose => {\n                if (errClose) {\n                  this.__handleError(errClose)\n                  reject(errClose)\n                  return\n                }\n\n                // resume download\n                // console.log(buffer)\n                this.resumeLastChunk = buffer\n                this.progress.downloaded = stats.size\n                this.requestOptions.headers!.range = `bytes=${stats.size - 10}-${endByte || ''}`\n                resolve()\n              })\n            })\n          })\n          return\n        }\n        resolve()\n      })\n    })\n  }\n\n  __httpFetch(url: string, options: Options['requestOptions']) {\n    // console.log(options)\n    let redirected = false\n    this.requestInstance = request(url, options)\n      .on('response', response => {\n        if (response.statusCode !== 200 && response.statusCode !== 206) {\n          if (response.statusCode == 416) {\n            fs.unlink(this.chunkInfo.path, (err) => {\n              this.__handleError(new Error(response.statusMessage))\n              this.chunkInfo.startByte = '0'\n              this.resumeLastChunk = null\n              this.progress.downloaded = 0\n              if (err) this.__handleError(err)\n            })\n            return\n          }\n          if ((response.statusCode == 301 || response.statusCode == 302) && response.headers.location && this.redirectNum < this.maxRedirectNum) {\n            console.log('current url:', url)\n            console.log('redirect to:', response.headers.location)\n            redirected = true\n            this.redirectNum++\n            const location = response.headers.location\n            this.__httpFetch(location, options)\n            return\n          }\n          this.status = STATUS.failed\n          this.emit('fail', response)\n          this.__clearTimeout()\n          this.__closeRequest()\n          void this.__closeWriteStream()\n          return\n        }\n        this.emit('response', response)\n        try {\n          this.__initDownload(response)\n        } catch (error: any) {\n          this.__handleError(error)\n          return\n        }\n        this.status = STATUS.running\n        this.__startTimeout()\n        response\n          .on('data', this.__handleWriteData.bind(this))\n          .on('error', err => { this.__handleError(err) })\n          .on('end', () => {\n            if (response.complete) {\n              this.__handleComplete()\n            } else {\n              // this.__handleError(new Error('The connection was terminated while the message was still being sent'))\n              void this.stop()\n            }\n          })\n      })\n      .on('error', err => { this.__handleError(err) })\n      .on('close', () => {\n        if (redirected) return\n        void this.__closeWriteStream()\n      })\n      .end()\n  }\n\n  __initDownload(response: http.IncomingMessage) {\n    this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0\n    if (!this.progress.total) {\n      this.__handleError(new Error('Content length is 0'))\n      return\n    }\n    let options: any = {}\n    let isResumable = this.options.forceResume ||\n      response.headers['accept-ranges'] !== 'none' ||\n      (typeof response.headers['accept-ranges'] == 'string' &&\n        parseInt(response.headers['accept-ranges'].replace(/^bytes=(\\d+)/, '$1')) > 0)\n\n    if (isResumable) {\n      options.flags = 'a'\n      if (this.progress.downloaded) this.progress.total -= 10\n    } else {\n      if (this.chunkInfo.startByte != '0') {\n        this.__handleError(new Error('The resource cannot be resumed download.'))\n        return\n      }\n    }\n    this.progress.total += this.progress.downloaded\n    this.statsEstimate.prevBytes = this.progress.downloaded\n    if (!this.chunkInfo.path) {\n      this.__handleError(new Error('Chunk save Path is not set.'))\n      return\n    }\n    this.ws = fs.createWriteStream(this.chunkInfo.path, options)\n\n    this.ws.on('finish', () => {\n      if (this.closeWaiting) return\n      void this.__closeWriteStream()\n    })\n    this.ws.on('error', err => {\n      fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {\n        this.__handleError(err)\n        this.chunkInfo.startByte = '0'\n        this.resumeLastChunk = null\n        this.progress.downloaded = 0\n        if (unlinkErr && unlinkErr.code !== 'ENOENT') this.__handleError(unlinkErr)\n      })\n    })\n  }\n\n  __handleComplete() {\n    if (this.status == STATUS.error) return\n    this.__clearTimeout()\n    if (this.progress.progress <= 0) {\n      this.status = STATUS.error\n      this.emit('error', new Error('Progress is 0, download failed.'))\n      return\n    }\n    void this.__closeWriteStream().then(() => {\n      if (this.progress.downloaded == this.progress.total) {\n        this.status = STATUS.completed\n        this.emit('completed')\n      } else {\n        this.status = STATUS.stopped\n        this.emit('stop')\n      }\n    })\n    // console.log('end')\n  }\n\n  __handleError(error: Error) {\n    if (this.status == STATUS.error) return\n    this.status = STATUS.error\n    this.__clearTimeout()\n    this.__closeRequest()\n    void this.__closeWriteStream()\n    if (error.message == 'aborted') return\n    this.emit('error', error)\n  }\n\n  async __closeWriteStream() {\n    return new Promise<void>((resolve, reject) => {\n      if (!this.ws) {\n        resolve()\n        return\n      }\n      // console.log('close write stream')\n      if (this.closeWaiting || this.dataWriteQueueLength) {\n        this.closeWaiting ||= true\n        this.ws.on('close', resolve)\n      } else {\n        this.ws.close(err => {\n          if (err) {\n            this.status = STATUS.error\n            this.emit('error', err)\n            reject(err)\n            return\n          }\n          this.ws = null\n          resolve()\n        })\n      }\n    })\n  }\n\n  __closeRequest() {\n    if (!this.requestInstance || this.requestInstance.destroyed) return\n    // console.log('close request')\n    this.requestInstance.destroy()\n    this.requestInstance = null\n  }\n\n  __handleWriteData(chunk: Buffer) {\n    if (this.resumeLastChunk) {\n      const result = this.__handleDiffChunk(chunk)\n      if (result) chunk = result\n      else {\n        void this.__handleStop().finally(() => {\n          // this.__handleError(new Error('Resume failed, response chunk does not match.'))\n          // Resume failed, response chunk does not match, remove file and restart download\n          console.log('Resume failed, response chunk does not match.')\n          fs.unlink(this.chunkInfo.path, (unlinkErr: any) => {\n            // this.__handleError(err)\n            this.chunkInfo.startByte = '0'\n            this.resumeLastChunk = null\n            if (unlinkErr && unlinkErr.code !== 'ENOENT') {\n              this.__handleError(unlinkErr)\n              return\n            }\n            void this.start()\n          })\n        })\n        return\n      }\n    }\n    // console.log('data', chunk)\n    if (this.status == STATUS.stopped || this.ws == null) {\n      console.log('cancel write')\n      return\n    }\n    this.dataWriteQueueLength++\n    this.__startTimeout()\n    this.__calculateProgress(chunk.length)\n    this.ws.write(chunk, err => {\n      this.dataWriteQueueLength--\n      if (this.status == STATUS.running) this.__calculateProgress(0)\n      if (err) {\n        console.log(err)\n        this.__handleError(err)\n        return\n      }\n      if (this.closeWaiting && !this.dataWriteQueueLength) this.ws?.close()\n    })\n  }\n\n  __handleDiffChunk(chunk: Buffer): Buffer | null {\n    // console.log('diff', chunk)\n    let resumeLastChunkLen = this.resumeLastChunk!.length\n    let chunkLen = chunk.length\n    let isOk\n    if (chunkLen >= resumeLastChunkLen) {\n      isOk = chunk.subarray(0, resumeLastChunkLen).toString('hex') === this.resumeLastChunk!.toString('hex')\n      if (!isOk) return null\n\n      this.resumeLastChunk = null\n      return chunk.subarray(resumeLastChunkLen)\n    } else {\n      isOk = chunk.subarray(0, chunkLen).toString('hex') === this.resumeLastChunk!.subarray(0, chunkLen).toString('hex')\n      if (!isOk) return null\n      this.resumeLastChunk = this.resumeLastChunk!.subarray(chunkLen)\n      return chunk.subarray(chunkLen)\n    }\n  }\n\n  async __handleStop() {\n    this.__clearTimeout()\n    this.__closeRequest()\n    return this.__closeWriteStream()\n  }\n\n  private __clearTimeout() {\n    if (!this.timeout) return\n    clearTimeout(this.timeout)\n    this.timeout = null\n  }\n\n  private __startTimeout() {\n    this.__clearTimeout()\n    this.timeout = setTimeout(() => {\n      this.__handleError(new Error('download timeout'))\n    }, this.options.timeout)\n  }\n\n  __calculateProgress(receivedBytes: number) {\n    const currentTime = performance.now()\n    const elaspsedTime = currentTime - this.statsEstimate.time\n\n    const progress = this.progress\n    progress.downloaded += receivedBytes\n    progress.progress = progress.total ? (progress.downloaded / progress.total) * 100 : -1\n\n\n    // emit the progress every second or if finished\n    if ((progress.downloaded === progress.total && this.dataWriteQueueLength == 0) || elaspsedTime > 1000) {\n      this.statsEstimate.time = currentTime\n      this.statsEstimate.bytes = progress.downloaded - this.statsEstimate.prevBytes\n      this.statsEstimate.prevBytes = progress.downloaded\n      this.emit('progress', {\n        total: progress.total,\n        downloaded: progress.downloaded,\n        progress: progress.progress,\n        speed: this.statsEstimate.bytes,\n        writeQueue: this.dataWriteQueueLength,\n      })\n    }\n  }\n\n  async start() {\n    this.status = STATUS.init\n    await this.__init()\n    if (this.status !== STATUS.init) return\n    this.status = STATUS.running\n    this.__httpFetch(this.downloadUrl, this.requestOptions)\n    this.emit('start')\n  }\n\n  async stop() {\n    if (this.status == STATUS.stopped || this.status == STATUS.completed) return\n    this.status = STATUS.stopped\n    await this.__handleStop()\n    this.emit('stop')\n  }\n\n  refreshUrl(url: string) {\n    this.downloadUrl = url\n  }\n\n  updateSaveInfo(filePath: string, fileName: string) {\n    this.chunkInfo.path = path.join(filePath, fileName)\n  }\n}\n\nexport default Task\n"
  },
  {
    "path": "src/common/utils/download/index.ts",
    "content": "import Downloader, { type Options as DownloaderOptions } from './Downloader'\nimport { getRequestAgent } from './util'\nimport { sizeFormate } from '@common/utils'\nimport type http from 'http'\n\n// these are the default options\n// const options = {\n//   method: 'GET', // Request Method Verb\n//   // Custom HTTP Header ex: Authorization, User-Agent\n//   headers: {},\n//   fileName: '', // Custom filename when saved\n//   override: false, // if true it will override the file, otherwise will append '(number)' to the end of file\n//   forceResume: false, // If the server does not return the \"accept-ranges\" header, can be force if it does support it\n//   // httpRequestOptions: {}, // Override the http request options\n//   // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs\n// }\n\nexport interface Options {\n  url: string\n  path: string\n  fileName: string\n  method?: DownloaderOptions['requestOptions']['method']\n  headers?: DownloaderOptions['requestOptions']['headers']\n  forceResume?: boolean\n  proxy?: { host: string, port: number }\n  onCompleted?: () => void\n  onError?: (error: Error) => void\n  onFail?: (response: http.IncomingMessage) => void\n  onStart?: () => void\n  onStop?: () => void\n  onProgress?: (progress: LX.Download.ProgressInfo) => void\n}\nconst noop = () => {}\nexport const createDownload = ({\n  url,\n  path,\n  fileName,\n  method = 'get',\n  forceResume,\n  proxy,\n  // resumeTime = 5000,\n  onCompleted = noop,\n  onError = noop,\n  onFail = noop,\n  onStart = noop,\n  onStop = noop,\n  onProgress = noop,\n}: Options) => {\n  const dl = new Downloader(url, path, fileName, {\n    requestOptions: {\n      method,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n      },\n      agent: getRequestAgent(url, proxy),\n      timeout: 60 * 1000,\n    },\n\n    forceResume,\n  })\n\n  dl.on('completed', () => {\n    onCompleted()\n  }).on('error', (err: any) => {\n    if (err.message === 'socket hang up') return\n    onError(err)\n  }).on('start', () => {\n    onStart()\n    // pauseResumeTimer(dl, resumeTime)\n  }).on('progress', (stats) => {\n    const speed = sizeFormate(stats.speed)\n    onProgress({\n      progress: parseInt(stats.progress.toFixed(2)),\n      speed,\n      downloaded: stats.downloaded,\n      total: stats.total,\n      writeQueue: stats.writeQueue,\n    })\n    // if (debugDownload) {\n    //   const downloaded = sizeFormate(stats.downloaded)\n    //   const total = sizeFormate(stats.total)\n    //   console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`)\n    // }\n  }).on('stop', () => {\n    onStop()\n    // debugDownload && console.log('paused')\n  }).on('fail', resp => {\n    onFail(resp)\n    // debugDownload && console.log('fail')\n  })\n\n  // debugDownload && console.log('Downloading: ', url)\n\n  dl.start().catch(err => {\n    onError(err)\n  })\n\n  return dl\n}\n\nexport type DownloaderType = Downloader\n"
  },
  {
    "path": "src/common/utils/download/request.ts",
    "content": "import { URL } from 'url'\nimport http from 'http'\nimport https from 'https'\n\nexport interface Options {\n  method: 'get' | 'head' | 'delete' | 'patch' | 'post' | 'put'\n  params?: Record<string, string>\n  // body?: Record<string, string>\n  headers?: Record<string, string>\n  timeout?: number\n  agent?: http.Agent\n}\n\nconst defaultOptions: Options = {\n  method: 'get',\n}\n\ntype HttpCallback = (res: http.IncomingMessage) => void\n\nconst sendRequest = (url: string, options: Options, callback?: HttpCallback) => {\n  const urlParse = new URL(url)\n  const httpOptions: http.RequestOptions | https.RequestOptions = {\n    host: urlParse.hostname,\n    port: urlParse.port,\n    path: urlParse.pathname + urlParse.search,\n    method: options.method,\n  }\n\n  if (options.params) {\n    (httpOptions.path!) += `${urlParse.search ? '&' : '?'}${Object.entries(options.params)\n      .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)\n      .join('&')}`\n  }\n\n  if (options.headers) httpOptions.headers = { ...options.headers }\n\n  if (options.agent) httpOptions.agent = options.agent\n\n  return urlParse.protocol == 'https:'\n    ? https.request(httpOptions, callback)\n    : http.request(httpOptions, callback)\n}\n\nconst applyTimeout = (request: http.ClientRequest, time: number) => {\n  let timeout: NodeJS.Timeout | null = setTimeout(() => {\n    timeout = null\n    if (request.destroyed) return\n    request.destroy(new Error('Request timeout'))\n  }, time)\n  request.on('response', () => {\n    if (!timeout) return\n    clearTimeout(timeout)\n    timeout = null\n  })\n}\n\n// const isRequireRedirect = (response: http.IncomingMessage) => {\n//   return response.statusCode &&\n//     response.statusCode > 300 &&\n//     response.statusCode < 400 &&\n//     Object.hasOwn(response.headers, 'location') &&\n//     response.headers.location\n// }\n\n// export function request(url: string, callback: HttpCallback)\n// export function request(url: string, options: Partial<Options>, callback: HttpCallback)\nexport function request(url: string, _options: Partial<Options>, callback?: HttpCallback) {\n  let options: Options = { ...defaultOptions, ..._options }\n  const request = sendRequest(url, options, callback)\n  if (options.timeout) applyTimeout(request, options.timeout)\n  return request\n}\n\n"
  },
  {
    "path": "src/common/utils/download/util.ts",
    "content": "import { httpOverHttp, httpsOverHttp } from 'tunnel'\n\nexport const STATUS = {\n  idle: 'IDLE',\n  init: 'INIT',\n  running: 'RUNNING',\n  paused: 'PAUSED',\n  stopped: 'STOPPED',\n  completed: 'COMPLETED',\n  error: 'ERROR',\n  failed: 'FAILED',\n} as const\n\nconst httpsRxp = /^https:/\nexport const getRequestAgent = (url: string, proxy?: { host: string, port: number }) => {\n  let options\n  if (proxy) {\n    options = {\n      proxy: {\n        host: proxy.host,\n        port: proxy.port,\n      },\n    }\n  }\n  return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined\n}\n"
  },
  {
    "path": "src/common/utils/effects/cursor-effects/bubbleCursor.js",
    "content": "// https://github.com/tholman/cursor-effects/blob/master/src/bubbleCursor.js\n\nclass Particle {\n  constructor(x, y, fillStyle, strokeStyle) {\n    const lifeSpan = Math.floor(Math.random() * 60 + 60)\n    this.initialLifeSpan = lifeSpan //\n    this.lifeSpan = lifeSpan // ms\n    this.velocity = {\n      x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 10),\n      y: -0.4 + Math.random() * -1,\n    }\n    this.position = { x, y }\n    this.fillStyle = fillStyle\n    this.strokeStyle = strokeStyle\n\n    this.baseDimension = 4\n  }\n\n  update(context, width) {\n    this.position.x += this.velocity.x\n    this.position.y += this.velocity.y\n\n    this.velocity.x += ((Math.random() < 0.5 ? -1 : 1) * 2) / 75\n    this.velocity.y -= Math.random() / 600\n    if (this.position.x >= width - 2 || this.position.x <= 2) this.lifeSpan = 0\n    else if (this.position.y <= 5) this.lifeSpan = 0\n    this.lifeSpan--\n\n    const scale =\n      0.2 + (this.initialLifeSpan - this.lifeSpan) / this.initialLifeSpan\n\n    context.fillStyle = this.fillStyle\n    context.strokeStyle = this.strokeStyle\n    context.beginPath()\n    context.arc(\n      this.position.x - (this.baseDimension / 2) * scale,\n      this.position.y - this.baseDimension / 2,\n      this.baseDimension * scale,\n      0,\n      2 * Math.PI,\n    )\n\n    context.stroke()\n    context.fill()\n\n    context.closePath()\n  }\n}\n\n\nexport default class BubbleCursor {\n  constructor({ element, fillStyle = 'rgba(77, 175, 124, 0.1)', strokeStyle = 'rgba(77, 175, 124, 0.3)' } = {}) {\n    this.hasWrapperEl = element\n    this.element = this.hasWrapperEl || document.body\n\n    this.width = window.innerWidth\n    this.height = window.innerHeight\n    this.cursor = { x: this.width / 2, y: this.width / 2 }\n    this.particles = []\n    this.canvas = null\n    this.context = null\n    this.fillStyle = fillStyle\n    this.strokeStyle = strokeStyle\n\n    this.init()\n  }\n\n  init() {\n    this.canvas = document.createElement('canvas')\n    this.context = this.canvas.getContext('2d')\n\n    this.canvas.style.top = '0px'\n    this.canvas.style.left = '0px'\n    this.canvas.style.pointerEvents = 'none'\n    this.canvas.style.zIndex = 100\n\n    if (this.hasWrapperEl) {\n      this.canvas.style.position = 'absolute'\n      this.element.appendChild(this.canvas)\n      this.canvas.width = this.element.clientWidth\n      this.canvas.height = this.element.clientHeight\n    } else {\n      this.canvas.style.position = 'fixed'\n      document.body.appendChild(this.canvas)\n      this.canvas.width = this.width\n      this.canvas.height = this.height\n    }\n\n    this.bindEvents()\n    this.loop()\n  }\n\n  bindEvents() {\n    this.element.addEventListener('mousemove', this.onMouseMove)\n    this.element.addEventListener('touchmove', this.onTouchMove, { passive: true })\n    this.element.addEventListener('touchstart', this.onTouchMove, { passive: true })\n    window.addEventListener('resize', this.onWindowResize)\n  }\n\n\n  onWindowResize = (e) => {\n    this.width = window.innerWidth\n    this.height = window.innerHeight\n\n    if (this.hasWrapperEl) {\n      this.canvas.width = this.element.clientWidth\n      this.canvas.height = this.element.clientHeight\n    } else {\n      this.canvas.width = this.width\n      this.canvas.height = this.height\n    }\n  }\n\n  onTouchMove = (e) => {\n    if (e.touches.length > 0) {\n      for (let i = 0; i < e.touches.length; i++) {\n        this.addParticle(\n          e.touches[i].clientX,\n          e.touches[i].clientY,\n        )\n      }\n    }\n  }\n\n  onMouseMove = (e) => {\n    if (this.hasWrapperEl) {\n      const boundingRect = this.element.getBoundingClientRect()\n      this.cursor.x = e.clientX - boundingRect.left\n      this.cursor.y = e.clientY - boundingRect.top\n    } else {\n      this.cursor.x = e.clientX\n      this.cursor.y = e.clientY\n    }\n\n    this.addParticle(this.cursor.x, this.cursor.y)\n  }\n\n  addParticle(x, y) {\n    this.particles.push(new Particle(x, y, this.fillStyle, this.strokeStyle))\n  }\n\n  updateParticles() {\n    this.context.clearRect(0, 0, this.width, this.height)\n\n    // Update\n    for (let i = 0; i < this.particles.length; i++) {\n      this.particles[i].update(this.context, this.width)\n    }\n\n    // Remove dead particles\n    for (let i = this.particles.length - 1; i >= 0; i--) {\n      if (this.particles[i].lifeSpan < 0) {\n        this.particles.splice(i, 1)\n      }\n    }\n  }\n\n  loop = () => {\n    this.updateParticles()\n    window.requestAnimationFrame(this.loop)\n  }\n\n  setColor(fillStyle, strokeStyle) {\n    this.fillStyle = fillStyle\n    this.strokeStyle = strokeStyle\n  }\n\n  destroy() {\n    this.element.removeEventListener('mousemove', this.onMouseMove)\n    this.element.removeEventListener('touchmove', this.onTouchMove, { passive: true })\n    this.element.removeEventListener('touchstart', this.onTouchMove, { passive: true })\n    window.removeEventListener('resize', this.onWindowResize)\n\n    if (this.hasWrapperEl) {\n      this.element.removeChild(this.canvas)\n    } else {\n      document.body.removeChild(this.canvas)\n    }\n\n    this.canvas = null\n    this.context = null\n  }\n}\n"
  },
  {
    "path": "src/common/utils/electron.ts",
    "content": "import { shell, clipboard } from 'electron'\n\n\n/**\n * 在资源管理器中打开目录\n * @param {string} dir\n */\nexport const openDirInExplorer = (dir: string) => {\n  shell.showItemInFolder(dir)\n}\n\n\n/**\n * 在浏览器打开URL\n * @param {*} url\n */\nexport const openUrl = async(url: string) => {\n  if (!/^https?:\\/\\//.test(url)) return\n  await shell.openExternal(url)\n}\n\n\n/**\n * 复制文本到剪贴板\n * @param str\n */\nexport const clipboardWriteText = (str: string) => {\n  clipboard.writeText(str)\n}\n\n/**\n * 从剪贴板读取文本\n * @returns\n */\nexport const clipboardReadText = (): string => {\n  return clipboard.readText()\n}\n\n\nexport const encodePath = (path: string) => {\n  // https://github.com/lyswhut/lx-music-desktop/issues/963\n  // https://github.com/lyswhut/lx-music-desktop/issues/1461\n  return path.replaceAll('%', '%25').replaceAll('#', '%23')\n}\n"
  },
  {
    "path": "src/common/utils/index.ts",
    "content": "import log from 'electron-log/node'\n\n\nexport const isLinux = process.platform == 'linux'\nexport const isWin = process.platform == 'win32'\nexport const isMac = process.platform == 'darwin'\nexport const isProd = process.env.NODE_ENV == 'production'\n\nexport const getPlatform = (platform: NodeJS.Platform = process.platform) => {\n  switch (platform) {\n    case 'win32': return 'windows'\n    case 'darwin': return 'mac'\n    default: return 'linux'\n  }\n}\n\n\n// https://stackoverflow.com/a/53387532\nexport function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {\n  // treat non-numerical characters as lower version\n  // replacing them with a negative number based on charcode of each character\n  const fix = (s: string) => `.${s.toLowerCase().charCodeAt(0) - 2147483647}.`\n\n  const currentVerArr: Array<string | number> = ('' + currentVer).replace(/[^0-9.]/g, fix).split('.')\n  const targetVerArr: Array<string | number> = ('' + targetVer).replace(/[^0-9.]/g, fix).split('.')\n  let c = Math.max(currentVerArr.length, targetVerArr.length)\n  for (let i = 0; i < c; i++) {\n    // convert to integer the most efficient way\n    currentVerArr[i] = ~~currentVerArr[i]\n    targetVerArr[i] = ~~targetVerArr[i]\n    if (currentVerArr[i] > targetVerArr[i]) return 1\n    else if (currentVerArr[i] < targetVerArr[i]) return -1\n  }\n  return 0\n}\n\n\nexport {\n  log,\n}\n\nexport * from './common'\n"
  },
  {
    "path": "src/common/utils/lyric-font-player/font-player.js",
    "content": "import { getNow, TimeoutTools } from './utils'\n\n// const fontFormateRxp = /(?=<\\d+,\\d+>).*?/g\nconst fontSplitRxp = /(?=<\\d+,\\d+>).*?/g\nconst timeRxpAll = /<(\\d+),(\\d+)>/g\nconst timeRxp = /<(\\d+),(\\d+)>/\n\n\n// Create animation\nconst createAnimation = (dom, duration, isVertical) => new window.Animation(new window.KeyframeEffect(dom, isVertical\n  ? [\n      { backgroundSize: '100% 0' },\n      { backgroundSize: '100% 100%' },\n    ]\n  : [\n      { backgroundSize: '0 100%' },\n      { backgroundSize: '100% 100%' },\n    ], {\n  duration,\n  easing: 'linear',\n},\n), document.timeline)\n\n\n// https://jsfiddle.net/ceqpnbky/\n// https://jsfiddle.net/ceqpnbky/1/\n\nexport default class FontPlayer {\n  constructor({\n    time = 0,\n    rate = 1,\n    lyric = '',\n    lineContentClassName = 'line-content',\n    lineClassName = 'line',\n    shadowClassName = 'shadow',\n    fontModeClassName = 'font-mode',\n    lineModeClassName = 'line-mode',\n    fontLrcClassName = 'font-lrc',\n    extendedLrcClassName = 'extended',\n    shadowContent = false,\n    extendedLyrics = [],\n    isVertical = false,\n  }) {\n    this.time = time\n    this.lyric = lyric\n\n    this._rate = rate\n\n    this.isVertical = isVertical\n\n    this.lineContentClassName = lineContentClassName\n    this.lineClassName = lineClassName\n\n    this.shadowContent = shadowContent\n    this.shadowClassName = shadowClassName\n\n    this.extendedLyrics = extendedLyrics\n    this.fontModeClassName = fontModeClassName\n    this.fontLrcClassName = fontLrcClassName\n    this.extendedLrcClassName = extendedLrcClassName\n    this.lineModeClassName = lineModeClassName\n\n\n    this.isPlay = false\n    this.curFontNum = 0\n    this.maxFontNum = 0\n    this._performanceTime = 0\n    this._startTime = 0\n\n    this.lineContent = null\n\n    this.timeoutTools = new TimeoutTools(50)\n    this.waitPlayTimeout = new TimeoutTools(50)\n\n    this._init()\n  }\n\n  _init() {\n    if (this.lyric == null) this.lyric = ''\n\n    this.isLineMode = false\n\n    this.lineContent = document.createElement('div')\n    this.lineContent.time = this.time\n    this.lineContent.className = this.lineContentClassName\n\n    this.line = document.createElement('div')\n    this.line.style = 'position:relative;display:inline-block;'\n    this.line.className = this.lineClassName\n    this.lineContent.appendChild(this.line)\n\n    this.lrcContent = document.createElement('div')\n    this.lrcContent.className = this.fontLrcClassName\n    // if (this.shadowContent) {\n    //   this.lrcShadowContent = document.createElement('div')\n    //   this.lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'\n    //   this.lrcShadowContent.className = this.shadowClassName\n    //   this.line.appendChild(this.lrcShadowContent)\n    // }\n    this.line.appendChild(this.lrcContent)\n\n    for (const lrc of this.extendedLyrics) {\n      const extendedLrcContent = document.createElement('div')\n      extendedLrcContent.style = 'position:relative;display:inline-block;'\n      extendedLrcContent.className = this.extendedLrcClassName\n      this.lineContent.appendChild(document.createElement('br'))\n      this.lineContent.appendChild(extendedLrcContent)\n\n\n      // if (this.shadowContent) {\n      //   const extendedLrcShadowContent = document.createElement('div')\n      //   extendedLrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'\n      //   extendedLrcShadowContent.className = this.shadowClassName\n      //   extendedLrcShadowContent.textContent = lrc\n      //   extendedLrcContent.appendChild(extendedLrcShadowContent)\n      // }\n\n      const lineContent = document.createElement('div')\n      lineContent.className = this.fontLrcClassName\n      lineContent.textContent = lrc.replace(timeRxpAll, '')\n      extendedLrcContent.appendChild(lineContent)\n    }\n    this._parseLyric()\n  }\n\n  _parseLyric() {\n    const fonts = this.lyric.split(fontSplitRxp)\n    // console.log(fonts)\n\n    this.maxFontNum = fonts.length - 1\n    this.fonts = []\n    let text\n    // let lineText = ''\n    let lrcShadowContent\n    for (const font of fonts) {\n      if (!timeRxp.test(font)) return this._handleLineParse()\n      text = font.replace(timeRxp, '')\n      const time = parseInt(RegExp.$2)\n\n      const dom = document.createElement('span')\n      dom.textContent = text\n      const animation = createAnimation(dom, time / this._rate, this.isVertical)\n      this.lrcContent.appendChild(dom)\n      // lineText += text\n\n      if (this.shadowContent) {\n        lrcShadowContent ??= document.createElement('div')\n        const shadowDom = document.createElement('span')\n        shadowDom.textContent = text\n        lrcShadowContent.appendChild(shadowDom)\n      }\n      // dom.style = shadowDom.style = this.fontStyle\n      // dom.className = shadowDom.className = this.fontClassName\n\n      this.fonts.push({\n        text,\n        startTime: parseInt(RegExp.$1),\n        time,\n        dom,\n        animation,\n      })\n    }\n\n    if (this.shadowContent && lrcShadowContent) {\n      lrcShadowContent.style = 'position:absolute;top:0;left:0;right:0;z-index:-1;'\n      lrcShadowContent.className = this.shadowClassName\n      this.line.appendChild(lrcShadowContent)\n    }\n\n    this.line.appendChild(this.lrcContent)\n    this.fonts.at(-1)?.animation.addEventListener('finish', () => {\n      this.lineContent.classList.add('played')\n      this.isPlay = false\n    })\n    this.lineContent.classList.add(this.fontModeClassName)\n    // if (this.shadowContent) this.lrcShadowContent.textContent = lineText\n    // console.log(this.fonts)\n  }\n\n  _handleLineParse() {\n    this.isLineMode = true\n    this.lineContent.classList.add(this.lineModeClassName)\n    this.lrcContent.textContent = this.lyric\n\n    // if (this.shadowContent) this.lrcShadowContent.textContent = this.lyric\n    this.fonts.push({\n      text: this.lyric,\n    })\n  }\n\n  _currentTime() {\n    return (getNow() - this._performanceTime) * this._rate + this._startTime\n  }\n\n  _findcurFontNum(curTime, startIndex = 0) {\n    const length = this.fonts.length\n    for (let index = startIndex; index < length; index++) if (curTime < this.fonts[index].startTime) return index == 0 ? 0 : index - 1\n    return length - 1\n  }\n\n  _handlePlayMaxFontNum() {\n    let curFont = this.fonts[this.curFontNum]\n    // console.log(curFont.text)\n    const currentTime = this._currentTime()\n    const driftTime = currentTime - curFont.startTime\n    if (currentTime > curFont.startTime + curFont.time) {\n      this._handlePlayFont(curFont, driftTime / this._rate, true)\n      this.lineContent.classList.add('played')\n      this.isPlay = false\n      this.pause()\n    } else {\n      this._handlePlayFont(curFont, driftTime)\n    }\n  }\n\n  _handlePlayFont(font, currentTime, toFinishe) {\n    switch (font.animation.playState) {\n      case 'finished':\n        break\n      case 'idle':\n        font.dom.style.backgroundSize = '100% 100%'\n        if (!toFinishe) font.animation.play()\n        break\n      default:\n        if (toFinishe) {\n          font.animation.cancel()\n        } else {\n          font.animation.currentTime = currentTime\n          font.animation.play()\n        }\n        break\n    }\n  }\n\n  _handlePlayLine(isPlayed) {\n    this.isPlay = false\n    if (isPlayed) {\n      this.lineContent.classList.add('played')\n    } else {\n      this.lineContent.classList.remove('played')\n    }\n    // this.fonts[0].dom.style.backgroundSize = isPlayed ? '100% 100%' : '100% 0'\n  }\n\n  _handlePauseFont(font) {\n    if (font.animation.playState == 'running') font.animation.pause()\n  }\n\n  _refresh() {\n    this.curFontNum++\n    // console.log('curFontNum time', this.fonts[this.curFontNum].time)\n    if (this.curFontNum >= this.maxFontNum) return this._handlePlayMaxFontNum()\n    let curFont = this.fonts[this.curFontNum]\n    // console.log(curFont, nextFont, this.curFontNum, this.maxFontNum)\n    const currentTime = this._currentTime()\n    // console.log(curFont.text)\n    const driftTime = currentTime - curFont.startTime\n\n    // console.log(currentTime, driftTime)\n\n    if (driftTime >= 0 || this.curFontNum == 0) {\n      let nextFont = this.fonts[this.curFontNum + 1]\n      const delay = (nextFont.startTime - curFont.startTime - driftTime) / this._rate\n      if (delay > 0) {\n        if (this.isPlay) {\n          this.timeoutTools.start(() => {\n            if (!this.isPlay) return\n            this._refresh()\n          }, delay)\n        }\n        this._handlePlayFont(curFont, driftTime)\n        return\n      } else {\n        let newCurLineNum = this._findcurFontNum(currentTime, this.curFontNum + 1)\n        if (newCurLineNum > this.curFontNum) this.curFontNum = newCurLineNum - 1\n        for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)\n        this._refresh()\n        return\n      }\n    } else if (this.curFontNum == 0) {\n      this.curFontNum--\n      if (this.isPlay) {\n        this.waitPlayTimeout.start(() => {\n          if (!this.isPlay) return\n          this._refresh()\n        }, -driftTime)\n      }\n      return\n    }\n\n    this.curFontNum = this._findcurFontNum(currentTime, this.curFontNum) - 1\n    for (let i = 0; i <= this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)\n    // this.curFontNum--\n    this._refresh()\n  }\n\n  play(curTime = 0) {\n    // console.log('play', curTime)\n    if (!this.fonts.length) return\n    this.pause()\n\n    if (this.isLineMode) return this._handlePlayLine(true)\n    this.lineContent.classList.remove('played')\n    this.isPlay = true\n    this._performanceTime = getNow()\n    this._startTime = curTime\n\n    this.curFontNum = this._findcurFontNum(curTime)\n\n    for (let i = this.curFontNum; i > -1; i--) {\n      this._handlePlayFont(this.fonts[i], 0, true)\n    }\n    for (let i = this.curFontNum, len = this.fonts.length; i < len; i++) {\n      let font = this.fonts[i]\n      font.animation.cancel()\n      font.dom.style.backgroundSize = '0 100%'\n    }\n\n    this.curFontNum--\n\n    this._refresh()\n  }\n\n  pause() {\n    if (!this.isPlay) return\n    this.isPlay = false\n    this.timeoutTools.clear()\n    this.waitPlayTimeout.clear()\n    this._handlePauseFont(this.fonts[this.curFontNum])\n    if (this.curFontNum === this.maxLine) return\n    const curFontNum = this._findcurFontNum(this._currentTime())\n    if (this.curFontNum === curFontNum) return\n    for (let i = 0; i < this.curFontNum; i++) this._handlePlayFont(this.fonts[i], 0, true)\n  }\n\n  finish() {\n    this.pause()\n    if (this.isLineMode) return this._handlePlayLine(true)\n    this.lineContent.classList.add('played')\n\n    for (const font of this.fonts) {\n      font.animation.cancel()\n      font.dom.style.backgroundSize = '100% 100%'\n    }\n    this.curFontNum = this.maxFontNum\n  }\n\n  setPlaybackRate(rate) {\n    this._rate = rate\n    if (!this.lines.length) return\n    if (!this.isPlay) return\n    this.play(this._currentTime())\n  }\n\n  reset() {\n    this.pause()\n    if (this.isLineMode) return this._handlePlayLine(false)\n    this.lineContent.classList.remove('played')\n    for (const font of this.fonts) {\n      font.animation.cancel()\n      font.dom.style.backgroundSize = '0 100%'\n    }\n    this.curFontNum = 0\n  }\n}\n\n"
  },
  {
    "path": "src/common/utils/lyric-font-player/index.js",
    "content": "import LinePlayer from './line-player'\nimport FontPlayer from './font-player'\n\nconst fontTimeExp = /<(\\d+),(\\d+)>/g\n\nexport default class Lyric {\n  constructor({\n    lyric = '',\n    extendedLyrics = [],\n    offset = 0,\n    rate = 1,\n    lineContentClassName = 'line-content',\n    lineClassName = 'line',\n    shadowClassName = 'shadow',\n    fontModeClassName = 'font-mode',\n    lineModeClassName = 'line-mode',\n    fontLrcClassName = 'font-lrc',\n    extendedLrcClassName = 'extended',\n    activeLineClassName = 'active',\n    shadowContent = false,\n    isVertical = false,\n    onPlay = function(line, text) { },\n    onSetLyric = function(lines, offset) { },\n    onUpdateLyric = function(lines) { },\n  }) {\n    this.lyric = lyric\n    this.extendedLyrics = extendedLyrics\n    this.offset = offset\n    this.rate = rate\n    this.onPlay = onPlay\n    this.onSetLyric = onSetLyric\n    this.onUpdateLyric = onUpdateLyric\n\n    this.lineContentClassName = lineContentClassName\n    this.lineClassName = lineClassName\n    this.shadowClassName = shadowClassName\n    this.fontModeClassName = fontModeClassName\n    this.lineModeClassName = lineModeClassName\n    this.fontLrcClassName = fontLrcClassName\n    this.extendedLrcClassName = extendedLrcClassName\n    this.activeLineClassName = activeLineClassName\n    this.shadowContent = shadowContent\n\n    this.isVertical = isVertical\n    this.playingLineNum = -1\n    this.isLineMode = false\n\n    this.initInfo = {\n      lines: [],\n      offset: 0,\n    }\n\n    this.linePlayer = new LinePlayer({\n      offset: this.offset,\n      rate: this.rate,\n      onPlay: this._handleLinePlayerOnPlay,\n      onSetLyric: this._handleLinePlayerOnSetLyric,\n    })\n  }\n\n  _init() {\n    this.playingLineNum = -1\n    this.isLineMode = false\n\n    this.linePlayer.setLyric(this.lyric, this.extendedLyrics)\n  }\n\n  _handleLinePlayerOnPlay = (num, text, curTime) => {\n    if (this.isLineMode) {\n      if (num < this.playingLineNum + 1) {\n        for (let i = this.playingLineNum, minNum = Math.max(num, 0) - 1; i > minNum; i--) {\n          const font = this._lineFonts[i]\n          font.reset()\n          font.lineContent.classList.remove(this.activeLineClassName)\n        }\n      } else if (num > this.playingLineNum) {\n        for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {\n          const font = this._lineFonts[i]\n          font.reset()\n          font.lineContent.classList.remove(this.activeLineClassName)\n        }\n      } else if (this.playingLineNum > -1) {\n        const font = this._lineFonts[this.playingLineNum]\n        font.reset()\n        font.lineContent.classList.remove(this.activeLineClassName)\n      }\n    } else {\n      if (num < this.playingLineNum + 1) {\n        for (let i = this.playingLineNum, minNum = Math.max(num, 0) - 1; i > minNum; i--) {\n          const font = this._lineFonts[i]\n          font.lineContent.classList.remove(this.activeLineClassName)\n          font.reset()\n        }\n      } else if (num > this.playingLineNum) {\n        for (let i = Math.max(this.playingLineNum, 0); i < num; i++) {\n          const font = this._lineFonts[i]\n          font.lineContent.classList.remove(this.activeLineClassName)\n          font.finish()\n        }\n      } else if (this.playingLineNum > -1) {\n        const font = this._lineFonts[this.playingLineNum]\n        font.lineContent.classList.remove(this.activeLineClassName)\n      }\n    }\n    this.playingLineNum = num\n    if (num > -1) {\n      const font = this._lineFonts[num]\n      font.lineContent.classList.add(this.activeLineClassName)\n      font.play(curTime - this._lines[num].time)\n    }\n    this.onPlay(num, this._lines[num]?.text ?? '')\n  }\n\n  _initLines = (lyricLines, offset, isUpdate) => {\n    // console.log(lyricLines)\n    // this._lines = lyricsLines\n    this.isLineMode = lyricLines.length && !/^<\\d+,\\d+>/.test(lyricLines[0].text)\n\n    this._lineFonts = []\n    if (this.isLineMode) {\n      this._lines = lyricLines.map(line => {\n        const fontPlayer = new FontPlayer({\n          time: line.time,\n          rate: this.rate,\n          lyric: line.text,\n          extendedLyrics: line.extendedLyrics,\n          lineContentClassName: this.lineContentClassName,\n          lineClassName: this.lineClassName,\n          shadowClassName: this.shadowClassName,\n          fontModeClassName: this.fontModeClassName,\n          lineModeClassName: this.lineModeClassName,\n          fontLrcClassName: this.fontLrcClassName,\n          extendedLrcClassName: this.extendedLrcClassName,\n          shadowContent: this.shadowContent,\n          isVertical: this.isVertical,\n        })\n\n        this._lineFonts.push(fontPlayer)\n        return {\n          text: line.text,\n          time: line.time,\n          extendedLyrics: line.extendedLyrics,\n          dom_line: fontPlayer.lineContent,\n        }\n      })\n    } else {\n      this._lines = lyricLines.map(line => {\n        const fontPlayer = new FontPlayer({\n          time: line.time,\n          rate: this.rate,\n          lyric: line.text,\n          extendedLyrics: line.extendedLyrics,\n          lineContentClassName: this.lineContentClassName,\n          lineClassName: this.lineClassName,\n          shadowClassName: this.shadowClassName,\n          fontModeClassName: this.fontModeClassName,\n          lineModeClassName: this.lineModeClassName,\n          fontLrcClassName: this.fontLrcClassName,\n          extendedLrcClassName: this.extendedLrcClassName,\n          shadowContent: this.shadowContent,\n          isVertical: this.isVertical,\n        })\n\n        this._lineFonts.push(fontPlayer)\n        return {\n          text: line.text.replace(fontTimeExp, ''),\n          time: line.time,\n          extendedLyrics: line.extendedLyrics,\n          dom_line: fontPlayer.lineContent,\n        }\n      })\n    }\n\n    // 如果是逐行歌词，则添加 60ms 的偏移\n    let newOffset = this.isLineMode ? this.offset + 60 : this.offset\n    offset = offset - this.linePlayer.offset + newOffset\n    this.linePlayer.offset = newOffset\n    if (isUpdate) this.onUpdateLyric(this._lines)\n    else this.onSetLyric(this._lines, offset)\n  }\n\n  _handleLinePlayerOnSetLyric = (lyricLines, offset) => {\n    this._initLines(lyricLines, offset, false)\n    this.playingLineNum = 0\n    this.initInfo.lines = lyricLines\n    this.initInfo.offset = offset\n  }\n\n  play(curTime) {\n    if (!this.linePlayer) return\n    this.linePlayer.play(curTime)\n  }\n\n  pause() {\n    if (!this.linePlayer) return\n    this.linePlayer.pause()\n    if (this.playingLineNum > -1) this._lineFonts[this.playingLineNum]?.pause()\n  }\n\n  setOffset(offset) {\n    this.linePlayer.offset = offset\n  }\n\n  setLyric(lyric, extendedLyrics) {\n    this.lyric = lyric\n    this.extendedLyrics = extendedLyrics\n    this._init()\n  }\n\n  setPlaybackRate(rate) {\n    this.rate = rate\n    this.linePlayer.setPlaybackRate(rate)\n    this._initLines(this.initInfo.lines, this.initInfo.offset, true)\n    if (this.linePlayer.isPlay) {\n      const num = this.playingLineNum\n      this.playingLineNum = 0\n      this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())\n    } else this.playingLineNum = 0\n  }\n\n  setVertical(isVertical) {\n    this.isVertical = isVertical\n    this._initLines(this.initInfo.lines, this.initInfo.offset, true)\n    if (this.linePlayer.isPlay) {\n      const num = this.playingLineNum\n      this.playingLineNum = 0\n      this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())\n    } else this.playingLineNum = 0\n  }\n\n  setDisabledAutoPause(autoPause) {\n    this.linePlayer.setDisabledAutoPause(autoPause)\n  }\n}\n"
  },
  {
    "path": "src/common/utils/lyric-font-player/line-player.js",
    "content": "import { getNow, TimeoutTools } from './utils'\n\nconst timeFieldExp = /^(?:\\[[\\d:.]+\\])+/g\nconst timeExp = /\\d{1,3}(:\\d{1,3}){0,2}(?:\\.\\d{1,3})/g\nconst tagRegMap = {\n  title: 'ti',\n  artist: 'ar',\n  album: 'al',\n  offset: 'offset',\n  by: 'by',\n}\n\nconst timeoutTools = new TimeoutTools()\n\nconst t_rxp_1 = /^0+(\\d+)/\nconst t_rxp_2 = /:0+(\\d+)/g\nconst t_rxp_3 = /\\.0+(\\d+)/\nconst formatTimeLabel = (label) => {\n  return label.replace(t_rxp_1, '$1')\n    .replace(t_rxp_2, ':$1')\n    .replace(t_rxp_3, '.$1')\n}\n\nconst parseExtendedLyric = (lrcLinesMap, extendedLyric) => {\n  const extendedLines = extendedLyric.split(/\\r\\n|\\n|\\r/)\n  for (let i = 0; i < extendedLines.length; i++) {\n    const line = extendedLines[i].trim()\n    let result = timeFieldExp.exec(line)\n    if (result) {\n      const timeField = result[0]\n      const text = line.replace(timeFieldExp, '').trim()\n      // https://github.com/lyswhut/lx-music-desktop/issues/1499\n      if (text && text != '//') {\n        const times = timeField.match(timeExp)\n        if (times == null) continue\n        for (let time of times) {\n          const timeStr = formatTimeLabel(time)\n          const targetLine = lrcLinesMap[timeStr]\n          if (targetLine) targetLine.extendedLyrics.push(text)\n        }\n      }\n    }\n  }\n}\n\nexport default class LinePlayer {\n  constructor({ offset = 0, rate = 1, onPlay = function() { }, onSetLyric = function() { } } = {}) {\n    this.tags = {}\n    this.lines = null\n    this.onPlay = onPlay\n    this.onSetLyric = onSetLyric\n    this.isPlay = false\n    this.curLineNum = 0\n    this.maxLine = 0\n    this.offset = offset\n    this._performanceTime = 0\n    this._startTime = 0\n    this._rate = rate\n  }\n\n  _init() {\n    if (this.lyric == null) this.lyric = ''\n    if (this.extendedLyrics == null) this.extendedLyrics = []\n    this._initTag()\n    this._initLines()\n    this.onSetLyric(this.lines, this.tags.offset + this.offset)\n  }\n\n  _initTag() {\n    this.tags = {}\n    for (let tag in tagRegMap) {\n      const matches = this.lyric.match(new RegExp(`\\\\[${tagRegMap[tag]}:([^\\\\]]*)]`, 'i'))\n      this.tags[tag] = (matches && matches[1]) || ''\n    }\n    if (this.tags.offset) {\n      let offset = parseInt(this.tags.offset)\n      this.tags.offset = Number.isNaN(offset) ? 0 : offset\n    } else {\n      this.tags.offset = 0\n    }\n  }\n\n  _initLines() {\n    this.lines = []\n    const lines = this.lyric.split(/\\r\\n|\\r|\\n/)\n    const linesMap = {}\n    for (let i = 0; i < lines.length; i++) {\n      const line = lines[i].trim()\n      let result = timeFieldExp.exec(line)\n      if (result) {\n        const timeField = result[0]\n        const text = line.replace(timeFieldExp, '').trim()\n        if (text) {\n          const times = timeField.match(timeExp)\n          if (times == null) continue\n          for (let time of times) {\n            const timeStr = formatTimeLabel(time)\n            if (linesMap[timeStr]) {\n              linesMap[timeStr].extendedLyrics.push(text)\n              continue\n            }\n            const timeArr = timeStr.split(':')\n            if (timeArr.length > 3) continue\n            else if (timeArr.length < 3) for (let i = 3 - timeArr.length; i--;) timeArr.unshift('0')\n            if (timeArr[2].indexOf('.') > -1) timeArr.splice(2, 1, ...timeArr[2].split('.'))\n\n            linesMap[timeStr] = {\n              time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),\n              text,\n              extendedLyrics: [],\n            }\n          }\n        }\n      }\n    }\n\n    for (const lrc of this.extendedLyrics) parseExtendedLyric(linesMap, lrc)\n    this.lines = Object.values(linesMap)\n    this.lines.sort((a, b) => {\n      return a.time - b.time\n    })\n    this.maxLine = this.lines.length - 1\n  }\n\n  _currentTime() {\n    return (getNow() - this._performanceTime) * this._rate + this._startTime\n  }\n\n  _findCurLineNum(curTime, startIndex = 0) {\n    if (curTime <= 0) return 0\n    const length = this.lines.length\n    for (let index = startIndex; index < length; index++) if (curTime <= this.lines[index].time) return index === 0 ? 0 : index - 1\n    return length - 1\n  }\n\n  _handleMaxLine() {\n    this.onPlay(this.curLineNum, this.lines[this.curLineNum].text, this._currentTime())\n    this.pause()\n  }\n\n  _refresh() {\n    this.curLineNum++\n    // console.log('curLineNum time', this.lines[this.curLineNum].time)\n    if (this.curLineNum >= this.maxLine) return this._handleMaxLine()\n\n    let curLine = this.lines[this.curLineNum]\n\n    const currentTime = this._currentTime()\n    const driftTime = currentTime - curLine.time\n\n    if (driftTime >= 0) {\n      let nextLine = this.lines[this.curLineNum + 1]\n      const delay = (nextLine.time - curLine.time - driftTime) / this._rate\n\n      if (delay > 0) {\n        if (this.isPlay) {\n          timeoutTools.start(() => {\n            if (!this.isPlay) return\n            this._refresh()\n          }, delay)\n        }\n        this.onPlay(this.curLineNum, curLine.text, currentTime)\n        return\n      } else {\n        let newCurLineNum = this._findCurLineNum(currentTime, this.curLineNum + 1)\n        if (newCurLineNum > this.curLineNum) this.curLineNum = newCurLineNum - 1\n        this._refresh()\n        return\n      }\n    } else if (this.curLineNum == 0) {\n      let firstLine = this.lines[0]\n      const delay = (firstLine.time - currentTime) / this._rate\n      if (this.isPlay) {\n        timeoutTools.start(() => {\n          if (!this.isPlay) return\n          this._refresh()\n        }, delay)\n      }\n      this.onPlay(-1, '', currentTime)\n      return\n    }\n\n    this.curLineNum = this._findCurLineNum(currentTime, this.curLineNum) - 1\n    this._refresh()\n  }\n\n  play(curTime = 0) {\n    if (!this.lines.length) return\n    this.pause()\n    this.isPlay = true\n\n    this._performanceTime = getNow() - parseInt(this.tags.offset + this.offset)\n    this._startTime = curTime\n\n    this.curLineNum = this._findCurLineNum(this._currentTime()) - 1\n\n    this._refresh()\n  }\n\n  pause() {\n    if (!this.isPlay) return\n    this.isPlay = false\n    timeoutTools.clear()\n    if (this.curLineNum === this.maxLine) return\n    const currentTime = this._currentTime()\n    const curLineNum = this._findCurLineNum(currentTime)\n    if (this.curLineNum !== curLineNum) {\n      this.curLineNum = curLineNum\n      this.onPlay(curLineNum, this.lines[curLineNum].text, currentTime)\n    }\n  }\n\n  setPlaybackRate(rate) {\n    this._rate = rate\n    if (!this.lines.length) return\n    if (!this.isPlay) return\n    this.play(this._currentTime())\n  }\n\n  setLyric(lyric, extendedLyrics) {\n    // console.log(extendedLyrics)\n    if (this.isPlay) this.pause()\n    this.lyric = lyric\n    this.extendedLyrics = extendedLyrics\n    this._init()\n  }\n\n  setDisabledAutoPause(disabledAutoPause) {\n    if (disabledAutoPause) {\n      timeoutTools.nextTick = (handler) => {\n        return setTimeout(handler, 20)\n      }\n      timeoutTools.cancelNextTick = clearTimeout.bind(global)\n    } else {\n      timeoutTools.nextTick = window.requestAnimationFrame.bind(window)\n      timeoutTools.cancelNextTick = window.cancelAnimationFrame.bind(window)\n    }\n  }\n}\n"
  },
  {
    "path": "src/common/utils/lyric-font-player/utils.js",
    "content": "\nexport const getNow = typeof performance == 'object' && window.performance.now ? window.performance.now.bind(window.performance) : Date.now.bind(Date)\n\nexport class TimeoutTools {\n  constructor(thresholdTime = 80) {\n    this.nextTick = window.requestAnimationFrame.bind(window)\n    this.cancelNextTick = window.cancelAnimationFrame.bind(window)\n    this.invokeTime = 0\n    this.animationFrameId = null\n    this.timeoutId = null\n    this.callback = null\n    this.thresholdTime = thresholdTime\n  }\n\n  run() {\n    this.animationFrameId = this.nextTick(() => {\n      this.animationFrameId = null\n      let diff = this.invokeTime - getNow()\n      // console.log('diff', diff)\n      if (diff > 0) {\n        if (diff < this.thresholdTime) return this.run()\n        // console.log('run timeout', diff, diff - this.thresholdTime)\n        return this.timeoutId = window.setTimeout(() => {\n          this.timeoutId = null\n          this.run()\n        }, diff - this.thresholdTime)\n      }\n\n      // console.log('diff', diff)\n      this.callback(diff)\n    })\n  }\n\n  start(callback = () => {}, timeout = 0) {\n    // console.log(timeout)\n    this.callback = callback\n    this.invokeTime = getNow() + timeout\n\n    this.run()\n  }\n\n  clear() {\n    if (this.animationFrameId) {\n      this.cancelNextTick(this.animationFrameId)\n      this.animationFrameId = null\n    }\n    if (this.timeoutId) {\n      window.clearTimeout(this.timeoutId)\n      this.timeoutId = null\n    }\n  }\n}\n\n"
  },
  {
    "path": "src/common/utils/lyricUtils/kg.js",
    "content": "import { inflate } from 'zlib'\nimport { decodeName } from './util'\n\n// https://github.com/lyswhut/lx-music-desktop/issues/296#issuecomment-683285784\nconst enc_key = Buffer.from([0x40, 0x47, 0x61, 0x77, 0x5e, 0x32, 0x74, 0x47, 0x51, 0x36, 0x31, 0x2d, 0xce, 0xd2, 0x6e, 0x69], 'binary')\nconst decodeLyric = str => new Promise((resolve, reject) => {\n  if (!str.length) return\n  const buf_str = Buffer.from(str, 'base64').subarray(4)\n  for (let i = 0, len = buf_str.length; i < len; i++) {\n    buf_str[i] = buf_str[i] ^ enc_key[i % 16]\n  }\n  inflate(buf_str, (err, result) => {\n    if (err) return reject(err)\n    resolve(result.toString())\n  })\n})\n\nconst headExp = /^.*\\[id:\\$\\w+\\]\\n/\n\nconst parseLyric = str => {\n  str = str.replace(/\\r/g, '')\n  if (headExp.test(str)) str = str.replace(headExp, '')\n  let trans = str.match(/\\[language:([\\w=\\\\/+]+)\\]/)\n  let lyric\n  let rlyric\n  let tlyric\n  if (trans) {\n    str = str.replace(/\\[language:[\\w=\\\\/+]+\\]\\n/, '')\n    let json = JSON.parse(Buffer.from(trans[1], 'base64').toString())\n    for (const item of json.content) {\n      switch (item.type) {\n        case 0:\n          rlyric = item.lyricContent\n          break\n        case 1:\n          tlyric = item.lyricContent\n          break\n      }\n    }\n  }\n  let i = 0\n  let lxlyric = str.replace(/\\[((\\d+),\\d+)\\].*/g, str => {\n    let result = str.match(/\\[((\\d+),\\d+)\\].*/)\n    let time = parseInt(result[2])\n    let ms = time % 1000\n    time /= 1000\n    let m = parseInt(time / 60).toString().padStart(2, '0')\n    time %= 60\n    let s = parseInt(time).toString().padStart(2, '0')\n    time = `${m}:${s}.${ms}`\n    if (rlyric) rlyric[i] = `[${time}]${rlyric[i]?.join('') ?? ''}`\n    if (tlyric) tlyric[i] = `[${time}]${tlyric[i]?.join('') ?? ''}`\n    i++\n    return str.replace(result[1], time)\n  })\n  rlyric = rlyric ? rlyric.join('\\n') : ''\n  tlyric = tlyric ? tlyric.join('\\n') : ''\n  lxlyric = lxlyric.replace(/<(\\d+,\\d+),\\d+>/g, '<$1>')\n  lxlyric = decodeName(lxlyric)\n  lyric = lxlyric.replace(/<\\d+,\\d+>/g, '')\n  rlyric = decodeName(rlyric)\n  tlyric = decodeName(tlyric)\n  return {\n    lyric,\n    tlyric,\n    rlyric,\n    lxlyric,\n  }\n}\n\n\nexport const decodeKrc = async(data) => {\n  return decodeLyric(data).then(parseLyric)\n}\n"
  },
  {
    "path": "src/common/utils/lyricUtils/util.ts",
    "content": "const encodeNames = {\n  '&nbsp;': ' ',\n  '&amp;': '&',\n  '&lt;': '<',\n  '&gt;': '>',\n  '&quot;': '\"',\n  '&apos;': \"'\",\n  '&#039;': \"'\",\n} as const\n\nexport const decodeName = (str: string | null = '') => {\n  return str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;|&nbsp;)/gm, (s: string) => encodeNames[s as keyof typeof encodeNames]) ?? ''\n}\n"
  },
  {
    "path": "src/common/utils/migrateSetting.ts",
    "content": "import { compareVer } from './index'\n\nconst oldThemeMap = {\n  0: 'green',\n  1: 'blue',\n  2: 'yellow',\n  3: 'orange',\n  4: 'red',\n  10: 'pink',\n  5: 'purple',\n  6: 'grey',\n  11: 'ming',\n  12: 'blue2',\n  13: 'black',\n  7: 'mid_autumn',\n  8: 'naruto',\n  9: 'happy_new_year',\n} as const\n\nexport default (setting: any): Partial<LX.AppSetting> => {\n  setting = { ...setting }\n\n  // 迁移 v2.0.0 之前的配置\n  if (compareVer(setting.version, '2.0.0') < 0) {\n    // 迁移列表滚动位置设置 ~0.18.3\n    if (setting.list?.scroll) {\n      let scroll = setting.list.scroll\n      setting.list.isSaveScrollLocation &&= scroll.enable\n      delete setting.list.scroll\n    }\n\n    // 修正拼写问题 v1.8.2 及以前\n    if (setting.player?.isShowLyricTransition != null) {\n      setting.player.isShowLyricTranslation = setting.player.isShowLyricTransition\n      delete setting.player.isShowLyricTransition\n    }\n\n    // 迁移v1.19.0之前的主题设置\n    if (setting.themeId != null) {\n      setting.theme = {\n        id: setting.themeId,\n      }\n      delete setting.themeId\n    }\n\n    if (setting.tray?.isShow != null) setting.tray.enable = setting.tray?.isShow\n\n    setting['common.windowSizeId'] = setting.windowSizeId\n    setting['common.startInFullscreen'] = setting.startInFullscreen\n    setting['common.langId'] = setting.langId\n    setting['common.apiSource'] = setting.apiSource\n    setting['common.sourceNameType'] = setting.sourceNameType\n    setting['common.font'] = setting.font\n    setting['common.isShowAnimation'] = setting.isShowAnimation\n    setting['common.randomAnimate'] = setting.randomAnimate\n    setting['common.isAgreePact'] = setting.isAgreePact\n    setting['common.controlBtnPosition'] = setting.controlBtnPosition\n\n    setting['player.togglePlayMethod'] = setting.player?.togglePlayMethod\n    setting['player.isShowTaskProgess'] = setting.player?.isShowTaskProgess\n    setting['player.volume'] = setting.player?.volume\n    setting['player.isMute'] = setting.player?.isMute\n    setting['player.mediaDeviceId'] = setting.player?.mediaDeviceId\n    setting['player.isMediaDeviceRemovedStopPlay'] = setting.player?.isMediaDeviceRemovedStopPlay\n    setting['player.isShowLyricTranslation'] = setting.player?.isShowLyricTranslation\n    setting['player.isShowLyricRoma'] = setting.player?.isShowLyricRoma\n    setting['player.isS2t'] = setting.player?.isS2t\n    setting['player.isPlayLxlrc'] = setting.player?.isPlayLxlrc\n    setting['player.isSavePlayTime'] = setting.player?.isSavePlayTime\n    setting['player.audioVisualization'] = setting.player?.audioVisualization\n    setting['player.waitPlayEndStop'] = setting.player?.waitPlayEndStop\n    setting['player.waitPlayEndStopTime'] = setting.player?.waitPlayEndStopTime\n    setting['player.autoSkipOnError'] = setting.player?.autoSkipOnError\n\n    setting['playDetail.isZoomActiveLrc'] = setting.playDetail?.isZoomActiveLrc\n    setting['playDetail.isShowLyricProgressSetting'] = setting.playDetail?.isShowLyricProgressSetting\n    setting['playDetail.style.fontSize'] = setting.playDetail?.style?.fontSize\n    setting['playDetail.style.align'] = setting.playDetail?.style?.align\n\n    setting['desktopLyric.enable'] = setting.desktopLyric?.enable\n    setting['desktopLyric.isLock'] = setting.desktopLyric?.isLock\n    setting['desktopLyric.isAlwaysOnTop'] = setting.desktopLyric?.isAlwaysOnTop\n    setting['desktopLyric.isAlwaysOnTopLoop'] = setting.desktopLyric?.isAlwaysOnTopLoop\n    setting['desktopLyric.width'] = setting.desktopLyric?.width\n    setting['desktopLyric.height'] = setting.desktopLyric?.height\n    setting['desktopLyric.x'] = setting.desktopLyric?.x\n    setting['desktopLyric.y'] = setting.desktopLyric?.y\n    setting['desktopLyric.isLockScreen'] = setting.desktopLyric?.isLockScreen\n    setting['desktopLyric.isDelayScroll'] = setting.desktopLyric?.isDelayScroll\n    setting['desktopLyric.isHoverHide'] = setting.desktopLyric?.isHoverHide\n    setting['desktopLyric.style.font'] = setting.desktopLyric?.style?.font\n    if (setting.desktopLyric?.style?.fontSize) setting['desktopLyric.style.fontSize'] = setting.desktopLyric.style.fontSize / 100 * 16\n    setting['desktopLyric.style.opacity'] = setting.desktopLyric?.style?.opacity\n    setting['desktopLyric.style.isZoomActiveLrc'] = setting.desktopLyric?.style?.isZoomActiveLrc\n\n    setting['list.isClickPlayList'] = setting.list?.isClickPlayList\n    setting['list.isShowAlbumName'] = setting.list?.isShowAlbumName\n    setting['list.isShowSource'] = setting.list?.isShowSource\n    setting['list.isSaveScrollLocation'] = setting.list?.isSaveScrollLocation\n    setting['list.addMusicLocationType'] = setting.list?.addMusicLocationType\n\n    setting['download.enable'] = setting.download?.enable\n    setting['download.savePath'] = setting.download?.savePath\n    setting['download.fileName'] = setting.download?.fileName\n    setting['download.maxDownloadNum'] = setting.download?.maxDownloadNum\n    setting['download.isDownloadLrc'] = setting.download?.isDownloadLrc\n    setting['download.lrcFormat'] = setting.download?.lrcFormat\n    setting['download.isEmbedPic'] = setting.download?.isEmbedPic\n    setting['download.isEmbedLyric'] = setting.download?.isEmbedLyric\n    setting['download.isUseOtherSource'] = setting.download?.isUseOtherSource\n\n    setting['search.isShowHotSearch'] = setting.search?.isShowHotSearch\n    setting['search.isShowHistorySearch'] = setting.search?.isShowHistorySearch\n    setting['search.isFocusSearchBox'] = setting.search?.isFocusSearchBox\n\n    setting['network.proxy.enable'] = setting.network?.proxy?.enable\n    setting['network.proxy.host'] = setting.network?.proxy?.host\n    setting['network.proxy.port'] = setting.network?.proxy?.port\n\n    setting['tray.enable'] = setting.tray?.enable\n    setting['tray.themeId'] = setting.tray?.themeId\n\n\n    setting['sync.enable'] = setting.sync?.enable\n    setting['sync.port'] = setting.sync?.port\n\n    setting['theme.id'] = oldThemeMap[setting.theme?.id as keyof typeof oldThemeMap]\n    setting['theme.lightId'] = oldThemeMap[setting.theme?.lightId as keyof typeof oldThemeMap]\n    setting['theme.darkId'] = oldThemeMap[setting.theme?.darkId as keyof typeof oldThemeMap]\n\n    setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput\n    setting['odc.isAutoClearSearchList'] = setting.odc?.isAutoClearSearchList\n\n    setting.version = '2.0.0'\n  }\n\n  // 迁移 v2.2.0 之前的设置数据\n  if (compareVer(setting.version, '2.1.0') < 0) {\n    setting['sync.erver.port'] = setting['sync.port']\n    setting.version = '2.1.0'\n  }\n\n\n  return setting\n}\n"
  },
  {
    "path": "src/common/utils/musicMeta/downloader.js",
    "content": "const http = require('http')\nconst https = require('https')\nconst fs = require('fs')\nconst { httpOverHttp, httpsOverHttp } = require('tunnel')\n\nconst httpsRxp = /^https:/\nconst getRequestAgent = (url, proxy) => {\n  return proxy ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({ proxy }) : undefined\n}\n\nconst sendRequest = (url, proxy) => {\n  const urlParse = new URL(url)\n  const httpOptions = {\n    method: 'get',\n    host: urlParse.hostname,\n    port: urlParse.port,\n    path: urlParse.pathname + urlParse.search,\n    agent: getRequestAgent(url, proxy),\n    headers: {\n      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n    },\n  }\n\n  // console.log(httpOptions)\n  return url.protocol === 'https:'\n    ? https.request(httpOptions)\n    : http.request(httpOptions)\n}\n\nmodule.exports = (url, filePath, proxy) => {\n  return new Promise((resolve) => {\n    sendRequest(url, proxy)\n      .on('response', response => {\n        // console.log(response.statusCode)\n        if (response.statusCode !== 200 && response.statusCode != 206) {\n          response.destroy(new Error('failed'))\n          return\n        }\n        response\n          .pipe(fs.createWriteStream(filePath))\n          .on('finish', () => {\n            // console.log('finish')\n            if (response.complete) {\n              // console.log('complete')\n              // meta.APIC = picPath\n              // handleWriteMeta(meta, filePath)\n              resolve(true)\n            } else {\n              resolve(false)\n              fs.unlink(filePath, err => {\n                if (err) console.log(err.message)\n              })\n            }\n          }).on('error', err => {\n            // console.log('response error')\n            if (err) console.log(err.message)\n            fs.unlink(filePath, err => {\n              if (err) console.log(err.message)\n            })\n            resolve(false)\n          })\n      })\n      .on('error', err => {\n        if (err) console.log(err.message)\n        // delete meta.APIC\n        // handleWriteMeta(meta, filePath)\n        resolve(false)\n      })\n      .end()\n  })\n}\n\n// const url = 'https://y.gtimg.cn/music/photo_new/T002R500x500M000000nfgwP0D6qxd.jpg'\n// // const url = 'http://p4.music.126.net/-U2K8GKlASCSXK0cRre1gA==/109951163188718762.jpg'\n// const picPath = require('path').join(__dirname, 'test.jpg')\n// module.exports(url, picPath).then((sucee) => {\n//   console.log(sucee)\n// })\n"
  },
  {
    "path": "src/common/utils/musicMeta/flac-metadata/index.js",
    "content": "// Fork from https://github.com/claus/flac-metadata\n// 在 flac-metadata 的基础上修复与改进标签与封面写入逻辑\n\n\nconst Transform = require('stream').Transform\n\nconst MetaDataBlock = require('./lib/MetaDataBlock')\nconst MetaDataBlockStreamInfo = require('./lib/MetaDataBlockStreamInfo')\nconst MetaDataBlockVorbisComment = require('./lib/MetaDataBlockVorbisComment')\nconst MetaDataBlockPicture = require('./lib/MetaDataBlockPicture')\n\nconst STATE_IDLE = 0\nconst STATE_MARKER = 1\nconst STATE_MDB_HEADER = 2\nconst STATE_MDB = 3\nconst STATE_PASS_THROUGH = 4\n\nclass Processor extends Transform {\n  constructor(options) {\n    super(options)\n    // MDB types\n    this.MDB_TYPE_STREAMINFO = 0\n    this.MDB_TYPE_PADDING = 1\n    this.MDB_TYPE_APPLICATION = 2\n    this.MDB_TYPE_SEEKTABLE = 3\n    this.MDB_TYPE_VORBIS_COMMENT = 4\n    this.MDB_TYPE_CUESHEET = 5\n    this.MDB_TYPE_PICTURE = 6\n    this.MDB_TYPE_INVALID = 127\n\n    this.state = STATE_IDLE\n\n    this.isFlac = false\n\n    this.buf = null\n    this.bufPos = 0\n\n    this.mdb = null\n    this.mdbLen = 0\n    this.mdbLast = false\n    this.mdbPush = false\n    this.mdbLastWritten = false\n\n    this.parseMetaDataBlocks = false\n\n    this.waitWriteVorbis = null\n    this.waitWritePicture = null\n    this.tasks = 0\n\n    if (!(this instanceof Processor)) return new Processor(options)\n    if (options && !!options.parseMetaDataBlocks) { this.parseMetaDataBlocks = true }\n  }\n\n  writeMeta({ vorbis, picture }) {\n    if (vorbis != null) {\n      this.waitWriteVorbis = vorbis\n      this.tasks++\n    }\n    if (picture != null) {\n      this.waitWritePicture = picture\n      this.tasks++\n    }\n  }\n\n  // clearMeta() {\n  //   this.mdbLastWritten = true\n  // }\n\n  readMeta(callback) {\n    this.parseMetaDataBlocks = true\n    this.readCallBack = callback\n  }\n\n  _transform(chunk, enc, done) {\n    let chunkPos = 0\n    let chunkLen = chunk.length\n    let isChunkProcessed = false\n    let _this = this\n\n    function _safePush(minCapacity, persist, validate) {\n      let slice\n      let chunkAvailable = chunkLen - chunkPos\n      let isDone = (chunkAvailable + this.bufPos >= minCapacity)\n      validate = (typeof validate === 'function') ? validate : function() { return true }\n      if (isDone) {\n        // Enough data available\n        if (persist) {\n          // Persist the entire block so it can be parsed\n          if (this.bufPos > 0) {\n            // Part of this block's data is in backup buffer, copy rest over\n            chunk.copy(this.buf, this.bufPos, chunkPos, chunkPos + minCapacity - this.bufPos)\n            slice = this.buf.slice(0, minCapacity)\n          } else {\n            // Entire block fits in current chunk\n            slice = chunk.slice(chunkPos, chunkPos + minCapacity)\n          }\n        } else {\n          slice = chunk.slice(chunkPos, chunkPos + minCapacity - this.bufPos)\n        }\n        // Push block after validation\n        validate(slice, isDone) && _this.push(slice)\n        chunkPos += minCapacity - this.bufPos\n        this.bufPos = 0\n        this.buf = null\n      } else {\n        // Not enough data available\n        if (persist) {\n          // Copy/append incomplete block to backup buffer\n          this.buf = this.buf || Buffer.alloc(minCapacity)\n          chunk.copy(this.buf, this.bufPos, chunkPos, chunkLen)\n        } else {\n          // Push incomplete block after validation\n          slice = chunk.slice(chunkPos, chunkLen)\n          validate(slice, isDone) && _this.push(slice)\n        }\n        this.bufPos += chunkLen - chunkPos\n      }\n      return isDone\n    };\n    let safePush = _safePush.bind(this)\n\n    while (!isChunkProcessed) {\n      switch (this.state) {\n        case STATE_IDLE:\n          this.state = STATE_MARKER\n          break\n        case STATE_MARKER:\n          if (safePush(4, true, this._validateMarker.bind(this))) {\n            this.state = this.isFlac ? STATE_MDB_HEADER : STATE_PASS_THROUGH\n          } else {\n            isChunkProcessed = true\n          }\n          break\n        case STATE_MDB_HEADER:\n          if (safePush(4, true, this._validateMDBHeader.bind(this))) {\n            this.state = STATE_MDB\n          } else {\n            isChunkProcessed = true\n          }\n          break\n        case STATE_MDB:\n          if (safePush(this.mdbLen, this.parseMetaDataBlocks, this._validateMDB.bind(this))) {\n            if (this.mdb.isLast) {\n              // This MDB has the isLast flag set to true.\n              // Ignore all following MDBs.\n              this.mdbLastWritten = true\n            }\n            this.readCallBack && this.readCallBack(this.mdb)\n            if (this.mdbLast) {\n              this._writeVorbisComment()\n              this._writePicture()\n              this.state = STATE_PASS_THROUGH\n            } else {\n              this.state = STATE_MDB_HEADER\n            }\n          } else {\n            isChunkProcessed = true\n          }\n          break\n        case STATE_PASS_THROUGH:\n          safePush(chunkLen - chunkPos, false)\n          isChunkProcessed = true\n          break\n      }\n    }\n\n    done()\n  }\n\n  _validateMarker(slice, isDone) {\n    this.isFlac = (slice.toString('utf8', 0) === 'fLaC')\n    // TODO: completely bail out if file is not a FLAC?\n    return true\n  }\n\n  _validateMDBHeader(slice, isDone) {\n    // Parse MDB header\n    let header = slice.readUInt32BE(0)\n    let type = (header >>> 24) & 0x7f\n    this.mdbLast = (((header >>> 24) & 0x80) !== 0)\n    this.mdbLen = header & 0xffffff\n    // Create appropriate MDB object\n    // (data is injected later in _validateMDB, if parseMetaDataBlocks option is set to true)\n    switch (type) {\n      case this.MDB_TYPE_STREAMINFO:\n        this.mdb = new MetaDataBlockStreamInfo(this.mdbLast)\n        break\n      case this.MDB_TYPE_VORBIS_COMMENT:\n        if (this.waitWriteVorbis) {\n          this._writeVorbisComment(slice, header)\n          this.mdbPush = false\n          return this.mdbPush\n        } else {\n          this.mdb = new MetaDataBlockVorbisComment(this.mdbLast)\n          this.readCallback && this.readCallback(this.mdb)\n        }\n        break\n      case this.MDB_TYPE_PICTURE:\n        if (this.waitWritePicture) {\n          this._writePicture(slice, header)\n          this.mdbPush = false\n          return this.mdbPush\n        } else {\n          this.mdb = new MetaDataBlockPicture(this.mdbLast)\n          this.readCallback && this.readCallback(this.mdb)\n        }\n        break\n      case this.MDB_TYPE_PADDING:\n      case this.MDB_TYPE_APPLICATION:\n      case this.MDB_TYPE_SEEKTABLE:\n      case this.MDB_TYPE_CUESHEET:\n      case this.MDB_TYPE_INVALID:\n      default:\n        this.mdb = new MetaDataBlock(this.mdbLast, type)\n        break\n    }\n\n    // this.emit('preprocess', this.mdb)\n\n    if (this.mdbLastWritten) {\n      // A previous MDB had the isLast flag set to true.\n      // Ignore all following MDBs.\n      this.mdb.remove()\n    } else {\n      if (this.mdbLast && this.tasks > 0) {\n        header &= 0x7fffffff\n        slice.writeUInt32BE(header >>> 0, 0)\n      }\n      // The consumer may change the MDB's isLast flag in the preprocess handler.\n      // Here that flag is updated in the MDB header.\n    }\n    this.mdbPush = !this.mdb.removed\n    return this.mdbPush\n  }\n\n  _validateMDB(slice, isDone) {\n    // Parse the MDB if parseMetaDataBlocks option is set to true\n    if (this.parseMetaDataBlocks && isDone) {\n      this.mdb.parse(slice)\n    }\n    return this.mdbPush\n  }\n\n  _flush(done) {\n    // All chunks have been processed\n    // Clean up\n    this.state = STATE_IDLE\n    this.mdbLastWritten = false\n    this.isFlac = false\n    this.bufPos = 0\n    this.buf = null\n    this.mdb = null\n    done()\n  }\n\n  _writeVorbisComment() {\n    if (this.waitWriteVorbis == null) return\n    let isLast = this.mdbLast && this.tasks === 1\n    this.tasks--\n    this.push(MetaDataBlockVorbisComment.create(isLast, this.waitWriteVorbis.vendor, this.waitWriteVorbis.comments).publish())\n    this.waitWriteVorbis = null\n  }\n\n  _writePicture() {\n    if (this.waitWritePicture == null) return\n    let isLast = this.mdbLast && this.tasks === 1\n    this.tasks--\n    this.push(\n      MetaDataBlockPicture.create(\n        isLast, this.waitWritePicture.pictureType,\n        this.waitWritePicture.mimeType, this.waitWritePicture.description,\n        this.waitWritePicture.width, this.waitWritePicture.height,\n        this.waitWritePicture.bitsPerPixel, this.waitWritePicture.colors,\n        this.waitWritePicture.pictureData,\n      ).publish(),\n    )\n    this.waitWritePicture = null\n  }\n}\n\nmodule.exports = Processor\n"
  },
  {
    "path": "src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlock.js",
    "content": "class MetaDataBlock {\n  constructor(isLast, type) {\n    this.isLast = isLast\n    this.type = type\n    this.error = null\n    this.hasData = false\n    this.removed = false\n  }\n\n  remove() {\n    this.removed = true\n  }\n\n  parse(buffer) {\n  }\n\n  toString() {\n    let str = '[MetaDataBlock]'\n    str += ' type: ' + this.type\n    str += ', isLast: ' + this.isLast\n    return str\n  }\n}\n\nmodule.exports = MetaDataBlock\n"
  },
  {
    "path": "src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockPicture.js",
    "content": "const MetaDataBlock = require('./MetaDataBlock')\n\nclass MetaDataBlockPicture extends MetaDataBlock {\n  constructor(isLast) {\n    super(isLast, 6)\n\n    this.pictureType = 0\n    this.mimeType = ''\n    this.description = ''\n    this.width = 0\n    this.height = 0\n    this.bitsPerPixel = 0\n    this.colors = 0\n    this.pictureData = null\n  }\n\n  static create(isLast, pictureType, mimeType, description, width, height, bitsPerPixel, colors, pictureData) {\n    let mdb = new MetaDataBlockPicture(isLast)\n    mdb.pictureType = pictureType\n    mdb.mimeType = mimeType\n    mdb.description = description\n    mdb.width = width\n    mdb.height = height\n    mdb.bitsPerPixel = bitsPerPixel\n    mdb.colors = colors\n    mdb.pictureData = pictureData\n    mdb.hasData = true\n    return mdb\n  }\n\n\n  parse(buffer) {\n    try {\n      let pos = 0\n\n      this.pictureType = buffer.readUInt32BE(pos)\n      pos += 4\n\n      let mimeTypeLength = buffer.readUInt32BE(pos)\n      this.mimeType = buffer.toString('utf8', pos + 4, pos + 4 + mimeTypeLength)\n      pos += 4 + mimeTypeLength\n\n      let descriptionLength = buffer.readUInt32BE(pos)\n      this.description = buffer.toString('utf8', pos + 4, pos + 4 + descriptionLength)\n      pos += 4 + descriptionLength\n\n      this.width = buffer.readUInt32BE(pos)\n      this.height = buffer.readUInt32BE(pos + 4)\n      this.bitsPerPixel = buffer.readUInt32BE(pos + 8)\n      this.colors = buffer.readUInt32BE(pos + 12)\n      pos += 16\n\n      let pictureDataLength = buffer.readUInt32BE(pos)\n      this.pictureData = Buffer.alloc(pictureDataLength)\n      buffer.copy(this.pictureData, 0, pos + 4, pictureDataLength)\n\n      this.hasData = true\n    } catch (e) {\n      this.error = e\n      this.hasData = false\n    }\n  }\n\n  publish() {\n    let pos = 0\n    let size = this.getSize()\n    let buffer = Buffer.alloc(4 + size)\n\n    let header = size\n    header |= (this.type << 24)\n    header |= (this.isLast ? 0x80000000 : 0)\n    buffer.writeUInt32BE(header >>> 0, pos)\n    pos += 4\n\n    buffer.writeUInt32BE(this.pictureType, pos)\n    pos += 4\n\n    let mimeTypeLen = Buffer.byteLength(this.mimeType)\n    buffer.writeUInt32BE(mimeTypeLen, pos)\n    buffer.write(this.mimeType, pos + 4)\n    pos += 4 + mimeTypeLen\n\n    let descriptionLen = Buffer.byteLength(this.description)\n    buffer.writeUInt32BE(descriptionLen, pos)\n    buffer.write(this.description, pos + 4)\n    pos += 4 + descriptionLen\n\n    buffer.writeUInt32BE(this.width, pos)\n    buffer.writeUInt32BE(this.height, pos + 4)\n    buffer.writeUInt32BE(this.bitsPerPixel, pos + 8)\n    buffer.writeUInt32BE(this.colors, pos + 12)\n    pos += 16\n\n    buffer.writeUInt32BE(this.pictureData.length, pos)\n    this.pictureData.copy(buffer, pos + 4)\n\n    return buffer\n  }\n\n  getSize() {\n    let size = 4\n    size += 4 + Buffer.byteLength(this.mimeType)\n    size += 4 + Buffer.byteLength(this.description)\n    size += 16\n    size += 4 + this.pictureData.length\n    return size\n  }\n\n  toString() {\n    let str = '[MetaDataBlockPicture]'\n    str += ' type: ' + this.type\n    str += ', isLast: ' + this.isLast\n    if (this.error) {\n      str += '\\n  ERROR: ' + this.error\n    }\n    if (this.hasData) {\n      str += '\\n  pictureType: ' + this.pictureType\n      str += '\\n  mimeType: ' + this.mimeType\n      str += '\\n  description: ' + this.description\n      str += '\\n  width: ' + this.width\n      str += '\\n  height: ' + this.height\n      str += '\\n  bitsPerPixel: ' + this.bitsPerPixel\n      str += '\\n  colors: ' + this.colors\n      str += '\\n  pictureData: ' + (this.pictureData ? this.pictureData.length : '<null>')\n    }\n    return str\n  }\n}\n\nmodule.exports = MetaDataBlockPicture\n"
  },
  {
    "path": "src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockStreamInfo.js",
    "content": "const MetaDataBlock = require('./MetaDataBlock')\n\nfunction pad(n, width) {\n  n = '' + n\n  return (n.length >= width) ? n : new Array(width - n.length + 1).join('0') + n\n}\n\nclass MetaDataBlockStreamInfo extends MetaDataBlock {\n  constructor(isLast) {\n    super(isLast, 0)\n\n    this.minBlockSize = 0\n    this.maxBlockSize = 0\n    this.minFrameSize = 0\n    this.maxFrameSize = 0\n    this.sampleRate = 0\n    this.channels = 0\n    this.bitsPerSample = 0\n    this.samples = 0\n    this.checksum = null\n    this.duration = 0\n    this.durationStr = '0:00.000'\n  }\n\n  remove() {\n    console.error(\"WARNING: Can't remove StreamInfo block!\")\n  }\n\n  parse(buffer) {\n    try {\n      let pos = 0\n\n      this.minBlockSize = buffer.readUInt16BE(pos)\n      this.maxBlockSize = buffer.readUInt16BE(pos + 2)\n      this.minFrameSize = (buffer.readUInt8(pos + 4) << 16) | buffer.readUInt16BE(pos + 5)\n      this.maxFrameSize = (buffer.readUInt8(pos + 7) << 16) | buffer.readUInt16BE(pos + 8)\n\n      let tmp = buffer.readUInt32BE(pos + 10)\n      this.sampleRate = tmp >>> 12\n      this.channels = (tmp >>> 9) & 0x07\n      this.bitsPerSample = (tmp >>> 4) & 0x1f\n      this.samples = +((tmp & 0x0f) << 4) + buffer.readUInt32BE(pos + 14)\n\n      this.checksum = Buffer.alloc(16)\n      buffer.copy(this.checksum, 0, 18, 34)\n\n      this.duration = this.samples / this.sampleRate\n\n      let minutes = '' + Math.floor(this.duration / 60)\n      let seconds = pad(Math.floor(this.duration % 60), 2)\n      let milliseconds = pad(Math.round(((this.duration % 60) - Math.floor(this.duration % 60)) * 1000), 3)\n      this.durationStr = minutes + ':' + seconds + '.' + milliseconds\n\n      this.hasData = true\n    } catch (e) {\n      this.error = e\n      this.hasData = false\n    }\n  }\n\n  toString() {\n    let str = '[MetaDataBlockStreamInfo]'\n    str += ' type: ' + this.type\n    str += ', isLast: ' + this.isLast\n    if (this.error) {\n      str += '\\n  ERROR: ' + this.error\n    }\n    if (this.hasData) {\n      str += '\\n  minBlockSize: ' + this.minBlockSize\n      str += '\\n  maxBlockSize: ' + this.maxBlockSize\n      str += '\\n  minFrameSize: ' + this.minFrameSize\n      str += '\\n  maxFrameSize: ' + this.maxFrameSize\n      str += '\\n  samples: ' + this.samples\n      str += '\\n  sampleRate: ' + this.sampleRate\n      str += '\\n  channels: ' + (this.channels + 1)\n      str += '\\n  bitsPerSample: ' + (this.bitsPerSample + 1)\n      str += '\\n  duration: ' + this.durationStr\n      str += '\\n  checksum: ' + (this.checksum ? this.checksum.toString('hex') : '<null>')\n    }\n    return str\n  }\n}\n\nmodule.exports = MetaDataBlockStreamInfo\n"
  },
  {
    "path": "src/common/utils/musicMeta/flac-metadata/lib/MetaDataBlockVorbisComment.js",
    "content": "const MetaDataBlock = require('./MetaDataBlock')\n\nclass MetaDataBlockVorbisComment extends MetaDataBlock {\n  constructor(isLast) {\n    super(isLast, 4)\n\n    this.vendor = ''\n    this.comments = []\n  }\n\n  static create(isLast, vendor, comments) {\n    let mdb = new MetaDataBlockVorbisComment(isLast)\n    mdb.vendor = vendor\n    mdb.comments = comments\n    mdb.hasData = true\n    return mdb\n  }\n\n  parse(buffer) {\n    try {\n      let pos = 0\n\n      let vendorLen = buffer.readUInt32LE(pos)\n      let vendor = buffer.toString('utf8', pos + 4, pos + 4 + vendorLen)\n      this.vendor = vendor\n      pos += 4 + vendorLen\n\n      let commentCount = buffer.readUInt32LE(pos)\n      pos += 4\n\n      while (commentCount-- > 0) {\n        let commentLen = buffer.readUInt32LE(pos)\n        let comment = buffer.toString('utf8', pos + 4, pos + 4 + commentLen)\n        this.comments.push(comment)\n        pos += 4 + commentLen\n      }\n\n      this.hasData = true\n    } catch (e) {\n      this.error = e\n      this.hasData = false\n    }\n  }\n\n  publish() {\n    let pos = 0\n    let size = this.getSize()\n    let buffer = Buffer.alloc(4 + size)\n\n    let header = size\n    header |= (this.type << 24)\n    header |= (this.isLast ? 0x80000000 : 0)\n    buffer.writeUInt32BE(header >>> 0, pos)\n    pos += 4\n    let vendorLen = Buffer.byteLength(this.vendor)\n    buffer.writeUInt32LE(vendorLen, pos)\n    buffer.write(this.vendor, pos + 4)\n    pos += 4 + vendorLen\n\n    let commentCount = this.comments.length\n    buffer.writeUInt32LE(commentCount, pos)\n    pos += 4\n\n    for (let i = 0; i < commentCount; i++) {\n      let comment = this.comments[i]\n      let commentLen = Buffer.byteLength(comment)\n      buffer.writeUInt32LE(commentLen, pos)\n      buffer.write(comment, pos + 4)\n      pos += 4 + commentLen\n    }\n\n    return buffer\n  }\n\n  getSize() {\n    let size = 8 + Buffer.byteLength(this.vendor)\n    for (let i = 0; i < this.comments.length; i++) {\n      size += 4 + Buffer.byteLength(this.comments[i])\n    }\n    return size\n  }\n\n  toString() {\n    let str = '[MetaDataBlockVorbisComment]'\n    str += ' type: ' + this.type\n    str += ', isLast: ' + this.isLast\n    if (this.error) {\n      str += '\\n  ERROR: ' + this.error\n    }\n    if (this.hasData) {\n      str += '\\n  vendor: ' + this.vendor\n      if (this.comments.length) {\n        str += '\\n  comments:'\n        for (let i = 0; i < this.comments.length; i++) {\n          str += '\\n    ' + this.comments[i].split('=').join(': ')\n        }\n      } else {\n        str += '\\n  comments: none'\n      }\n    }\n    return str\n  }\n}\n\nmodule.exports = MetaDataBlockVorbisComment\n"
  },
  {
    "path": "src/common/utils/musicMeta/flacMeta.js",
    "content": "const fs = require('fs')\nconst fsPromises = fs.promises\nconst path = require('path')\nconst getImgSize = require('image-size')\nconst download = require('./downloader')\n\nconst FlacProcessor = require('./flac-metadata/index')\n\nconst extReg = /^(\\.(?:jpe?g|png)).*$/\nconst vendor = 'reference libFLAC 1.2.1 20070917'\n\nconst writeMeta = async(filePath, meta, picPath) => {\n  const comments = Object.keys(meta).map(key => `${key.toUpperCase()}=${meta[key] || ''}`)\n  const data = {\n    vorbis: {\n      vendor,\n      comments,\n    },\n  }\n  if (picPath) {\n    const apicData = await fsPromises.readFile(picPath)\n    let imgSize = getImgSize(apicData)\n    let mime_type\n    let bitsPerPixel\n    if (apicData[0] == 0xff && apicData[1] == 0xd8 && apicData[2] == 0xff) {\n      mime_type = 'image/jpeg'\n      bitsPerPixel = 24\n    } else {\n      mime_type = 'image/png'\n      bitsPerPixel = 32\n    }\n    data.picture = {\n      pictureType: 3,\n      mimeType: mime_type,\n      description: '',\n      width: imgSize.width,\n      height: imgSize.height,\n      bitsPerPixel,\n      colors: 0,\n      pictureData: apicData,\n    }\n  }\n\n  const reader = fs.createReadStream(filePath)\n  const tempPath = filePath + '.lxmtemp'\n  const writer = fs.createWriteStream(tempPath)\n  const flacProcessor = new FlacProcessor()\n  flacProcessor.writeMeta(data)\n\n  reader.pipe(flacProcessor).pipe(writer).on('finish', () => {\n    fs.unlink(filePath, err => {\n      if (err) return console.log(err.message)\n      fs.rename(tempPath, filePath, err => {\n        if (err) console.log(err.message)\n      })\n    })\n  })\n}\n\nmodule.exports = (filePath, meta, proxy) => {\n  if (!meta.APIC) return writeMeta(filePath, meta)\n  let picUrl = meta.APIC\n  delete meta.APIC\n  if (!/^http/.test(picUrl)) {\n    return writeMeta(filePath, meta)\n  }\n  let ext = path.extname(picUrl)\n  let picPath = filePath.replace(/\\.flac$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')\n\n  if (picUrl.includes('music.126.net')) picUrl += `${picUrl.includes('?') ? '&' : '?'}param=500y500`\n  download(picUrl, picPath, proxy).then(success => {\n    if (success) {\n      writeMeta(filePath, meta, picPath).finally(() => {\n        fs.unlink(picPath, err => {\n          if (err) console.log(err.message)\n        })\n      })\n    } else writeMeta(filePath, meta)\n  })\n}\n\n"
  },
  {
    "path": "src/common/utils/musicMeta/index.d.ts",
    "content": "export interface MusicMeta {\n  title: string\n  artist: string | null\n  album: string | null\n  APIC: string | null\n  lyrics: string | null\n}\nexport function setMeta(filePath: string, meta: MusicMeta, proxy?: { host: string, port: number }): void\n"
  },
  {
    "path": "src/common/utils/musicMeta/index.js",
    "content": "const path = require('path')\nconst mp3Meta = require('./mp3Meta')\nconst flacMeta = require('./flacMeta')\n\nexports.setMeta = (filePath, meta, proxy) => {\n  switch (path.extname(filePath)) {\n    case '.mp3':\n      mp3Meta(filePath, meta, proxy)\n      break\n    case '.flac':\n      flacMeta(filePath, meta, proxy)\n      break\n  }\n}\n"
  },
  {
    "path": "src/common/utils/musicMeta/mp3Meta.js",
    "content": "const NodeID3 = require('node-id3')\nconst path = require('path')\nconst fs = require('fs')\nconst download = require('./downloader')\nconst extReg = /^(\\.(?:jpe?g|png)).*$/\n\nconst handleWriteMeta = (meta, filePath) => {\n  if (meta.lyrics) {\n    meta.unsynchronisedLyrics = {\n      language: 'zho',\n      text: meta.lyrics,\n    }\n    delete meta.lyrics\n  }\n  NodeID3.write(meta, filePath)\n}\n\nmodule.exports = (filePath, meta, proxy) => {\n  if (!meta.APIC) return handleWriteMeta(meta, filePath)\n  if (!/^http/.test(meta.APIC)) {\n    delete meta.APIC\n    return handleWriteMeta(meta, filePath)\n  }\n  let ext = path.extname(meta.APIC)\n  let picPath = filePath.replace(/\\.mp3$/, '') + (ext ? ext.replace(extReg, '$1') : '.jpg')\n\n  let picUrl = meta.APIC\n  if (picUrl.includes('music.126.net')) picUrl += `${picUrl.includes('?') ? '&' : '?'}param=500y500`\n  download(picUrl, picPath, proxy).then(success => {\n    if (success) {\n      meta.APIC = picPath\n      handleWriteMeta(meta, filePath)\n      fs.unlink(picPath, err => {\n        if (err) console.log(err.message)\n      })\n    } else {\n      delete meta.APIC\n      handleWriteMeta(meta, filePath)\n    }\n  })\n}\n"
  },
  {
    "path": "src/common/utils/nodejs.ts",
    "content": "import fs from 'node:fs'\nimport crypto from 'node:crypto'\nimport { gzip, gunzip } from 'node:zlib'\nimport path from 'node:path'\nimport { networkInterfaces } from 'node:os'\nimport { log } from '@common/utils'\n\nexport const joinPath = (...paths: string[]): string => path.join(...paths)\n\nexport const extname = (p: string): string => path.extname(p)\nexport const basename = (p: string, ext?: string): string => path.basename(p, ext)\nexport const dirname = (p: string): string => path.dirname(p)\n\n/**\n * 检查路径是否存在\n * @param {*} path 路径\n */\nexport const checkPath = async(path: string): Promise<boolean> => {\n  return new Promise(resolve => {\n    if (!path) {\n      resolve(false)\n      return\n    }\n    fs.access(path, fs.constants.F_OK, err => {\n      if (err) {\n        resolve(false)\n        return\n      }\n      resolve(true)\n    })\n  })\n}\n\n/**\n * 检查路径并创建目录\n * @param path\n * @returns\n */\nexport const checkAndCreateDir = async(path: string) => {\n  return fs.promises.access(path, fs.constants.F_OK | fs.constants.W_OK)\n    .catch(async(err: NodeJS.ErrnoException) => {\n      if (err.code != 'ENOENT') throw err as Error\n      return fs.promises.mkdir(path, { recursive: true })\n    })\n    .then(() => true)\n    .catch((err) => {\n      console.error(err)\n      return false\n    })\n}\n\n\nexport const getFileStats = async(path: string): Promise<fs.Stats | null> => {\n  return new Promise(resolve => {\n    if (!path) {\n      resolve(null)\n      return\n    }\n    fs.stat(path, (err, stats) => {\n      if (err) {\n        resolve(null)\n        return\n      }\n      resolve(stats)\n    })\n  })\n}\n\n/**\n * 检查路径并创建目录\n * @param path\n * @returns\n */\nexport const createDir = async(path: string) => new Promise<void>((resolve, reject) => {\n  fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {\n    if (err) {\n      if (err.code === 'ENOENT') {\n        fs.mkdir(path, { recursive: true }, err => {\n          if (err) {\n            reject(err)\n            return\n          }\n          resolve()\n        })\n        return\n      }\n      reject(err)\n      return\n    }\n    resolve()\n  })\n})\n\nexport const removeFile = async(path: string) => new Promise<void>((resolve, reject) => {\n  fs.access(path, fs.constants.F_OK, err => {\n    if (err) {\n      err.code == 'ENOENT' ? resolve() : reject(err)\n      return\n    }\n    fs.unlink(path, err => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve()\n    })\n  })\n})\n\nexport const readFile = async(path: string) => fs.promises.readFile(path)\n\n\n/**\n * 创建 MD5 hash\n * @param {*} str\n */\nexport const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex')\n\nexport const gzipData = async(str: string): Promise<Buffer> => {\n  return new Promise((resolve, reject) => {\n    gzip(str, (err, result) => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve(result)\n    })\n  })\n}\n\nexport const gunzipData = async(buf: Buffer): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    gunzip(buf, (err, result) => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve(result.toString())\n    })\n  })\n}\n\n/**\n * 保存lx配置文件\n * @param path 保存路径\n * @param data 数据\n */\nexport const saveLxConfigFile = async(path: string, data: any) => {\n  if (!path.endsWith('.lxmc')) path += '.lxmc'\n  fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {\n    console.log(err)\n  })\n}\n\n/**\n * 读取lx配置文件\n * @param path 文件路径\n * @returns 数据\n */\nexport const readLxConfigFile = async(path: string): Promise<any> => {\n  let isJSON = path.endsWith('.json')\n  let data: string | Buffer = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')\n  if (!data) return data\n  if (!isJSON) data = await gunzipData(Buffer.from(data, 'binary'))\n  data = JSON.parse(data)\n\n  // 修复v1.14.0出现的导出数据被序列化两次的问题\n  if (typeof data != 'object') {\n    try {\n      data = JSON.parse(data)\n    } catch (err) {\n      return data\n    }\n  }\n\n  return data\n}\n\nexport const saveStrToFile = async(path: string, str: string | Buffer): Promise<void> => {\n  await new Promise<void>((resolve, reject) => {\n    fs.writeFile(path, str, err => {\n      if (err) {\n        log.error(err)\n        reject(err)\n        return\n      }\n      resolve()\n    })\n  })\n}\n\nexport const b64DecodeUnicode = (str: string): string => {\n  // Going backwards: from bytestream, to percent-encoding, to original string.\n  // return decodeURIComponent(window.atob(str).split('').map(function(c) {\n  //   return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)\n  // }).join(''))\n\n  return Buffer.from(str, 'base64').toString()\n}\n\nexport const copyFile = async(sourcePath: string, distPath: string) => {\n  return fs.promises.copyFile(sourcePath, distPath)\n}\n\nexport const moveFile = async(sourcePath: string, distPath: string) => {\n  return fs.promises.rename(sourcePath, distPath)\n}\n\nexport const getAddress = (): string[] => {\n  const nets = networkInterfaces()\n  const results: string[] = []\n  // console.log(nets)\n\n  for (const interfaceInfos of Object.values(nets)) {\n    if (!interfaceInfos) continue\n    // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses\n    for (const interfaceInfo of interfaceInfos) {\n      if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {\n        results.push(interfaceInfo.address)\n      }\n    }\n  }\n  return results\n}\n"
  },
  {
    "path": "src/common/utils/pinyin/kMandarin_8105.txt",
    "content": "# https://github.com/mozillazg/pinyin-data\n# 《通用规范汉字表》(2013 年版)里 8105 个汉字的最常用读音\n# -> 表示需要验证\n# ? 表示有争议，无法确定哪个是最常用的\n# <- 表示需要补充拼音信息\n# URO (7,832)\nU+4E00: yī  # 一\nU+4E01: dīng  # 丁\nU+4E03: qī  # 七\nU+4E07: wàn  # 万\nU+4E08: zhàng  # 丈\nU+4E09: sān  # 三\nU+4E0A: shàng  # 上\nU+4E0B: xià  # 下\nU+4E0D: bù  # 不\nU+4E0E: yǔ  # 与\nU+4E0F: miǎn  # 丏\nU+4E10: gài  # 丐\nU+4E11: chǒu  # 丑\nU+4E13: zhuān  # 专\nU+4E14: qiě  # 且\nU+4E15: pī  # 丕\nU+4E16: shì  # 世\nU+4E18: qiū  # 丘\nU+4E19: bǐng  # 丙\nU+4E1A: yè  # 业\nU+4E1B: cóng  # 丛\nU+4E1C: dōng  # 东\nU+4E1D: sī  # 丝\nU+4E1E: chéng  # 丞\nU+4E22: diū  # 丢\nU+4E24: liǎng  # 两\nU+4E25: yán  # 严\nU+4E27: sàng  # 丧 ?-> sāng\nU+4E2A: gè  # 个\nU+4E2B: yā  # 丫\nU+4E2D: zhōng  # 中\nU+4E30: fēng  # 丰\nU+4E32: chuàn  # 串\nU+4E34: lín  # 临\nU+4E38: wán  # 丸\nU+4E39: dān  # 丹\nU+4E3A: wèi  # 为 ? -> wéi\nU+4E3B: zhǔ  # 主\nU+4E3D: lì  # 丽\nU+4E3E: jǔ  # 举\nU+4E42: yì  # 乂\nU+4E43: nǎi  # 乃\nU+4E45: jiǔ  # 久\nU+4E48: me  # 么\nU+4E49: yì  # 义\nU+4E4B: zhī  # 之\nU+4E4C: wū  # 乌\nU+4E4D: zhà  # 乍\nU+4E4E: hū  # 乎\nU+4E4F: fá  # 乏\nU+4E50: lè  # 乐\nU+4E52: pīng  # 乒\nU+4E53: pāng  # 乓\nU+4E54: qiáo  # 乔\nU+4E56: guāi  # 乖\nU+4E58: chéng  # 乘\nU+4E59: yǐ  # 乙\nU+4E5C: miē  # 乜\nU+4E5D: jiǔ  # 九\nU+4E5E: qǐ  # 乞\nU+4E5F: yě  # 也\nU+4E60: xí  # 习\nU+4E61: xiāng  # 乡\nU+4E66: shū  # 书\nU+4E69: jī  # 乩\nU+4E70: mǎi  # 买\nU+4E71: luàn  # 乱\nU+4E73: rǔ  # 乳\nU+4E78: nǎ  # 乸\nU+4E7E: qián  # 乾 ?-> gān\nU+4E86: le  # 了\nU+4E88: yǔ  # 予 ?-> yú\nU+4E89: zhēng  # 争\nU+4E8B: shì  # 事\nU+4E8C: èr  # 二\nU+4E8D: chù  # 亍\nU+4E8E: yú  # 于\nU+4E8F: kuī  # 亏\nU+4E91: yún  # 云\nU+4E92: hù  # 互\nU+4E93: qí  # 亓\nU+4E94: wǔ  # 五\nU+4E95: jǐng  # 井\nU+4E98: gèn  # 亘\nU+4E9A: yà  # 亚\nU+4E9B: xiē  # 些\nU+4E9F: jí  # 亟\nU+4EA1: wáng  # 亡\nU+4EA2: kàng  # 亢\nU+4EA4: jiāo  # 交\nU+4EA5: hài  # 亥\nU+4EA6: yì  # 亦\nU+4EA7: chǎn  # 产\nU+4EA8: hēng  # 亨\nU+4EA9: mǔ  # 亩\nU+4EAB: xiǎng  # 享\nU+4EAC: jīng  # 京\nU+4EAD: tíng  # 亭\nU+4EAE: liàng  # 亮\nU+4EB2: qīn  # 亲\nU+4EB3: bó  # 亳\nU+4EB5: xiè  # 亵\nU+4EB6: dǎn  # 亶\nU+4EB8: duǒ  # 亸\nU+4EB9: wěi  # 亹 ?-> mén\nU+4EBA: rén  # 人\nU+4EBF: yì  # 亿\nU+4EC0: shén  # 什\nU+4EC1: rén  # 仁\nU+4EC2: lè  # 仂\nU+4EC3: dīng  # 仃\nU+4EC4: zè  # 仄\nU+4EC5: jǐn  # 仅\nU+4EC6: pū  # 仆\nU+4EC7: chóu  # 仇\nU+4EC9: zhǎng  # 仉\nU+4ECA: jīn  # 今\nU+4ECB: jiè  # 介\nU+4ECD: réng  # 仍\nU+4ECE: cóng  # 从\nU+4ED1: lún  # 仑\nU+4ED3: cāng  # 仓\nU+4ED4: zǎi  # 仔 ?-> zǐ zī\nU+4ED5: shì  # 仕\nU+4ED6: tā  # 他\nU+4ED7: zhàng  # 仗\nU+4ED8: fù  # 付\nU+4ED9: xiān  # 仙\nU+4EDD: tóng  # 仝\nU+4EDE: rèn  # 仞\nU+4EDF: qiān  # 仟\nU+4EE1: gē  # 仡 ?-> yì\nU+4EE3: dài  # 代\nU+4EE4: lìng  # 令\nU+4EE5: yǐ  # 以\nU+4EE8: sā  # 仨\nU+4EEA: yí  # 仪\nU+4EEB: mù  # 仫\nU+4EEC: men  # 们 ?-> mén\nU+4EF0: yǎng  # 仰\nU+4EF2: zhòng  # 仲\nU+4EF3: pǐ  # 仳\nU+4EF5: wǔ  # 仵\nU+4EF6: jiàn  # 件\nU+4EF7: jià  # 价\nU+4EFB: rèn  # 任\nU+4EFD: fèn  # 份\nU+4EFF: fǎng  # 仿\nU+4F01: qǐ  # 企\nU+4F08: xǐn  # 伈\nU+4F09: kàng  # 伉\nU+4F0A: yī  # 伊\nU+4F0B: jí  # 伋\nU+4F0D: wǔ  # 伍\nU+4F0E: jì  # 伎\nU+4F0F: fú  # 伏\nU+4F10: fá  # 伐\nU+4F11: xiū  # 休\nU+4F17: zhòng  # 众\nU+4F18: yōu  # 优\nU+4F19: huǒ  # 伙\nU+4F1A: huì  # 会\nU+4F1B: yǔ  # 伛\nU+4F1E: sǎn  # 伞\nU+4F1F: wěi  # 伟\nU+4F20: chuán  # 传\nU+4F22: yá  # 伢\nU+4F23: qiàn  # 伣\nU+4F24: shāng  # 伤\nU+4F25: chāng  # 伥\nU+4F26: lún  # 伦\nU+4F27: cāng  # 伧\nU+4F2A: wěi  # 伪\nU+4F2B: zhù  # 伫\nU+4F2D: xián  # 伭\nU+4F2F: bó  # 伯\nU+4F30: gū  # 估\nU+4F32: nì  # 伲 ?-> nǐ\nU+4F34: bàn  # 伴\nU+4F36: líng  # 伶\nU+4F38: shēn  # 伸\nU+4F3A: cì  # 伺\nU+4F3C: shì  # 似 ?-> sì\nU+4F3D: gā  # 伽 ?-> jiā qié\nU+4F3E: pī  # 伾\nU+4F41: yǐ  # 佁\nU+4F43: diàn  # 佃\nU+4F46: dàn  # 但\nU+4F4D: wèi  # 位\nU+4F4E: dī  # 低\nU+4F4F: zhù  # 住\nU+4F50: zuǒ  # 佐\nU+4F51: yòu  # 佑\nU+4F53: tǐ  # 体\nU+4F55: hé  # 何\nU+4F56: bì  # 佖\nU+4F57: tuó  # 佗\nU+4F58: shé  # 佘\nU+4F59: yú  # 余\nU+4F5A: yì  # 佚\nU+4F5B: fú  # 佛 ?-> fó\nU+4F5C: zuò  # 作\nU+4F5D: gōu  # 佝\nU+4F5E: nìng  # 佞\nU+4F5F: tóng  # 佟\nU+4F60: nǐ  # 你\nU+4F63: yōng  # 佣\nU+4F64: wǎ  # 佤\nU+4F65: qiān  # 佥\nU+4F69: pèi  # 佩\nU+4F6C: lǎo  # 佬\nU+4F6F: yáng  # 佯\nU+4F70: bǎi  # 佰\nU+4F73: jiā  # 佳\nU+4F74: èr  # 佴\nU+4F76: jí  # 佶\nU+4F78: huó  # 佸\nU+4F7A: quán  # 佺\nU+4F7B: tiāo  # 佻\nU+4F7C: jiǎo  # 佼\nU+4F7D: cì  # 佽\nU+4F7E: yì  # 佾\nU+4F7F: shǐ  # 使\nU+4F81: shēn  # 侁\nU+4F82: tuō  # 侂\nU+4F83: kǎn  # 侃\nU+4F84: zhí  # 侄\nU+4F88: chǐ  # 侈\nU+4F89: kuǎ  # 侉\nU+4F8B: lì  # 例\nU+4F8D: shì  # 侍\nU+4F8F: zhū  # 侏\nU+4F91: yòu  # 侑\nU+4F94: móu  # 侔\nU+4F97: dòng  # 侗\nU+4F98: chà  # 侘\nU+4F9B: gōng  # 供\nU+4F9D: yī  # 依\nU+4FA0: xiá  # 侠\nU+4FA3: lǚ  # 侣\nU+4FA5: jiǎo  # 侥\nU+4FA6: zhēn  # 侦\nU+4FA7: cè  # 侧\nU+4FA8: qiáo  # 侨\nU+4FA9: kuài  # 侩\nU+4FAA: chái  # 侪\nU+4FAC: nóng  # 侬\nU+4FAE: wǔ  # 侮\nU+4FAF: hóu  # 侯\nU+4FB4: chǒu  # 侴 ?-> hào\nU+4FB5: qīn  # 侵\nU+4FB9: tǐng  # 侹\nU+4FBF: biàn  # 便\nU+4FC3: cù  # 促\nU+4FC4: é  # 俄\nU+4FC5: qiú  # 俅\nU+4FCA: jùn  # 俊\nU+4FCD: liáng  # 俍\nU+4FCE: zǔ  # 俎\nU+4FCF: qiào  # 俏\nU+4FD0: lì  # 俐\nU+4FD1: yǒng  # 俑\nU+4FD7: sú  # 俗\nU+4FD8: fú  # 俘\nU+4FD9: xī  # 俙\nU+4FDA: lǐ  # 俚\nU+4FDC: pīng  # 俜\nU+4FDD: bǎo  # 保\nU+4FDE: yú  # 俞\nU+4FDF: qí  # 俟 ?-> sì\nU+4FE1: xìn  # 信\nU+4FE3: yǔ  # 俣\nU+4FE6: chóu  # 俦\nU+4FE8: yǎn  # 俨\nU+4FE9: liǎ  # 俩 ?-> liǎng\nU+4FEA: lì  # 俪\nU+4FEB: lái  # 俫\nU+4FED: jiǎn  # 俭\nU+4FEE: xiū  # 修\nU+4FEF: fǔ  # 俯\nU+4FF1: jù  # 俱\nU+4FF3: pái  # 俳\nU+4FF5: biào  # 俵\nU+4FF6: chù  # 俶\nU+4FF8: fèng  # 俸\nU+4FFA: ǎn  # 俺\nU+4FFE: bǐ  # 俾\nU+500C: guān  # 倌\nU+500D: bèi  # 倍\nU+500F: shū  # 倏\nU+5012: dào  # 倒 ?-> dǎo\nU+5013: tán  # 倓\nU+5014: jué  # 倔\nU+5015: chuí  # 倕\nU+5018: tǎng  # 倘\nU+5019: hòu  # 候\nU+501A: yǐ  # 倚\nU+501C: tì  # 倜\nU+501E: jìng  # 倞 -> liàng\nU+501F: jiè  # 借\nU+5021: chàng  # 倡\nU+5025: kōng  # 倥\nU+5026: juàn  # 倦\nU+5027: zōng  # 倧\nU+5028: jù  # 倨\nU+5029: qiàn  # 倩\nU+502A: ní  # 倪\nU+502C: zhuō  # 倬\nU+502D: wō  # 倭\nU+502E: luǒ  # 倮\nU+5034: bèn  # 倴\nU+503A: zhài  # 债\nU+503B: yē  # 倻\nU+503C: zhí  # 值\nU+503E: qīng  # 倾\nU+5041: chēng  # 偁\nU+5043: yǎn  # 偃\nU+5047: jiǎ  # 假\nU+5048: jì  # 偈\nU+504C: ruò  # 偌\nU+504E: wēi  # 偎\nU+504F: piān  # 偏\nU+5053: wò  # 偓\nU+5055: xié  # 偕\nU+505A: zuò  # 做\nU+505C: tíng  # 停\nU+5061: zhàn  # 偡\nU+5065: jiàn  # 健\nU+506C: zǒng  # 偬\nU+506D: miǎn  # 偭\nU+5070: xiè  # 偰\nU+5072: cāi  # 偲\nU+5076: ǒu  # 偶\nU+5077: tōu  # 偷\nU+507B: lóu  # 偻 -> lǚ\nU+507E: fèn  # 偾\nU+507F: cháng  # 偿\nU+5080: guī  # 傀 -> kuǐ\nU+5083: sù  # 傃\nU+5085: fù  # 傅\nU+5088: lì  # 傈\nU+5089: nù  # 傉\nU+508D: bàng  # 傍\nU+5092: xī  # 傒\nU+5095: jué  # 傕 -> què\nU+50A3: dǎi  # 傣\nU+50A5: tǎng  # 傥\nU+50A7: bīn  # 傧\nU+50A8: chǔ  # 储\nU+50A9: nuó  # 傩\nU+50AC: cuī  # 催\nU+50B2: ào  # 傲\nU+50BA: chì  # 傺\nU+50BB: shǎ  # 傻\nU+50C7: lù  # 僇\nU+50CE: zhuàn  # 僎\nU+50CF: xiàng  # 像\nU+50D4: zǔn  # 僔\nU+50D6: xī  # 僖\nU+50DA: liáo  # 僚\nU+50E6: jiù  # 僦\nU+50E7: sēng  # 僧\nU+50EC: jiāo  # 僬\nU+50ED: jiàn  # 僭\nU+50EE: tóng  # 僮\nU+50F0: bó  # 僰\nU+50F3: sù  # 僳\nU+50F5: jiāng  # 僵\nU+50FB: pì  # 僻\nU+5106: jǐng  # 儆\nU+5107: xuān  # 儇\nU+510B: dān  # 儋\nU+5112: rú  # 儒\nU+5121: lěi  # 儡\nU+5126: biāo  # 儦\nU+5133: chán  # 儳\nU+5134: ráng  # 儴\nU+513F: ér  # 儿\nU+5140: wù  # 兀\nU+5141: yǔn  # 允\nU+5143: yuán  # 元\nU+5144: xiōng  # 兄\nU+5145: chōng  # 充\nU+5146: zhào  # 兆\nU+5148: xiān  # 先\nU+5149: guāng  # 光\nU+514B: kè  # 克\nU+514D: miǎn  # 免\nU+5151: duì  # 兑\nU+5154: tù  # 兔\nU+5155: sì  # 兕\nU+5156: yǎn  # 兖\nU+515A: dǎng  # 党\nU+515C: dōu  # 兜\nU+5162: jīng  # 兢\nU+5165: rù  # 入\nU+5168: quán  # 全\nU+516B: bā  # 八\nU+516C: gōng  # 公\nU+516D: liù  # 六\nU+516E: xī  # 兮\nU+5170: lán  # 兰\nU+5171: gòng  # 共\nU+5173: guān  # 关\nU+5174: xīng  # 兴 -> xìng\nU+5175: bīng  # 兵\nU+5176: qí  # 其\nU+5177: jù  # 具\nU+5178: diǎn  # 典\nU+5179: zī  # 兹\nU+517B: yǎng  # 养\nU+517C: jiān  # 兼\nU+517D: shòu  # 兽\nU+5180: jì  # 冀\nU+5181: chǎn  # 冁\nU+5185: nèi  # 内\nU+5188: gāng  # 冈\nU+5189: rǎn  # 冉\nU+518C: cè  # 册\nU+518D: zài  # 再\nU+518F: jiǒng  # 冏\nU+5192: mào  # 冒\nU+5194: xǔ  # 冔 -> xú\nU+5195: miǎn  # 冕\nU+5197: rǒng  # 冗\nU+5199: xiě  # 写\nU+519B: jūn  # 军\nU+519C: nóng  # 农\nU+51A0: guān  # 冠\nU+51A2: zhǒng  # 冢\nU+51A4: yuān  # 冤\nU+51A5: míng  # 冥\nU+51AC: dōng  # 冬\nU+51AE: gāng  # 冮\nU+51AF: féng  # 冯\nU+51B0: bīng  # 冰\nU+51B1: hù  # 冱\nU+51B2: chōng  # 冲\nU+51B3: jué  # 决\nU+51B5: kuàng  # 况\nU+51B6: yě  # 冶\nU+51B7: lěng  # 冷\nU+51BB: dòng  # 冻\nU+51BC: xiǎn  # 冼\nU+51BD: liè  # 冽\nU+51C0: jìng  # 净\nU+51C4: qī  # 凄\nU+51C6: zhǔn  # 准\nU+51C7: sōng  # 凇\nU+51C9: liáng  # 凉\nU+51CB: diāo  # 凋\nU+51CC: líng  # 凌\nU+51CF: jiǎn  # 减\nU+51D1: còu  # 凑\nU+51D3: lì  # 凓\nU+51D8: sī  # 凘\nU+51DB: lǐn  # 凛\nU+51DD: níng  # 凝\nU+51E0: jǐ  # 几 -> jī\nU+51E1: fán  # 凡\nU+51E4: fèng  # 凤\nU+51EB: fú  # 凫\nU+51ED: píng  # 凭\nU+51EF: kǎi  # 凯\nU+51F0: huáng  # 凰\nU+51F3: dèng  # 凳\nU+51F6: xiōng  # 凶\nU+51F8: tū  # 凸\nU+51F9: āo  # 凹\nU+51FA: chū  # 出\nU+51FB: jī  # 击\nU+51FC: dàng  # 凼\nU+51FD: hán  # 函\nU+51FF: záo  # 凿\nU+5200: dāo  # 刀\nU+5201: diāo  # 刁\nU+5203: rèn  # 刃\nU+5206: fēn  # 分\nU+5207: qiè  # 切 -> qiē\nU+5208: yì  # 刈\nU+520A: kān  # 刊\nU+520D: chú  # 刍\nU+520E: wěn  # 刎\nU+5211: xíng  # 刑\nU+5212: huà  # 划 -> huá\nU+5216: yuè  # 刖\nU+5217: liè  # 列\nU+5218: liú  # 刘\nU+5219: zé  # 则\nU+521A: gāng  # 刚\nU+521B: chuàng  # 创\nU+521D: chū  # 初\nU+5220: shān  # 删\nU+5224: pàn  # 判\nU+5228: páo  # 刨\nU+5229: lì  # 利\nU+522B: bié  # 别\nU+522C: chǎn  # 刬\nU+522D: jǐng  # 刭\nU+522E: guā  # 刮\nU+5230: dào  # 到\nU+5233: kū  # 刳\nU+5236: zhì  # 制\nU+5237: shuā  # 刷\nU+5238: quàn  # 券\nU+5239: shā  # 刹 -> chà\nU+523A: cì  # 刺\nU+523B: kè  # 刻\nU+523D: guì  # 刽\nU+523F: guì  # 刿\nU+5240: kǎi  # 剀\nU+5241: duò  # 剁\nU+5242: jì  # 剂\nU+5243: tì  # 剃\nU+5245: lóu  # 剅\nU+524A: xuē  # 削 -> xiāo\nU+524B: kè  # 剋 -> kēi\nU+524C: lá  # 剌 -> là\nU+524D: qián  # 前\nU+5250: guǎ  # 剐\nU+5251: jiàn  # 剑\nU+5254: tī  # 剔\nU+5255: fèi  # 剕\nU+5256: pōu  # 剖\nU+525C: wān  # 剜\nU+525E: jī  # 剞\nU+525F: duō  # 剟\nU+5261: shàn  # 剡 -> yǎn\nU+5265: bō  # 剥 -> bāo\nU+5267: jù  # 剧\nU+5269: shèng  # 剩\nU+526A: jiǎn  # 剪\nU+526F: fù  # 副\nU+5272: gē  # 割\nU+527D: piāo  # 剽\nU+527F: jiǎo  # 剿\nU+5281: qiāo  # 劁\nU+5282: jué  # 劂\nU+5284: zhā  # 劄\nU+5288: pī  # 劈\nU+5290: huō  # 劐\nU+5293: yì  # 劓\nU+529B: lì  # 力\nU+529D: quàn  # 劝\nU+529E: bàn  # 办\nU+529F: gōng  # 功\nU+52A0: jiā  # 加\nU+52A1: wù  # 务\nU+52A2: mài  # 劢\nU+52A3: liè  # 劣\nU+52A8: dòng  # 动\nU+52A9: zhù  # 助\nU+52AA: nǔ  # 努\nU+52AB: jié  # 劫\nU+52AC: qú  # 劬\nU+52AD: shào  # 劭\nU+52B1: lì  # 励\nU+52B2: jìn  # 劲\nU+52B3: láo  # 劳\nU+52BC: jié  # 劼\nU+52BE: hé  # 劾\nU+52BF: shì  # 势\nU+52C3: bó  # 勃\nU+52C7: yǒng  # 勇\nU+52C9: miǎn  # 勉\nU+52CB: xūn  # 勋\nU+52CD: qíng  # 勍\nU+52D0: měng  # 勐\nU+52D2: lēi  # 勒 -> lè\nU+52D4: miǎn  # 勔\nU+52D6: xù  # 勖\nU+52D8: kān  # 勘\nU+52DA: yì  # 勚\nU+52DF: mù  # 募\nU+52E0: lù  # 勠\nU+52E4: qín  # 勤\nU+52F0: xié  # 勰\nU+52FA: sháo  # 勺\nU+52FE: gōu  # 勾\nU+52FF: wù  # 勿\nU+5300: yún  # 匀\nU+5305: bāo  # 包\nU+5306: cōng  # 匆\nU+5308: xiōng  # 匈\nU+530D: pú  # 匍\nU+530F: páo  # 匏\nU+5310: fú  # 匐\nU+5315: bǐ  # 匕\nU+5316: huà  # 化\nU+5317: běi  # 北\nU+5319: shi  # 匙 -> chí\nU+531C: yí  # 匜\nU+531D: zā  # 匝\nU+5320: jiàng  # 匠\nU+5321: kuāng  # 匡\nU+5323: xiá  # 匣\nU+5326: guǐ  # 匦\nU+532A: fěi  # 匪\nU+532E: kuì  # 匮\nU+5339: pǐ  # 匹\nU+533A: qū  # 区\nU+533B: yī  # 医\nU+533C: kē  # 匼\nU+533E: biǎn  # 匾\nU+533F: nì  # 匿\nU+5341: shí  # 十\nU+5343: qiān  # 千\nU+5345: sà  # 卅\nU+5347: shēng  # 升\nU+5348: wǔ  # 午\nU+5349: huì  # 卉\nU+534A: bàn  # 半\nU+534E: huá  # 华\nU+534F: xié  # 协\nU+5351: bēi  # 卑\nU+5352: zú  # 卒\nU+5353: zhuó  # 卓\nU+5355: dān  # 单\nU+5356: mài  # 卖\nU+5357: nán  # 南\nU+535A: bó  # 博\nU+535C: bo  # 卜 -> bǔ\nU+535E: biàn  # 卞\nU+535F: bǔ  # 卟\nU+5360: zhàn  # 占 -> zhān\nU+5361: kǎ  # 卡\nU+5362: lú  # 卢\nU+5363: yǒu  # 卣\nU+5364: lǔ  # 卤\nU+5366: guà  # 卦\nU+5367: wò  # 卧\nU+536B: wèi  # 卫\nU+536C: áng  # 卬 -> yǎng\nU+536E: zhī  # 卮\nU+536F: mǎo  # 卯\nU+5370: yìn  # 印\nU+5371: wēi  # 危\nU+5373: jí  # 即\nU+5374: què  # 却\nU+5375: luǎn  # 卵\nU+5377: juǎn  # 卷 -> juàn\nU+5378: xiè  # 卸\nU+537A: jǐn  # 卺\nU+537F: qīng  # 卿\nU+5382: chǎng  # 厂\nU+5384: è  # 厄\nU+5385: tīng  # 厅\nU+5386: lì  # 历\nU+5389: lì  # 厉\nU+538B: yā  # 压\nU+538C: yàn  # 厌\nU+538D: shè  # 厍\nU+5395: cè  # 厕\nU+5396: páng  # 厖 -> máng\nU+5398: lí  # 厘\nU+539A: hòu  # 厚\nU+539D: cuò  # 厝\nU+539F: yuán  # 原\nU+53A2: xiāng  # 厢\nU+53A3: yǎn  # 厣\nU+53A5: jué  # 厥\nU+53A6: shà  # 厦\nU+53A8: chú  # 厨\nU+53A9: jiù  # 厩\nU+53AE: sī  # 厮\nU+53BB: qù  # 去\nU+53BE: dū  # 厾\nU+53BF: xiàn  # 县\nU+53C1: sān  # 叁\nU+53C2: cān  # 参\nU+53C6: ài  # 叆\nU+53C7: dài  # 叇\nU+53C8: yòu  # 又\nU+53C9: chā  # 叉\nU+53CA: jí  # 及\nU+53CB: yǒu  # 友\nU+53CC: shuāng  # 双\nU+53CD: fǎn  # 反\nU+53D1: fā  # 发\nU+53D4: shū  # 叔\nU+53D5: zhuó  # 叕\nU+53D6: qǔ  # 取\nU+53D7: shòu  # 受\nU+53D8: biàn  # 变\nU+53D9: xù  # 叙\nU+53DA: jiǎ  # 叚\nU+53DB: pàn  # 叛\nU+53DF: sǒu  # 叟\nU+53E0: dié  # 叠\nU+53E3: kǒu  # 口\nU+53E4: gǔ  # 古\nU+53E5: jù  # 句\nU+53E6: lìng  # 另\nU+53E8: dāo  # 叨 -> tāo\nU+53E9: kòu  # 叩\nU+53EA: zhǐ  # 只 -> zhī\nU+53EB: jiào  # 叫\nU+53EC: zhào  # 召\nU+53ED: bā  # 叭\nU+53EE: dīng  # 叮\nU+53EF: kě  # 可\nU+53F0: tái  # 台\nU+53F1: chì  # 叱\nU+53F2: shǐ  # 史\nU+53F3: yòu  # 右\nU+53F5: pǒ  # 叵\nU+53F6: yè  # 叶\nU+53F7: hào  # 号\nU+53F8: sī  # 司\nU+53F9: tàn  # 叹\nU+53FB: lè  # 叻\nU+53FC: diāo  # 叼\nU+53FD: jī  # 叽\nU+5401: xū  # 吁\nU+5403: chī  # 吃\nU+5404: gè  # 各\nU+5406: yāo  # 吆\nU+5408: hé  # 合\nU+5409: jí  # 吉\nU+540A: diào  # 吊\nU+540C: tóng  # 同\nU+540D: míng  # 名\nU+540E: hòu  # 后\nU+540F: lì  # 吏\nU+5410: tǔ  # 吐\nU+5411: xiàng  # 向\nU+5412: zhā  # 吒 -> zhà\nU+5413: xià  # 吓\nU+5415: lǚ  # 吕\nU+5416: yā  # 吖\nU+5417: ma  # 吗 -> má\nU+541B: jūn  # 君\nU+541D: lìn  # 吝\nU+541E: tūn  # 吞\nU+541F: yín  # 吟\nU+5420: fèi  # 吠\nU+5421: bǐ  # 吡 -> pǐ\nU+5423: qìn  # 吣\nU+5426: fǒu  # 否\nU+5427: ba  # 吧 -> bā\nU+5428: dūn  # 吨\nU+5429: fēn  # 吩\nU+542B: hán  # 含\nU+542C: tīng  # 听\nU+542D: kēng  # 吭 -> háng\nU+542E: shǔn  # 吮\nU+542F: qǐ  # 启\nU+5431: zhī  # 吱\nU+5432: yǐn  # 吲\nU+5434: wú  # 吴\nU+5435: chǎo  # 吵\nU+5438: xī  # 吸\nU+5439: chuī  # 吹\nU+543B: wěn  # 吻\nU+543C: hǒu  # 吼\nU+543D: hōng  # 吽 -> hǒu\nU+543E: wú  # 吾\nU+5440: ya  # 呀 -> yā\nU+5443: è  # 呃\nU+5446: dāi  # 呆\nU+5447: qǐ  # 呇\nU+5448: chéng  # 呈\nU+544A: gào  # 告\nU+544B: fū  # 呋\nU+5450: nà  # 呐\nU+5452: wǔ  # 呒 -> fǔ\nU+5453: yì  # 呓\nU+5454: dāi  # 呔\nU+5455: ǒu  # 呕\nU+5456: lì  # 呖\nU+5457: bei  # 呗\nU+5458: yuán  # 员\nU+5459: guō  # 呙 -> wāi\nU+545B: qiāng  # 呛\nU+545C: wū  # 呜\nU+5462: ne  # 呢\nU+5463: ḿ  # 呣\nU+5464: lìng  # 呤 -> líng\nU+5466: yōu  # 呦\nU+5468: zhōu  # 周\nU+5471: gū  # 呱 -> guā\nU+5472: cī  # 呲 -> zī\nU+5473: wèi  # 味\nU+5475: hē  # 呵\nU+5476: náo  # 呶\nU+5477: gā  # 呷 -> xiā\nU+5478: pēi  # 呸\nU+547B: shēn  # 呻\nU+547C: hū  # 呼\nU+547D: mìng  # 命\nU+5480: jǔ  # 咀\nU+5482: zā  # 咂\nU+5484: duō  # 咄\nU+5486: páo  # 咆\nU+5487: bié  # 咇 -> bì\nU+5489: yāng  # 咉 -> yǎng\nU+548B: zǎ  # 咋\nU+548C: hé  # 和\nU+548D: hāi  # 咍\nU+548E: jiù  # 咎\nU+548F: yǒng  # 咏\nU+5490: fù  # 咐\nU+5492: zhòu  # 咒\nU+5494: kā  # 咔 -> kǎ\nU+5495: gū  # 咕\nU+5496: kā  # 咖\nU+5499: lóng  # 咙\nU+549A: dōng  # 咚\nU+549B: níng  # 咛\nU+549D: sī  # 咝\nU+54A1: èr  # 咡\nU+54A3: guāng  # 咣\nU+54A4: zhà  # 咤\nU+54A5: xì  # 咥 -> dié\nU+54A6: yí  # 咦\nU+54A7: liě  # 咧\nU+54A8: zī  # 咨\nU+54A9: miē  # 咩\nU+54AA: mī  # 咪\nU+54AB: zhǐ  # 咫\nU+54AC: yǎo  # 咬\nU+54AF: gē  # 咯 -> lo\nU+54B1: zán  # 咱\nU+54B3: ké  # 咳\nU+54B4: huī  # 咴\nU+54B8: xián  # 咸\nU+54BA: xuǎn  # 咺\nU+54BB: xiū  # 咻\nU+54BD: yàn  # 咽 -> yān\nU+54BF: yī  # 咿\nU+54C0: āi  # 哀\nU+54C1: pǐn  # 品\nU+54C2: shěn  # 哂\nU+54C3: tóng  # 哃\nU+54C4: hǒng  # 哄\nU+54C6: duō  # 哆\nU+54C7: wa  # 哇 -> wā\nU+54C8: hā  # 哈\nU+54C9: zāi  # 哉\nU+54CC: pài  # 哌\nU+54CD: xiǎng  # 响\nU+54CE: āi  # 哎\nU+54CF: gén  # 哏\nU+54D0: kuāng  # 哐\nU+54D1: yǎ  # 哑\nU+54D2: dá  # 哒 -> dā\nU+54D3: xiāo  # 哓\nU+54D4: bì  # 哔\nU+54D5: huì  # 哕 -> yuě\nU+54D7: huā  # 哗 -> huá\nU+54D9: kuài  # 哙\nU+54DA: duǒ  # 哚\nU+54DD: nóng  # 哝\nU+54DE: mōu  # 哞\nU+54DF: yō  # 哟\nU+54E2: lòng  # 哢\nU+54E5: gē  # 哥\nU+54E6: ó  # 哦\nU+54E7: chī  # 哧\nU+54E8: shào  # 哨\nU+54E9: lī  # 哩 -> li\nU+54EA: nǎ  # 哪\nU+54ED: kū  # 哭\nU+54EE: xiāo  # 哮 -> xiào\nU+54F1: bō  # 哱 -> pò\nU+54F2: zhé  # 哲\nU+54F3: zhā  # 哳\nU+54FA: bǔ  # 哺\nU+54FC: hēng  # 哼\nU+54FD: gěng  # 哽\nU+54FF: gě  # 哿\nU+5501: yàn  # 唁\nU+5506: suō  # 唆\nU+5507: chún  # 唇\nU+5509: āi  # 唉\nU+550F: xī  # 唏\nU+5510: táng  # 唐\nU+5511: zuò  # 唑\nU+5514: wú  # 唔 -> wù\nU+551B: mà  # 唛\nU+551D: gòng  # 唝\nU+5520: láo  # 唠 -> lào\nU+5522: suǒ  # 唢\nU+5523: zào  # 唣\nU+5524: huàn  # 唤\nU+5527: jī  # 唧\nU+552A: fěng  # 唪\nU+552C: hǔ  # 唬\nU+552E: shòu  # 售\nU+552F: wéi  # 唯\nU+5530: shuā  # 唰\nU+5531: chàng  # 唱\nU+5533: lì  # 唳\nU+5535: ǎn  # 唵\nU+5537: yō  # 唷\nU+553C: shà  # 唼\nU+553E: tuò  # 唾\nU+553F: hū  # 唿\nU+5541: zhāo  # 啁 -> zhōu\nU+5543: kěn  # 啃\nU+5544: zhuó  # 啄\nU+5546: shāng  # 商\nU+5549: lín  # 啉 -> lán\nU+554A: a  # 啊 -> ā\nU+5550: cuì  # 啐\nU+5555: táo  # 啕\nU+5556: dàn  # 啖\nU+555C: chuài  # 啜 -> chuò\nU+5561: fēi  # 啡\nU+5564: pí  # 啤\nU+5565: shá  # 啥\nU+5566: la  # 啦 -> lā\nU+5567: zé  # 啧\nU+556A: pā  # 啪\nU+556B: zhě  # 啫\nU+556C: sè  # 啬\nU+556D: zhuàn  # 啭\nU+556E: niè  # 啮\nU+5570: luō  # 啰\nU+5574: chǎn  # 啴 -> tān\nU+5575: bō  # 啵 -> bo\nU+5576: dìng  # 啶\nU+5577: lāng  # 啷\nU+5578: xiào  # 啸\nU+557B: chì  # 啻\nU+557C: tí  # 啼\nU+557E: jiū  # 啾\nU+5580: kā  # 喀\nU+5581: yóng  # 喁\nU+5582: wèi  # 喂\nU+5583: nán  # 喃\nU+5584: shàn  # 善\nU+5586: zhé  # 喆\nU+5587: lǎ  # 喇\nU+5588: jiē  # 喈\nU+5589: hóu  # 喉\nU+558A: hǎn  # 喊\nU+558B: dié  # 喋\nU+558F: nuò  # 喏\nU+5591: yīn  # 喑\nU+5594: ō  # 喔\nU+5598: chuǎn  # 喘\nU+5599: huì  # 喙\nU+559C: xǐ  # 喜\nU+559D: hē  # 喝\nU+559F: kuì  # 喟\nU+55A4: huáng  # 喤\nU+55A7: xuān  # 喧\nU+55B1: lí  # 喱\nU+55B3: zhā  # 喳\nU+55B5: miāo  # 喵\nU+55B7: pēn  # 喷\nU+55B9: kuí  # 喹\nU+55BB: yù  # 喻\nU+55BD: lóu  # 喽\nU+55BE: kù  # 喾\nU+55C4: á  # 嗄 -> shà\nU+55C5: xiù  # 嗅\nU+55C9: sù  # 嗉\nU+55CC: ài  # 嗌 -> yì\nU+55CD: suō  # 嗍\nU+55D0: hài  # 嗐\nU+55D1: kē  # 嗑\nU+55D2: dā  # 嗒 -> tà\nU+55D3: sǎng  # 嗓\nU+55D4: chēn  # 嗔\nU+55D6: sōu  # 嗖\nU+55DC: shì  # 嗜\nU+55DD: gé  # 嗝\nU+55DE: zī  # 嗞\nU+55DF: jiē  # 嗟\nU+55E1: wēng  # 嗡\nU+55E3: sì  # 嗣\nU+55E4: chī  # 嗤\nU+55E5: háo  # 嗥\nU+55E6: suo  # 嗦 -> suō\nU+55E8: hāi  # 嗨\nU+55EA: qín  # 嗪\nU+55EB: niè  # 嗫\nU+55EC: hē  # 嗬\nU+55EF: ń  # 嗯 -> ǹg\nU+55F2: diē  # 嗲 -> diǎ\nU+55F3: āi  # 嗳 -> ǎi\nU+55F5: tōng  # 嗵\nU+55F7: áo  # 嗷\nU+55FD: sòu  # 嗽\nU+55FE: sǒu  # 嗾\nU+5600: dí  # 嘀\nU+5601: qī  # 嘁\nU+5608: cáo  # 嘈\nU+5609: jiā  # 嘉\nU+560C: piào  # 嘌 -> piāo\nU+560E: gā  # 嘎\nU+560F: gǔ  # 嘏\nU+5618: xū  # 嘘\nU+561A: dē  # 嘚\nU+561B: ma  # 嘛 -> má\nU+561E: lei  # 嘞\nU+561F: dū  # 嘟\nU+5621: tāng  # 嘡\nU+5623: bēng  # 嘣\nU+5624: yīng  # 嘤\nU+5627: mì  # 嘧\nU+562C: chuài  # 嘬 -> zuō\nU+562D: pēng  # 嘭\nU+5631: zhǔ  # 嘱\nU+5632: cháo  # 嘲\nU+5634: zuǐ  # 嘴\nU+5636: sī  # 嘶\nU+5639: liáo  # 嘹\nU+563B: xī  # 嘻\nU+563F: hēi  # 嘿\nU+5640: xùn  # 噀\nU+5642: zǔn  # 噂 -> zūn\nU+5647: chuáng  # 噇\nU+564C: cēng  # 噌\nU+564D: jiào  # 噍\nU+564E: yē  # 噎\nU+5654: dēng  # 噔\nU+5657: pū  # 噗\nU+5658: juē  # 噘\nU+5659: qín  # 噙\nU+565C: lū  # 噜\nU+5662: ō  # 噢\nU+5664: jìn  # 噤\nU+5668: qì  # 器\nU+5669: è  # 噩\nU+566A: zào  # 噪\nU+566B: yī  # 噫\nU+566C: shì  # 噬\nU+5671: jué  # 噱\nU+5676: gá  # 噶\nU+567B: sāi  # 噻\nU+567C: pī  # 噼\nU+5684: huō  # 嚄 -> huò\nU+5685: rú  # 嚅\nU+5686: hāo  # 嚆\nU+568E: háo  # 嚎\nU+568F: tì  # 嚏\nU+5693: cā  # 嚓\nU+569A: yín  # 嚚\nU+56A3: xiāo  # 嚣\nU+56AD: pǐ  # 嚭\nU+56AF: huò  # 嚯\nU+56B7: rǎng  # 嚷\nU+56BC: jué  # 嚼 -> jiáo\nU+56CA: náng  # 囊\nU+56D4: nāng  # 囔\nU+56DA: qiú  # 囚\nU+56DB: sì  # 四\nU+56DE: huí  # 回\nU+56DF: xìn  # 囟\nU+56E0: yīn  # 因\nU+56E1: nān  # 囡\nU+56E2: tuán  # 团\nU+56E4: dùn  # 囤\nU+56EB: hú  # 囫\nU+56ED: yuán  # 园\nU+56F0: kùn  # 困\nU+56F1: cōng  # 囱\nU+56F4: wéi  # 围\nU+56F5: lún  # 囵\nU+56F7: qūn  # 囷\nU+56F9: líng  # 囹\nU+56FA: gù  # 固\nU+56FD: guó  # 国\nU+56FE: tú  # 图\nU+56FF: yòu  # 囿\nU+5703: pǔ  # 圃\nU+5704: yǔ  # 圄\nU+5706: yuán  # 圆\nU+5708: quān  # 圈\nU+5709: yǔ  # 圉\nU+570A: qīng  # 圊\nU+570C: chuán  # 圌\nU+5710: kū  # 圐\nU+5719: lüè  # 圙\nU+571C: huán  # 圜\nU+571F: tǔ  # 土\nU+5722: tǐng  # 圢\nU+5723: shèng  # 圣\nU+5728: zài  # 在\nU+5729: wéi  # 圩\nU+572A: gē  # 圪\nU+572B: yù  # 圫\nU+572C: wū  # 圬\nU+572D: guī  # 圭\nU+572E: pǐ  # 圮\nU+572F: yí  # 圯\nU+5730: dì  # 地\nU+5732: qiān  # 圲\nU+5733: zhèn  # 圳\nU+5739: kuàng  # 圹\nU+573A: chǎng  # 场 -> cháng\nU+573B: qí  # 圻\nU+573E: jī  # 圾\nU+5740: zhǐ  # 址\nU+5742: bǎn  # 坂\nU+5747: jūn  # 均\nU+5749: tún  # 坉\nU+574A: fāng  # 坊\nU+574B: bèn  # 坋\nU+574C: bèn  # 坌\nU+574D: tān  # 坍\nU+574E: kǎn  # 坎\nU+574F: huài  # 坏\nU+5750: zuò  # 坐\nU+5751: kēng  # 坑\nU+5752: bì  # 坒\nU+5757: kuài  # 块\nU+575A: jiān  # 坚\nU+575B: tán  # 坛\nU+575C: lì  # 坜\nU+575D: bà  # 坝\nU+575E: wù  # 坞\nU+575F: fén  # 坟\nU+5760: zhuì  # 坠\nU+5761: pō  # 坡\nU+5764: kūn  # 坤\nU+5765: qū  # 坥\nU+5766: tǎn  # 坦\nU+5768: tuó  # 坨\nU+5769: gān  # 坩\nU+576A: píng  # 坪\nU+576B: diàn  # 坫\nU+576C: guà  # 坬\nU+576D: ní  # 坭\nU+576F: pī  # 坯\nU+5770: jiōng  # 坰\nU+5773: ào  # 坳\nU+5777: kě  # 坷 -> kē\nU+577B: chí  # 坻 -> dǐ\nU+577C: chè  # 坼\nU+577D: líng  # 坽\nU+5782: chuí  # 垂\nU+5783: lā  # 垃\nU+5784: lǒng  # 垄\nU+5786: lú  # 垆\nU+5788: dài  # 垈\nU+578B: xíng  # 型\nU+578C: dòng  # 垌\nU+578D: jì  # 垍\nU+578E: hè  # 垎\nU+578F: lǜ  # 垏\nU+5792: lěi  # 垒\nU+5793: gāi  # 垓\nU+5795: hòu  # 垕\nU+5799: guāng  # 垙\nU+579A: yáo  # 垚\nU+579B: duǒ  # 垛\nU+579E: chá  # 垞\nU+579F: yáng  # 垟\nU+57A0: yín  # 垠\nU+57A1: fá  # 垡\nU+57A2: gòu  # 垢\nU+57A3: yuán  # 垣\nU+57A4: dié  # 垤\nU+57A6: kěn  # 垦\nU+57A7: shǎng  # 垧\nU+57A9: è  # 垩\nU+57AB: diàn  # 垫\nU+57AD: yā  # 垭\nU+57AE: kuǎ  # 垮\nU+57AF: da  # 垯 -> dá\nU+57B1: dàng  # 垱\nU+57B2: kǎi  # 垲\nU+57B4: nǎo  # 垴\nU+57B5: ǎn  # 垵\nU+57B8: yuàn  # 垸\nU+57BA: fū  # 垺 -> póu\nU+57BE: hàn  # 垾\nU+57BF: xù  # 垿\nU+57C2: gěng  # 埂\nU+57C3: āi  # 埃\nU+57C6: què  # 埆\nU+57C7: yǒng  # 埇\nU+57CB: mái  # 埋\nU+57CC: làng  # 埌\nU+57CE: chéng  # 城\nU+57CF: shān  # 埏 -> yán\nU+57D2: liè  # 埒\nU+57D4: pǔ  # 埔\nU+57D5: chéng  # 埕\nU+57D7: bù  # 埗\nU+57D8: shí  # 埘\nU+57D9: xūn  # 埙\nU+57DA: guō  # 埚\nU+57DD: niàn  # 埝\nU+57DF: yù  # 域\nU+57E0: bù  # 埠\nU+57E4: pí  # 埤\nU+57EA: kōng  # 埪\nU+57EB: chǒng  # 埫\nU+57ED: dài  # 埭\nU+57EF: ǎn  # 埯\nU+57F4: zhí  # 埴\nU+57F5: duǒ  # 埵\nU+57F8: yì  # 埸\nU+57F9: péi  # 培\nU+57FA: jī  # 基\nU+57FC: qí  # 埼\nU+57FD: sào  # 埽\nU+5802: táng  # 堂\nU+5803: kūn  # 堃\nU+5806: duī  # 堆\nU+5807: jǐn  # 堇 -> jīn\nU+5809: yù  # 堉\nU+580B: péng  # 堋\nU+580C: gù  # 堌\nU+580D: tù  # 堍\nU+580E: lèng  # 堎\nU+5810: yá  # 堐\nU+5811: qiàn  # 堑\nU+5815: duò  # 堕\nU+5819: yīn  # 堙\nU+581E: dié  # 堞\nU+5820: hòu  # 堠\nU+5821: bǎo  # 堡\nU+5824: dī  # 堤\nU+5827: ruán  # 堧\nU+5828: yè  # 堨 -> è\nU+582A: kān  # 堪\nU+5830: yàn  # 堰\nU+5832: cí  # 堲 -> jí\nU+5835: dǔ  # 堵\nU+583C: hèng  # 堼 -> fēng\nU+583D: gāng  # 堽\nU+583E: chūn  # 堾 -> chuǎn\nU+5844: léng  # 塄\nU+5845: duàn  # 塅\nU+5846: wān  # 塆\nU+584C: tā  # 塌\nU+584D: chéng  # 塍\nU+5851: sù  # 塑\nU+5854: tǎ  # 塔\nU+5858: táng  # 塘\nU+585D: bàng  # 塝\nU+585E: sāi  # 塞\nU+5865: gé  # 塥\nU+586B: tián  # 填\nU+586C: yuán  # 塬\nU+5871: lǎng  # 塱\nU+587E: shú  # 塾\nU+5880: chí  # 墀\nU+5881: màn  # 墁\nU+5883: jìng  # 境\nU+5885: shù  # 墅\nU+5888: kàn  # 墈\nU+5889: yōng  # 墉\nU+5890: jìn  # 墐\nU+5892: shāng  # 墒\nU+5893: mù  # 墓\nU+5895: yàn  # 墕\nU+5898: qián  # 墘\nU+5899: qiáng  # 墙\nU+589A: liáng  # 墚\nU+589E: zēng  # 增\nU+589F: xū  # 墟\nU+58A1: shàn  # 墡\nU+58A3: pú  # 墣\nU+58A6: fán  # 墦\nU+58A8: mò  # 墨\nU+58A9: dūn  # 墩\nU+58BC: jī  # 墼\nU+58C1: bì  # 壁\nU+58C5: yōng  # 壅\nU+58D1: hè  # 壑\nU+58D5: háo  # 壕\nU+58E4: rǎng  # 壤\nU+58EB: shì  # 士\nU+58EC: rén  # 壬\nU+58EE: zhuàng  # 壮\nU+58F0: shēng  # 声\nU+58F3: ké  # 壳\nU+58F6: hú  # 壶\nU+58F8: kǔn  # 壸\nU+58F9: yī  # 壹\nU+5904: chù  # 处 -> chǔ\nU+5907: bèi  # 备\nU+590D: fù  # 复\nU+590F: xià  # 夏\nU+5910: xiòng  # 夐\nU+5914: kuí  # 夔\nU+5915: xī  # 夕\nU+5916: wài  # 外\nU+5919: sù  # 夙\nU+591A: duō  # 多\nU+591C: yè  # 夜\nU+591F: gòu  # 够\nU+5924: yín  # 夤\nU+5925: huǒ  # 夥\nU+5927: dà  # 大\nU+5929: tiān  # 天\nU+592A: tài  # 太\nU+592B: fū  # 夫\nU+592C: guài  # 夬\nU+592D: yāo  # 夭\nU+592E: yāng  # 央\nU+592F: hāng  # 夯\nU+5931: shī  # 失\nU+5934: tóu  # 头\nU+5937: yí  # 夷\nU+5938: kuā  # 夸\nU+5939: jiā  # 夹\nU+593A: duó  # 夺\nU+593C: kuǎng  # 夼\nU+5941: lián  # 奁\nU+5942: huàn  # 奂\nU+5944: yǎn  # 奄\nU+5947: qí  # 奇\nU+5948: nài  # 奈\nU+5949: fèng  # 奉\nU+594B: fèn  # 奋\nU+594E: kuí  # 奎\nU+594F: zòu  # 奏\nU+5951: qì  # 契\nU+5953: zhā  # 奓 -> shē\nU+5954: bēn  # 奔\nU+5955: yì  # 奕\nU+5956: jiǎng  # 奖\nU+5957: tào  # 套\nU+5958: zàng  # 奘\nU+595A: xī  # 奚\nU+5960: diàn  # 奠\nU+5961: ào  # 奡\nU+5962: shē  # 奢\nU+5965: ào  # 奥\nU+596D: shì  # 奭\nU+5973: nǚ  # 女\nU+5974: nú  # 奴\nU+5976: nǎi  # 奶\nU+5978: jiān  # 奸\nU+5979: tā  # 她\nU+597D: hǎo  # 好\nU+5981: shuò  # 妁\nU+5982: rú  # 如\nU+5983: fēi  # 妃\nU+5984: wàng  # 妄\nU+5986: zhuāng  # 妆\nU+5987: fù  # 妇\nU+5988: mā  # 妈\nU+598A: rèn  # 妊\nU+598D: yán  # 妍\nU+5992: dù  # 妒\nU+5993: jì  # 妓\nU+5996: yāo  # 妖\nU+5997: jìn  # 妗\nU+5998: yún  # 妘\nU+5999: miào  # 妙\nU+599E: niū  # 妞\nU+59A3: bǐ  # 妣\nU+59A4: yú  # 妤\nU+59A5: tuǒ  # 妥\nU+59A7: wàn  # 妧\nU+59A8: fáng  # 妨\nU+59A9: wǔ  # 妩\nU+59AA: yù  # 妪\nU+59AB: guī  # 妫\nU+59AD: bá  # 妭\nU+59AE: nī  # 妮\nU+59AF: zhóu  # 妯\nU+59B2: dá  # 妲\nU+59B9: mèi  # 妹\nU+59BB: qī  # 妻\nU+59BE: qiè  # 妾\nU+59C6: mǔ  # 姆\nU+59C8: líng  # 姈\nU+59CA: zǐ  # 姊\nU+59CB: shǐ  # 始\nU+59D0: jiě  # 姐\nU+59D1: gū  # 姑\nU+59D2: sì  # 姒\nU+59D3: xìng  # 姓\nU+59D4: wěi  # 委\nU+59D7: shān  # 姗\nU+59D8: pīn  # 姘\nU+59DA: yáo  # 姚\nU+59DC: jiāng  # 姜\nU+59DD: shū  # 姝\nU+59DE: jí  # 姞\nU+59E3: jiāo  # 姣\nU+59E4: gòu  # 姤\nU+59E5: lǎo  # 姥 -> mǔ\nU+59E8: yí  # 姨\nU+59EC: jī  # 姬\nU+59EE: héng  # 姮\nU+59F1: kuā  # 姱\nU+59F6: è  # 姶\nU+59F9: chà  # 姹\nU+59FB: yīn  # 姻\nU+59FD: guǐ  # 姽\nU+59FF: zī  # 姿\nU+5A00: sōng  # 娀\nU+5A01: wēi  # 威\nU+5A03: wá  # 娃\nU+5A04: lóu  # 娄\nU+5A05: yà  # 娅\nU+5A06: ráo  # 娆\nU+5A07: jiāo  # 娇\nU+5A08: luán  # 娈\nU+5A09: pīng  # 娉\nU+5A0C: lǐ  # 娌\nU+5A11: suō  # 娑\nU+5A13: wěi  # 娓\nU+5A18: niáng  # 娘\nU+5A1C: nà  # 娜\nU+5A1F: juān  # 娟\nU+5A20: shēn  # 娠\nU+5A23: dì  # 娣\nU+5A25: é  # 娥\nU+5A29: miǎn  # 娩\nU+5A31: yú  # 娱\nU+5A32: wā  # 娲\nU+5A34: xián  # 娴\nU+5A35: jū  # 娵\nU+5A36: qǔ  # 娶\nU+5A3C: chāng  # 娼\nU+5A40: ē  # 婀\nU+5A46: pó  # 婆\nU+5A49: wǎn  # 婉\nU+5A4A: biǎo  # 婊\nU+5A4C: shú  # 婌 -> shū\nU+5A4D: qǐ  # 婍\nU+5A55: jié  # 婕\nU+5A58: quán  # 婘\nU+5A5A: hūn  # 婚\nU+5A5E: xìng  # 婞\nU+5A60: wān  # 婠\nU+5A62: bì  # 婢\nU+5A64: chōu  # 婤 -> zhōu\nU+5A67: jìng  # 婧\nU+5A6A: lán  # 婪\nU+5A6B: kūn  # 婫 -> hùn\nU+5A73: huà  # 婳\nU+5A74: yīng  # 婴\nU+5A75: chán  # 婵\nU+5A76: shěn  # 婶\nU+5A77: tíng  # 婷\nU+5A7A: wù  # 婺\nU+5A7B: nàn  # 婻\nU+5A7C: chuò  # 婼 -> ruò\nU+5A7F: xù  # 婿\nU+5A82: dì  # 媂\nU+5A84: měi  # 媄\nU+5A86: ruǎn  # 媆\nU+5A92: méi  # 媒\nU+5A93: huáng  # 媓\nU+5A96: yīng  # 媖\nU+5A9A: mèi  # 媚\nU+5A9B: yuàn  # 媛\nU+5A9E: shì  # 媞 -> tí\nU+5AAA: ǎo  # 媪\nU+5AAD: xū  # 媭\nU+5AB1: yáo  # 媱\nU+5AB2: pì  # 媲\nU+5AB3: xí  # 媳\nU+5AB5: yìng  # 媵\nU+5AB8: chī  # 媸\nU+5ABE: gòu  # 媾\nU+5AC1: jià  # 嫁\nU+5AC2: sǎo  # 嫂\nU+5AC4: yuán  # 嫄\nU+5AC9: jí  # 嫉\nU+5ACC: xián  # 嫌\nU+5AD2: ài  # 嫒\nU+5AD4: pín  # 嫔\nU+5AD5: yì  # 嫕\nU+5AD6: piáo  # 嫖\nU+5AD8: léi  # 嫘\nU+5ADA: mān  # 嫚 -> màn\nU+5ADC: zhāng  # 嫜\nU+5AE0: lí  # 嫠\nU+5AE1: dí  # 嫡\nU+5AE3: yān  # 嫣\nU+5AE6: cháng  # 嫦\nU+5AE9: nèn  # 嫩\nU+5AEA: lào  # 嫪\nU+5AEB: mó  # 嫫\nU+5AED: hù  # 嫭\nU+5AF1: qiáng  # 嫱\nU+5AFD: liáo  # 嫽\nU+5B09: xī  # 嬉\nU+5B16: bì  # 嬖\nU+5B17: shàn  # 嬗\nU+5B1B: huán  # 嬛\nU+5B25: tiǎo  # 嬥\nU+5B2C: rú  # 嬬\nU+5B34: yíng  # 嬴\nU+5B37: mā  # 嬷 -> mó\nU+5B3F: yàn  # 嬿\nU+5B40: shuāng  # 孀\nU+5B45: qiān  # 孅 -> xiān\nU+5B50: zi  # 子 -> zǐ\nU+5B51: jié  # 孑\nU+5B53: jué  # 孓\nU+5B54: kǒng  # 孔\nU+5B55: yùn  # 孕\nU+5B56: mā  # 孖 -> zī\nU+5B57: zì  # 字\nU+5B58: cún  # 存\nU+5B59: sūn  # 孙\nU+5B5A: fú  # 孚\nU+5B5B: bèi  # 孛\nU+5B5C: zī  # 孜\nU+5B5D: xiào  # 孝\nU+5B5F: mèng  # 孟\nU+5B62: bāo  # 孢\nU+5B63: jì  # 季\nU+5B64: gū  # 孤\nU+5B65: nú  # 孥\nU+5B66: xué  # 学\nU+5B69: hái  # 孩\nU+5B6A: luán  # 孪\nU+5B6C: nāo  # 孬\nU+5B70: shú  # 孰\nU+5B71: càn  # 孱 -> chán\nU+5B73: zī  # 孳\nU+5B75: fū  # 孵\nU+5B7A: rú  # 孺\nU+5B7D: niè  # 孽\nU+5B81: níng  # 宁\nU+5B83: tā  # 它\nU+5B84: guǐ  # 宄\nU+5B85: zhái  # 宅\nU+5B87: yǔ  # 宇\nU+5B88: shǒu  # 守\nU+5B89: ān  # 安\nU+5B8B: sòng  # 宋\nU+5B8C: wán  # 完\nU+5B8F: hóng  # 宏\nU+5B93: mì  # 宓\nU+5B95: dàng  # 宕\nU+5B97: zōng  # 宗\nU+5B98: guān  # 官\nU+5B99: zhòu  # 宙\nU+5B9A: dìng  # 定\nU+5B9B: wǎn  # 宛\nU+5B9C: yí  # 宜\nU+5B9D: bǎo  # 宝\nU+5B9E: shí  # 实\nU+5BA0: chǒng  # 宠\nU+5BA1: shěn  # 审\nU+5BA2: kè  # 客\nU+5BA3: xuān  # 宣\nU+5BA4: shì  # 室\nU+5BA5: yòu  # 宥\nU+5BA6: huàn  # 宦\nU+5BA7: yí  # 宧\nU+5BAA: xiàn  # 宪\nU+5BAB: gōng  # 宫\nU+5BAC: chéng  # 宬\nU+5BB0: zǎi  # 宰\nU+5BB3: hài  # 害\nU+5BB4: yàn  # 宴\nU+5BB5: xiāo  # 宵\nU+5BB6: jiā  # 家\nU+5BB8: chén  # 宸\nU+5BB9: róng  # 容\nU+5BBD: kuān  # 宽\nU+5BBE: bīn  # 宾\nU+5BBF: sù  # 宿\nU+5BC1: zǎn  # 寁\nU+5BC2: jì  # 寂\nU+5BC4: jì  # 寄\nU+5BC5: yín  # 寅\nU+5BC6: mì  # 密\nU+5BC7: kòu  # 寇\nU+5BCC: fù  # 富\nU+5BD0: mèi  # 寐\nU+5BD2: hán  # 寒\nU+5BD3: yù  # 寓\nU+5BDD: qǐn  # 寝\nU+5BDE: mò  # 寞\nU+5BDF: chá  # 察\nU+5BE1: guǎ  # 寡\nU+5BE4: wù  # 寤\nU+5BE5: liáo  # 寥\nU+5BE8: zhài  # 寨\nU+5BEE: liáo  # 寮\nU+5BF0: huán  # 寰\nU+5BF8: cùn  # 寸\nU+5BF9: duì  # 对\nU+5BFA: sì  # 寺\nU+5BFB: xún  # 寻\nU+5BFC: dǎo  # 导\nU+5BFF: shòu  # 寿\nU+5C01: fēng  # 封\nU+5C04: shè  # 射\nU+5C06: jiāng  # 将\nU+5C09: wèi  # 尉\nU+5C0A: zūn  # 尊\nU+5C0F: xiǎo  # 小\nU+5C11: shǎo  # 少\nU+5C14: ěr  # 尔\nU+5C15: gǎ  # 尕\nU+5C16: jiān  # 尖\nU+5C18: chén  # 尘\nU+5C1A: shàng  # 尚\nU+5C1C: gá  # 尜\nU+5C1D: cháng  # 尝\nU+5C22: yóu  # 尢\nU+5C24: yóu  # 尤\nU+5C25: liào  # 尥\nU+5C27: yáo  # 尧\nU+5C28: máng  # 尨 -> lóng\nU+5C2A: wāng  # 尪\nU+5C2C: gà  # 尬\nU+5C31: jiù  # 就\nU+5C34: gān  # 尴\nU+5C38: shī  # 尸\nU+5C39: yǐn  # 尹\nU+5C3A: chǐ  # 尺\nU+5C3B: kāo  # 尻\nU+5C3C: ní  # 尼\nU+5C3D: jǐn  # 尽 -> jìn\nU+5C3E: wěi  # 尾\nU+5C3F: niào  # 尿\nU+5C40: jú  # 局\nU+5C41: pì  # 屁\nU+5C42: céng  # 层\nU+5C43: xì  # 屃\nU+5C45: jū  # 居\nU+5C48: qū  # 屈\nU+5C49: tì  # 屉\nU+5C4A: jiè  # 届\nU+5C4B: wū  # 屋\nU+5C4E: shǐ  # 屎\nU+5C4F: píng  # 屏\nU+5C50: jī  # 屐\nU+5C51: xiè  # 屑\nU+5C55: zhǎn  # 展\nU+5C59: ē  # 屙\nU+5C5E: shǔ  # 属\nU+5C60: tú  # 屠\nU+5C61: lǚ  # 屡\nU+5C63: xǐ  # 屣\nU+5C65: lǚ  # 履\nU+5C66: jù  # 屦\nU+5C6F: tún  # 屯\nU+5C71: shān  # 山\nU+5C79: yì  # 屹\nU+5C7A: qǐ  # 屺\nU+5C7C: wù  # 屼\nU+5C7E: shēn  # 屾\nU+5C7F: yǔ  # 屿\nU+5C81: suì  # 岁\nU+5C82: qǐ  # 岂\nU+5C88: yá  # 岈\nU+5C8A: jié  # 岊\nU+5C8C: jí  # 岌\nU+5C8D: qiān  # 岍\nU+5C90: qí  # 岐\nU+5C91: cén  # 岑\nU+5C94: chà  # 岔\nU+5C96: qū  # 岖\nU+5C97: gǎng  # 岗\nU+5C98: xiàn  # 岘\nU+5C99: ào  # 岙\nU+5C9A: lán  # 岚\nU+5C9B: dǎo  # 岛\nU+5C9C: bā  # 岜\nU+5C9E: zuò  # 岞\nU+5CA0: jù  # 岠\nU+5CA2: kě  # 岢\nU+5CA3: gǒu  # 岣\nU+5CA8: qū  # 岨 -> jū\nU+5CA9: yán  # 岩\nU+5CAB: xiù  # 岫\nU+5CAC: jiǎ  # 岬\nU+5CAD: lǐng  # 岭\nU+5CB1: dài  # 岱\nU+5CB3: yuè  # 岳\nU+5CB5: hù  # 岵\nU+5CB7: mín  # 岷\nU+5CB8: àn  # 岸\nU+5CBD: dōng  # 岽\nU+5CBF: kuī  # 岿\nU+5CC1: mǎo  # 峁\nU+5CC2: tóng  # 峂\nU+5CC3: xué  # 峃\nU+5CC4: yì  # 峄\nU+5CCB: xún  # 峋\nU+5CD2: dòng  # 峒 -> tóng\nU+5CD7: wéi  # 峗\nU+5CD8: huán  # 峘\nU+5CD9: zhì  # 峙\nU+5CDB: lǐ  # 峛\nU+5CE1: xiá  # 峡\nU+5CE3: yáo  # 峣\nU+5CE4: jiào  # 峤\nU+5CE5: zhēng  # 峥\nU+5CE6: luán  # 峦\nU+5CE7: jiāo  # 峧\nU+5CE8: é  # 峨\nU+5CEA: yù  # 峪\nU+5CED: qiào  # 峭\nU+5CF0: fēng  # 峰\nU+5CF1: náo  # 峱\nU+5CFB: jùn  # 峻\nU+5CFF: yǔ  # 峿 -> wú\nU+5D00: làng  # 崀 -> lǎng\nU+5D01: kàn  # 崁\nU+5D02: láo  # 崂\nU+5D03: lái  # 崃\nU+5D04: xiǎn  # 崄\nU+5D06: kōng  # 崆\nU+5D07: chóng  # 崇\nU+5D0C: jū  # 崌\nU+5D0E: qí  # 崎\nU+5D12: zú  # 崒\nU+5D14: cuī  # 崔\nU+5D16: yá  # 崖\nU+5D1A: léng  # 崚 -> líng\nU+5D1B: jué  # 崛\nU+5D1E: guō  # 崞\nU+5D1F: yín  # 崟\nU+5D21: hán  # 崡\nU+5D24: xiáo  # 崤\nU+5D26: yān  # 崦\nU+5D27: sōng  # 崧\nU+5D29: bēng  # 崩\nU+5D2D: zhǎn  # 崭\nU+5D2E: gù  # 崮\nU+5D34: wǎi  # 崴\nU+5D36: fēng  # 崶\nU+5D3D: zǎi  # 崽\nU+5D3E: yǎo  # 崾\nU+5D3F: è  # 崿\nU+5D41: kān  # 嵁\nU+5D45: hán  # 嵅\nU+5D47: jī  # 嵇\nU+5D4A: shèng  # 嵊\nU+5D4B: méi  # 嵋\nU+5D4C: qiàn  # 嵌\nU+5D4E: yú  # 嵎\nU+5D56: chá  # 嵖\nU+5D58: róng  # 嵘\nU+5D5A: qīn  # 嵚\nU+5D5B: yú  # 嵛\nU+5D5D: lǒu  # 嵝\nU+5D69: sōng  # 嵩\nU+5D6B: zī  # 嵫\nU+5D6C: wéi  # 嵬\nU+5D6F: cuó  # 嵯\nU+5D72: niè  # 嵲\nU+5D74: jǐ  # 嵴\nU+5D82: zhàng  # 嶂\nU+5D85: áo  # 嶅\nU+5D8D: xí  # 嶍\nU+5D92: céng  # 嶒\nU+5D93: bō  # 嶓\nU+5D99: lín  # 嶙\nU+5D9D: dèng  # 嶝\nU+5D9F: zūn  # 嶟\nU+5DA6: zhān  # 嶦\nU+5DB2: xī  # 嶲 -> guī\nU+5DB7: yí  # 嶷\nU+5DC5: diān  # 巅\nU+5DC7: xī  # 巇\nU+5DC9: chán  # 巉\nU+5DCD: wēi  # 巍\nU+5DDD: chuān  # 川\nU+5DDE: zhōu  # 州\nU+5DE1: xún  # 巡\nU+5DE2: cháo  # 巢\nU+5DE5: gōng  # 工\nU+5DE6: zuǒ  # 左\nU+5DE7: qiǎo  # 巧\nU+5DE8: jù  # 巨\nU+5DE9: gǒng  # 巩\nU+5DEB: wū  # 巫\nU+5DEE: chà  # 差\nU+5DEF: qiú  # 巯\nU+5DF1: jǐ  # 己\nU+5DF2: yǐ  # 已\nU+5DF3: sì  # 巳\nU+5DF4: bā  # 巴\nU+5DF7: xiàng  # 巷\nU+5DFD: xùn  # 巽\nU+5DFE: jīn  # 巾\nU+5E01: bì  # 币\nU+5E02: shì  # 市\nU+5E03: bù  # 布\nU+5E05: shuài  # 帅\nU+5E06: fān  # 帆\nU+5E08: shī  # 师\nU+5E0C: xī  # 希\nU+5E0F: wéi  # 帏\nU+5E10: zhàng  # 帐\nU+5E11: tǎng  # 帑\nU+5E14: pèi  # 帔\nU+5E15: pà  # 帕\nU+5E16: tiē  # 帖 -> tiè\nU+5E18: lián  # 帘\nU+5E19: zhì  # 帙\nU+5E1A: zhǒu  # 帚\nU+5E1B: bó  # 帛\nU+5E1C: zhì  # 帜\nU+5E1D: dì  # 帝\nU+5E21: píng  # 帡\nU+5E26: dài  # 带\nU+5E27: zhēn  # 帧\nU+5E28: shuì  # 帨\nU+5E2D: xí  # 席\nU+5E2E: bāng  # 帮\nU+5E31: chóu  # 帱\nU+5E37: wéi  # 帷\nU+5E38: cháng  # 常\nU+5E3B: zé  # 帻\nU+5E3C: guó  # 帼\nU+5E3D: mào  # 帽\nU+5E42: mì  # 幂\nU+5E44: wò  # 幄\nU+5E45: fú  # 幅\nU+5E4C: huǎng  # 幌\nU+5E54: màn  # 幔\nU+5E55: mù  # 幕\nU+5E56: biāo  # 幖\nU+5E5B: zhàng  # 幛\nU+5E5E: fú  # 幞\nU+5E61: fān  # 幡\nU+5E62: chuáng  # 幢\nU+5E6A: méng  # 幪\nU+5E72: gàn  # 干 -> gān\nU+5E73: píng  # 平\nU+5E74: nián  # 年\nU+5E76: bìng  # 并\nU+5E78: xìng  # 幸\nU+5E7A: yāo  # 幺\nU+5E7B: huàn  # 幻\nU+5E7C: yòu  # 幼\nU+5E7D: yōu  # 幽\nU+5E7F: guǎng  # 广\nU+5E84: zhuāng  # 庄\nU+5E86: qìng  # 庆\nU+5E87: bì  # 庇\nU+5E8A: chuáng  # 床\nU+5E8B: guǐ  # 庋\nU+5E8F: xù  # 序\nU+5E90: lú  # 庐\nU+5E91: wǔ  # 庑\nU+5E93: kù  # 库\nU+5E94: yīng  # 应\nU+5E95: dǐ  # 底\nU+5E96: páo  # 庖\nU+5E97: diàn  # 店\nU+5E99: miào  # 庙\nU+5E9A: gēng  # 庚\nU+5E9C: fǔ  # 府\nU+5E9E: páng  # 庞\nU+5E9F: fèi  # 废\nU+5EA0: xiáng  # 庠\nU+5EA4: zhì  # 庤\nU+5EA5: xiū  # 庥\nU+5EA6: dù  # 度\nU+5EA7: zuò  # 座\nU+5EAD: tíng  # 庭\nU+5EB1: chěng  # 庱\nU+5EB3: bì  # 庳 -> bēi\nU+5EB5: ān  # 庵\nU+5EB6: shù  # 庶\nU+5EB7: kāng  # 康\nU+5EB8: yōng  # 庸\nU+5EB9: tuǒ  # 庹\nU+5EBC: qǐng  # 庼\nU+5EBE: yǔ  # 庾\nU+5EC6: guī  # 廆\nU+5EC9: lián  # 廉\nU+5ECA: láng  # 廊\nU+5ECB: sōu  # 廋\nU+5ED1: jǐn  # 廑\nU+5ED2: áo  # 廒\nU+5ED3: kuò  # 廓\nU+5ED6: liào  # 廖\nU+5ED9: yì  # 廙\nU+5EDB: chán  # 廛\nU+5EE8: xiè  # 廨\nU+5EEA: lǐn  # 廪\nU+5EF6: yán  # 延\nU+5EF7: tíng  # 廷\nU+5EFA: jiàn  # 建\nU+5EFF: niàn  # 廿\nU+5F00: kāi  # 开\nU+5F01: biàn  # 弁\nU+5F02: yì  # 异\nU+5F03: qì  # 弃\nU+5F04: nòng  # 弄\nU+5F06: jǔ  # 弆\nU+5F07: yǎn  # 弇\nU+5F08: yì  # 弈\nU+5F0A: bì  # 弊\nU+5F0B: yì  # 弋\nU+5F0F: shì  # 式\nU+5F11: shì  # 弑\nU+5F13: gōng  # 弓\nU+5F15: yǐn  # 引\nU+5F17: fú  # 弗\nU+5F18: hóng  # 弘\nU+5F1B: chí  # 弛\nU+5F1F: dì  # 弟\nU+5F20: zhāng  # 张\nU+5F22: tāo  # 弢\nU+5F25: mí  # 弥\nU+5F26: xián  # 弦\nU+5F27: hú  # 弧\nU+5F28: chāo  # 弨\nU+5F29: nǔ  # 弩\nU+5F2D: mǐ  # 弭\nU+5F2F: wān  # 弯\nU+5F31: ruò  # 弱\nU+5F36: jiàng  # 弶\nU+5F38: péng  # 弸\nU+5F39: dàn  # 弹\nU+5F3A: qiáng  # 强\nU+5F3C: bì  # 弼\nU+5F40: gòu  # 彀\nU+5F52: guī  # 归\nU+5F53: dāng  # 当\nU+5F55: lù  # 录\nU+5F56: tuàn  # 彖\nU+5F57: huì  # 彗\nU+5F58: zhì  # 彘\nU+5F5D: yí  # 彝\nU+5F5F: yuē  # 彟 -> huò\nU+5F62: xíng  # 形\nU+5F64: tóng  # 彤\nU+5F66: yàn  # 彦\nU+5F67: yù  # 彧\nU+5F69: cǎi  # 彩\nU+5F6A: biāo  # 彪\nU+5F6C: bīn  # 彬\nU+5F6D: péng  # 彭\nU+5F70: zhāng  # 彰\nU+5F71: yǐng  # 影\nU+5F73: chì  # 彳\nU+5F77: páng  # 彷\nU+5F79: yì  # 役\nU+5F7B: chè  # 彻\nU+5F7C: bǐ  # 彼\nU+5F80: wǎng  # 往\nU+5F81: zhēng  # 征\nU+5F82: cú  # 徂\nU+5F84: jìng  # 径\nU+5F85: dài  # 待\nU+5F87: xùn  # 徇\nU+5F88: hěn  # 很\nU+5F89: yáng  # 徉\nU+5F8A: huái  # 徊\nU+5F8B: lǜ  # 律\nU+5F90: xú  # 徐\nU+5F92: tú  # 徒\nU+5F95: lái  # 徕 -> lài\nU+5F97: dé  # 得\nU+5F98: pái  # 徘\nU+5F99: xǐ  # 徙\nU+5F9B: jì  # 徛\nU+5F9C: cháng  # 徜\nU+5FA1: yù  # 御\nU+5FA8: huáng  # 徨\nU+5FAA: xún  # 循\nU+5FAD: yáo  # 徭\nU+5FAE: wēi  # 微\nU+5FB5: zhēng  # 徵 -> zhǐ\nU+5FB7: dé  # 德\nU+5FBC: jiǎo  # 徼\nU+5FBD: huī  # 徽\nU+5FC3: xīn  # 心\nU+5FC5: bì  # 必\nU+5FC6: yì  # 忆\nU+5FC9: dāo  # 忉\nU+5FCC: jì  # 忌\nU+5FCD: rěn  # 忍\nU+5FCF: chàn  # 忏\nU+5FD0: tǎn  # 忐\nU+5FD1: tè  # 忑\nU+5FD2: tè  # 忒\nU+5FD6: cǔn  # 忖\nU+5FD7: zhì  # 志\nU+5FD8: wàng  # 忘\nU+5FD9: máng  # 忙\nU+5FDD: tiǎn  # 忝\nU+5FDE: mín  # 忞 -> mǐn\nU+5FE0: zhōng  # 忠\nU+5FE1: chōng  # 忡\nU+5FE4: wǔ  # 忤\nU+5FE7: yōu  # 忧\nU+5FEA: sōng  # 忪\nU+5FEB: kuài  # 快\nU+5FED: biàn  # 忭\nU+5FEE: zhì  # 忮\nU+5FF1: chén  # 忱\nU+5FF3: tún  # 忳\nU+5FF5: niàn  # 念\nU+5FF8: niǔ  # 忸\nU+5FFA: xiān  # 忺\nU+5FFB: xīn  # 忻\nU+5FFD: hū  # 忽\nU+5FFE: kài  # 忾\nU+5FFF: fèn  # 忿\nU+6000: huái  # 怀\nU+6001: tài  # 态\nU+6002: sǒng  # 怂\nU+6003: wǔ  # 怃\nU+6004: òu  # 怄\nU+6005: chàng  # 怅\nU+6006: chuàng  # 怆\nU+600A: chāo  # 怊\nU+600D: zuò  # 怍\nU+600E: zěn  # 怎\nU+600F: yàng  # 怏\nU+6012: nù  # 怒\nU+6014: zhēng  # 怔\nU+6015: pà  # 怕\nU+6016: bù  # 怖\nU+6019: hù  # 怙\nU+601B: dá  # 怛\nU+601C: lián  # 怜\nU+601D: sī  # 思\nU+6020: dài  # 怠\nU+6021: yí  # 怡\nU+6025: jí  # 急\nU+6026: pēng  # 怦\nU+6027: xìng  # 性\nU+6028: yuàn  # 怨\nU+6029: ní  # 怩\nU+602A: guài  # 怪\nU+602B: fú  # 怫\nU+602F: qiè  # 怯\nU+6035: chù  # 怵\nU+603B: zǒng  # 总\nU+603C: duì  # 怼\nU+603F: yì  # 怿\nU+6041: nèn  # 恁\nU+6042: xún  # 恂\nU+6043: shì  # 恃\nU+604B: liàn  # 恋\nU+604D: huǎng  # 恍\nU+6050: kǒng  # 恐\nU+6052: héng  # 恒\nU+6053: xī  # 恓\nU+6054: jiǎo  # 恔 -> xiào\nU+6055: shù  # 恕\nU+6059: yàng  # 恙\nU+605A: huì  # 恚\nU+605D: jiá  # 恝\nU+6062: huī  # 恢\nU+6063: zì  # 恣\nU+6064: xù  # 恤\nU+6067: nǜ  # 恧\nU+6068: hèn  # 恨\nU+6069: ēn  # 恩\nU+606A: kè  # 恪\nU+606B: dòng  # 恫\nU+606C: tián  # 恬\nU+606D: gōng  # 恭\nU+606F: xī  # 息\nU+6070: qià  # 恰\nU+6073: kěn  # 恳\nU+6076: è  # 恶\nU+6078: tòng  # 恸\nU+6079: yān  # 恹\nU+607A: kǎi  # 恺\nU+607B: cè  # 恻\nU+607C: nǎo  # 恼\nU+607D: yùn  # 恽\nU+607F: yǒng  # 恿\nU+6083: kǔn  # 悃\nU+6084: qiāo  # 悄 -> qiǎo\nU+6086: yù  # 悆\nU+6088: jiè  # 悈\nU+6089: xī  # 悉\nU+608C: tì  # 悌\nU+608D: hàn  # 悍\nU+6092: yì  # 悒\nU+6094: huǐ  # 悔\nU+6096: bèi  # 悖\nU+609A: sǒng  # 悚\nU+609B: quān  # 悛\nU+609D: kuī  # 悝\nU+609F: wù  # 悟\nU+60A0: yōu  # 悠\nU+60A2: liàng  # 悢\nU+60A3: huàn  # 患\nU+60A6: yuè  # 悦\nU+60A8: nín  # 您\nU+60AB: què  # 悫\nU+60AC: xuán  # 悬\nU+60AD: qiān  # 悭\nU+60AF: mǐn  # 悯\nU+60B0: cóng  # 悰\nU+60B1: fěi  # 悱\nU+60B2: bēi  # 悲\nU+60B4: cuì  # 悴\nU+60B8: jì  # 悸\nU+60BB: xìng  # 悻\nU+60BC: dào  # 悼\nU+60C5: qíng  # 情\nU+60C6: chóu  # 惆\nU+60C7: dūn  # 惇\nU+60CA: jīng  # 惊\nU+60CB: wǎn  # 惋\nU+60CE: jì  # 惎\nU+60D1: huò  # 惑\nU+60D4: tán  # 惔\nU+60D5: tì  # 惕\nU+60D8: wǎng  # 惘\nU+60D9: chuò  # 惙\nU+60DA: hū  # 惚\nU+60DB: hūn  # 惛\nU+60DC: xī  # 惜\nU+60DD: chǎng  # 惝\nU+60DF: wéi  # 惟\nU+60E0: huì  # 惠\nU+60E6: diàn  # 惦\nU+60E7: jù  # 惧\nU+60E8: cǎn  # 惨\nU+60E9: chéng  # 惩\nU+60EB: bèi  # 惫\nU+60EC: qiè  # 惬\nU+60ED: cán  # 惭\nU+60EE: dàn  # 惮\nU+60EF: guàn  # 惯\nU+60F0: duò  # 惰\nU+60F3: xiǎng  # 想\nU+60F4: zhuì  # 惴\nU+60F6: huáng  # 惶\nU+60F9: rě  # 惹\nU+60FA: xīng  # 惺\nU+6100: qiǎo  # 愀\nU+6101: chóu  # 愁\nU+6103: xuān  # 愃\nU+6106: qiān  # 愆\nU+6108: yù  # 愈\nU+6109: yú  # 愉\nU+610D: mǐn  # 愍\nU+610E: bì  # 愎\nU+610F: yì  # 意\nU+6110: miǎn  # 愐\nU+6114: yīn  # 愔\nU+6115: è  # 愕\nU+611A: yú  # 愚\nU+611F: gǎn  # 感\nU+6120: yùn  # 愠\nU+6123: lèng  # 愣\nU+6124: fèn  # 愤\nU+6126: kuì  # 愦\nU+6127: kuì  # 愧\nU+612B: sù  # 愫\nU+612D: qí  # 愭\nU+613F: yuàn  # 愿\nU+6146: tāo  # 慆\nU+6148: cí  # 慈\nU+614A: qiàn  # 慊\nU+614C: huāng  # 慌\nU+614E: shèn  # 慎\nU+6151: shè  # 慑\nU+6155: mù  # 慕\nU+615D: tè  # 慝\nU+6162: màn  # 慢\nU+6165: zào  # 慥\nU+6167: huì  # 慧\nU+6168: kǎi  # 慨\nU+616C: qín  # 慬\nU+616D: yìn  # 慭\nU+6170: wèi  # 慰\nU+6175: yōng  # 慵\nU+6177: kāng  # 慷\nU+618B: biē  # 憋\nU+618E: zēng  # 憎\nU+6194: qiáo  # 憔\nU+6195: chéng  # 憕\nU+6199: xī  # 憙 -> xǐ\nU+61A7: chōng  # 憧\nU+61A8: hān  # 憨\nU+61A9: qì  # 憩\nU+61AC: jǐng  # 憬\nU+61AD: liǎo  # 憭\nU+61B7: chù  # 憷\nU+61BA: dàn  # 憺\nU+61BE: hàn  # 憾\nU+61C2: dǒng  # 懂\nU+61C8: xiè  # 懈\nU+61CA: ào  # 懊\nU+61CB: mào  # 懋\nU+61D1: mèn  # 懑\nU+61D2: lǎn  # 懒\nU+61D4: lǐn  # 懔\nU+61E6: nuò  # 懦\nU+61F5: měng  # 懵\nU+61FF: yì  # 懿\nU+6206: gàng  # 戆\nU+6208: gē  # 戈\nU+620A: wù  # 戊\nU+620B: jiān  # 戋\nU+620C: xū  # 戌\nU+620D: shù  # 戍\nU+620E: róng  # 戎\nU+620F: xì  # 戏\nU+6210: chéng  # 成\nU+6211: wǒ  # 我\nU+6212: jiè  # 戒\nU+6215: qiāng  # 戕\nU+6216: huò  # 或\nU+6217: qiāng  # 戗\nU+6218: zhàn  # 战\nU+621A: qī  # 戚\nU+621B: jiá  # 戛\nU+621F: jǐ  # 戟\nU+6221: kān  # 戡\nU+6222: jí  # 戢\nU+6223: kuí  # 戣\nU+6224: gài  # 戤\nU+6225: děng  # 戥\nU+622A: jié  # 截\nU+622C: jiǎn  # 戬\nU+622D: yǎn  # 戭\nU+622E: lù  # 戮\nU+6233: chuō  # 戳\nU+6234: dài  # 戴\nU+6237: hù  # 户\nU+623D: hù  # 戽\nU+623E: lì  # 戾\nU+623F: fáng  # 房\nU+6240: suǒ  # 所\nU+6241: biǎn  # 扁\nU+6242: diàn  # 扂\nU+6243: jiōng  # 扃\nU+6245: yí  # 扅\nU+6246: yǐ  # 扆\nU+6247: shàn  # 扇\nU+6248: hù  # 扈\nU+6249: fēi  # 扉\nU+624A: yǎn  # 扊\nU+624B: shǒu  # 手\nU+624D: cái  # 才\nU+624E: zhā  # 扎 -> zā\nU+6251: pū  # 扑\nU+6252: bā  # 扒\nU+6253: dǎ  # 打\nU+6254: rēng  # 扔\nU+6258: tuō  # 托\nU+625B: káng  # 扛\nU+625E: gǎn  # 扞 -> hàn\nU+6263: kòu  # 扣\nU+6266: qiān  # 扦\nU+6267: zhí  # 执\nU+6269: kuò  # 扩\nU+626A: mén  # 扪\nU+626B: sǎo  # 扫\nU+626C: yáng  # 扬\nU+626D: niǔ  # 扭\nU+626E: bàn  # 扮\nU+626F: chě  # 扯\nU+6270: rǎo  # 扰\nU+6273: bān  # 扳\nU+6276: fú  # 扶\nU+6279: pī  # 批\nU+627A: zhǐ  # 扺\nU+627C: è  # 扼\nU+627D: dèn  # 扽\nU+627E: zhǎo  # 找\nU+627F: chéng  # 承\nU+6280: jì  # 技\nU+6283: biàn  # 抃\nU+6284: chāo  # 抄\nU+6289: jué  # 抉\nU+628A: bǎ  # 把\nU+6291: yì  # 抑\nU+6292: shū  # 抒\nU+6293: zhuā  # 抓\nU+6294: póu  # 抔\nU+6295: tóu  # 投\nU+6296: dǒu  # 抖\nU+6297: kàng  # 抗\nU+6298: zhé  # 折 -> zhē\nU+629A: fǔ  # 抚\nU+629B: pāo  # 抛\nU+629F: tuán  # 抟\nU+62A0: kōu  # 抠\nU+62A1: lūn  # 抡\nU+62A2: qiǎng  # 抢 -> qiāng\nU+62A4: hù  # 护\nU+62A5: bào  # 报\nU+62A8: pēng  # 抨\nU+62AB: pī  # 披\nU+62AC: tái  # 抬\nU+62B1: bào  # 抱\nU+62B5: dǐ  # 抵\nU+62B9: mǒ  # 抹\nU+62BB: chēn  # 抻\nU+62BC: yā  # 押\nU+62BD: chōu  # 抽\nU+62BF: mǐn  # 抿\nU+62C2: fú  # 拂\nU+62C3: zhǎ  # 拃\nU+62C4: zhǔ  # 拄\nU+62C5: dān  # 担\nU+62C6: chāi  # 拆\nU+62C7: mǔ  # 拇\nU+62C8: niān  # 拈\nU+62C9: lā  # 拉\nU+62CA: fǔ  # 拊\nU+62CC: bàn  # 拌\nU+62CD: pāi  # 拍\nU+62CE: līn  # 拎\nU+62D0: guǎi  # 拐\nU+62D2: jù  # 拒\nU+62D3: tuò  # 拓\nU+62D4: bá  # 拔\nU+62D6: tuō  # 拖\nU+62D7: ǎo  # 拗\nU+62D8: jū  # 拘\nU+62D9: zhuō  # 拙\nU+62DB: zhāo  # 招\nU+62DC: bài  # 拜\nU+62DF: nǐ  # 拟\nU+62E2: lǒng  # 拢\nU+62E3: jiǎn  # 拣\nU+62E4: qiá  # 拤 -> qiǎ\nU+62E5: yōng  # 拥\nU+62E6: lán  # 拦\nU+62E7: níng  # 拧\nU+62E8: bō  # 拨\nU+62E9: zé  # 择\nU+62EC: kuò  # 括\nU+62ED: shì  # 拭\nU+62EE: jié  # 拮\nU+62EF: zhěng  # 拯\nU+62F1: gǒng  # 拱\nU+62F3: quán  # 拳\nU+62F4: shuān  # 拴\nU+62F6: zā  # 拶\nU+62F7: kǎo  # 拷\nU+62FC: pīn  # 拼\nU+62FD: zhuāi  # 拽 -> zhuài\nU+62FE: shí  # 拾\nU+62FF: ná  # 拿\nU+6301: chí  # 持\nU+6302: guà  # 挂\nU+6307: zhǐ  # 指\nU+6308: qiè  # 挈\nU+6309: àn  # 按\nU+630E: kuà  # 挎\nU+6311: tiāo  # 挑\nU+6313: zhā  # 挓\nU+6316: wā  # 挖\nU+631A: zhì  # 挚\nU+631B: luán  # 挛\nU+631D: wō  # 挝 -> zhuā\nU+631E: tà  # 挞\nU+631F: xié  # 挟\nU+6320: náo  # 挠\nU+6321: dǎng  # 挡\nU+6323: zhēng  # 挣 -> zhèng\nU+6324: jǐ  # 挤\nU+6325: huī  # 挥\nU+6326: xián  # 挦\nU+6328: āi  # 挨\nU+632A: nuó  # 挪\nU+632B: cuò  # 挫\nU+632F: zhèn  # 振\nU+6332: sā  # 挲 -> suō\nU+6339: yì  # 挹\nU+633A: tǐng  # 挺\nU+633D: wǎn  # 挽\nU+6342: wǔ  # 捂\nU+6343: jùn  # 捃\nU+6345: tǒng  # 捅\nU+6346: kǔn  # 捆\nU+6349: zhuō  # 捉\nU+634B: lǚ  # 捋 -> luō\nU+634C: bā  # 捌\nU+634D: hàn  # 捍\nU+634E: shāo  # 捎\nU+634F: niē  # 捏\nU+6350: juān  # 捐\nU+6355: bǔ  # 捕\nU+635E: lāo  # 捞\nU+635F: sǔn  # 损\nU+6361: jiǎn  # 捡\nU+6362: huàn  # 换\nU+6363: dǎo  # 捣\nU+6367: pěng  # 捧\nU+6369: liè  # 捩\nU+636D: bǎi  # 捭\nU+636E: jù  # 据\nU+636F: dáo  # 捯\nU+6376: chuí  # 捶\nU+6377: jié  # 捷\nU+637A: nà  # 捺\nU+637B: niǎn  # 捻\nU+637D: zuó  # 捽\nU+6380: xiān  # 掀\nU+6382: diān  # 掂\nU+6387: duō  # 掇\nU+6388: shòu  # 授\nU+6389: diào  # 掉\nU+638A: póu  # 掊 -> pǒu\nU+638C: zhǎng  # 掌\nU+638E: jǐ  # 掎\nU+638F: tāo  # 掏\nU+6390: qiā  # 掐\nU+6392: pái  # 排\nU+6396: yē  # 掖 -> yè\nU+6398: jué  # 掘\nU+639E: shàn  # 掞\nU+63A0: lüè  # 掠\nU+63A2: tàn  # 探\nU+63A3: chè  # 掣\nU+63A5: jiē  # 接\nU+63A7: kòng  # 控\nU+63A8: tuī  # 推\nU+63A9: yǎn  # 掩\nU+63AA: cuò  # 措\nU+63AC: jū  # 掬\nU+63AD: tiàn  # 掭\nU+63AE: qián  # 掮\nU+63B0: bāi  # 掰\nU+63B3: lǔ  # 掳\nU+63B4: guāi  # 掴 -> guó\nU+63B7: zhì  # 掷\nU+63B8: dǎn  # 掸\nU+63BA: càn  # 掺 -> chān\nU+63BC: guàn  # 掼\nU+63BE: yuàn  # 掾\nU+63C4: yú  # 揄\nU+63C6: kuí  # 揆\nU+63C9: róu  # 揉\nU+63CD: zòu  # 揍\nU+63CF: miáo  # 描\nU+63D0: tí  # 提\nU+63D2: chā  # 插\nU+63D5: zhèn  # 揕\nU+63D6: yī  # 揖\nU+63E0: yà  # 揠\nU+63E1: wò  # 握\nU+63E3: chuāi  # 揣 -> chuǎi\nU+63E9: kāi  # 揩\nU+63EA: jiū  # 揪\nU+63ED: jiē  # 揭\nU+63F3: xiē  # 揳\nU+63F4: yuán  # 援\nU+63F6: yé  # 揶\nU+63F8: zhā  # 揸\nU+63FD: lǎn  # 揽\nU+63FF: qìn  # 揿\nU+6400: chān  # 搀\nU+6401: gē  # 搁\nU+6402: lǒu  # 搂\nU+6405: jiǎo  # 搅\nU+640B: chuāi  # 搋\nU+640C: zhǎn  # 搌\nU+640F: bó  # 搏\nU+6410: chù  # 搐\nU+6412: bàng  # 搒\nU+6413: cuō  # 搓\nU+6414: sāo  # 搔\nU+641B: jiān  # 搛\nU+641C: sōu  # 搜\nU+641E: gǎo  # 搞\nU+6420: shuò  # 搠\nU+6421: sǎng  # 搡\nU+6426: nuò  # 搦\nU+642A: táng  # 搪\nU+642C: bān  # 搬\nU+642D: dā  # 搭\nU+6434: qiān  # 搴\nU+643A: xié  # 携\nU+643D: chá  # 搽\nU+6441: èn  # 摁\nU+6444: shè  # 摄\nU+6445: shū  # 摅\nU+6446: bǎi  # 摆\nU+6447: yáo  # 摇\nU+6448: bìn  # 摈\nU+644A: tān  # 摊\nU+644F: chōng  # 摏\nU+6452: bǐng  # 摒 -> bìng\nU+6454: shuāi  # 摔\nU+6458: zhāi  # 摘\nU+645B: chī  # 摛\nU+645E: luò  # 摞\nU+6467: cuī  # 摧\nU+6469: mó  # 摩\nU+646D: zhí  # 摭\nU+6474: chū  # 摴\nU+6478: mō  # 摸\nU+6479: mó  # 摹\nU+647D: biāo  # 摽 -> biào\nU+6482: liào  # 撂\nU+6484: yīng  # 撄\nU+6485: juē  # 撅\nU+6487: piē  # 撇\nU+6491: chēng  # 撑\nU+6492: sā  # 撒\nU+6495: sī  # 撕\nU+6496: hàn  # 撖\nU+6499: zǔn  # 撙\nU+649E: zhuàng  # 撞\nU+64A4: chè  # 撤\nU+64A9: liāo  # 撩\nU+64AC: qiào  # 撬\nU+64AD: bō  # 播\nU+64AE: cuō  # 撮\nU+64B0: zhuàn  # 撰\nU+64B5: niǎn  # 撵\nU+64B7: xié  # 撷\nU+64B8: lū  # 撸\nU+64BA: cuān  # 撺\nU+64BC: hàn  # 撼\nU+64C0: gǎn  # 擀\nU+64C2: léi  # 擂\nU+64C5: shàn  # 擅\nU+64CD: cāo  # 操\nU+64CE: qíng  # 擎\nU+64D0: huàn  # 擐\nU+64D2: qín  # 擒\nU+64D8: bāi  # 擘 -> bò\nU+64DE: sǒu  # 擞 -> sòu\nU+64E2: zhuó  # 擢\nU+64E4: xǐng  # 擤\nU+64E6: cā  # 擦\nU+64FF: tī  # 擿\nU+6500: pān  # 攀\nU+6509: huō  # 攉\nU+6512: zǎn  # 攒\nU+6518: rǎng  # 攘\nU+6525: zuàn  # 攥\nU+652B: jué  # 攫\nU+652E: nǎng  # 攮\nU+652F: zhī  # 支\nU+6536: shōu  # 收\nU+6538: yōu  # 攸\nU+6539: gǎi  # 改\nU+653B: gōng  # 攻\nU+653D: bān  # 攽\nU+653E: fàng  # 放\nU+653F: zhèng  # 政\nU+6545: gù  # 故\nU+6548: xiào  # 效\nU+6549: mǐ  # 敉\nU+654C: dí  # 敌\nU+654F: mǐn  # 敏\nU+6551: jiù  # 救\nU+6554: yǔ  # 敔\nU+6555: chì  # 敕\nU+6556: áo  # 敖\nU+6559: jiào  # 教\nU+655B: liǎn  # 敛\nU+655D: bì  # 敝\nU+655E: chǎng  # 敞\nU+6562: gǎn  # 敢\nU+6563: sàn  # 散\nU+6566: dūn  # 敦\nU+6569: xiào  # 敩\nU+656B: jiǎo  # 敫\nU+656C: jìng  # 敬\nU+6570: shù  # 数\nU+6572: qiāo  # 敲\nU+6574: zhěng  # 整\nU+6577: fū  # 敷\nU+6587: wén  # 文\nU+658B: zhāi  # 斋\nU+658C: bīn  # 斌\nU+6590: fěi  # 斐\nU+6591: bān  # 斑\nU+6593: lán  # 斓\nU+6597: dòu  # 斗 -> dǒu\nU+6599: liào  # 料\nU+659B: hú  # 斛\nU+659C: xié  # 斜\nU+659D: jiǎ  # 斝\nU+659F: zhēn  # 斟\nU+65A0: jiào  # 斠\nU+65A1: wò  # 斡\nU+65A4: jīn  # 斤\nU+65A5: chì  # 斥\nU+65A7: fǔ  # 斧\nU+65A9: zhǎn  # 斩\nU+65AB: zhuó  # 斫\nU+65AD: duàn  # 断\nU+65AF: sī  # 斯\nU+65B0: xīn  # 新\nU+65B6: chù  # 斶\nU+65B9: fāng  # 方\nU+65BC: yú  # 於\nU+65BD: shī  # 施\nU+65C1: páng  # 旁\nU+65C3: zhān  # 旃\nU+65C4: máo  # 旄\nU+65C5: lǚ  # 旅\nU+65C6: pèi  # 旆\nU+65CB: xuán  # 旋\nU+65CC: jīng  # 旌\nU+65CE: nǐ  # 旎\nU+65CF: zú  # 族\nU+65D0: zhào  # 旐\nU+65D2: liú  # 旒\nU+65D6: yǐ  # 旖\nU+65D7: qí  # 旗\nU+65DE: suì  # 旞\nU+65E0: wú  # 无\nU+65E2: jì  # 既\nU+65E5: rì  # 日\nU+65E6: dàn  # 旦\nU+65E7: jiù  # 旧\nU+65E8: zhǐ  # 旨\nU+65E9: zǎo  # 早\nU+65EC: xún  # 旬\nU+65ED: xù  # 旭\nU+65EE: gā  # 旮\nU+65EF: lá  # 旯\nU+65F0: gàn  # 旰\nU+65F1: hàn  # 旱\nU+65F4: xū  # 旴 -> xù\nU+65F5: chǎn  # 旵\nU+65F6: shí  # 时\nU+65F7: kuàng  # 旷\nU+65F8: yáng  # 旸\nU+65FA: wàng  # 旺\nU+65FB: mín  # 旻\nU+65FF: wǔ  # 旿 -> wù\nU+6600: yún  # 昀\nU+6602: áng  # 昂\nU+6603: zè  # 昃\nU+6604: bǎn  # 昄\nU+6606: kūn  # 昆\nU+6607: shēng  # 昇\nU+6608: hù  # 昈\nU+6609: fǎng  # 昉\nU+660A: hào  # 昊\nU+660C: chāng  # 昌\nU+660E: míng  # 明\nU+660F: hūn  # 昏\nU+6612: hū  # 昒\nU+6613: yì  # 易\nU+6614: xī  # 昔\nU+6615: xīn  # 昕\nU+6619: tán  # 昙\nU+661D: zǎn  # 昝\nU+661F: xīng  # 星\nU+6620: yìng  # 映\nU+6621: xuàn  # 昡\nU+6623: zhěn  # 昣\nU+6624: líng  # 昤\nU+6625: chūn  # 春\nU+6627: mèi  # 昧\nU+6628: zuó  # 昨\nU+662A: biàn  # 昪\nU+662B: xù  # 昫\nU+662D: zhāo  # 昭\nU+662F: shì  # 是\nU+6631: yù  # 昱\nU+6633: dié  # 昳\nU+6634: mǎo  # 昴\nU+6635: nì  # 昵\nU+6636: chǎng  # 昶\nU+663A: bǐng  # 昺\nU+663C: zhòu  # 昼\nU+663D: lóng  # 昽\nU+663E: xiǎn  # 显\nU+6641: cháo  # 晁\nU+6643: huǎng  # 晃\nU+6645: xuǎn  # 晅 -> xuān\nU+664A: zhì  # 晊\nU+664B: jìn  # 晋\nU+664C: shǎng  # 晌\nU+664F: yàn  # 晏\nU+6650: gāi  # 晐\nU+6652: shài  # 晒\nU+6653: xiǎo  # 晓\nU+6654: yè  # 晔\nU+6655: yūn  # 晕 -> yùn\nU+6656: huī  # 晖\nU+6657: hán  # 晗\nU+6659: jùn  # 晙\nU+665A: wǎn  # 晚\nU+665E: xī  # 晞\nU+665F: chéng  # 晟 -> shèng\nU+6661: bū  # 晡\nU+6662: zhé  # 晢\nU+6664: wù  # 晤\nU+6666: huì  # 晦\nU+6668: chén  # 晨\nU+666A: tiǎn  # 晪\nU+666B: zhuó  # 晫\nU+666E: pǔ  # 普\nU+666F: jǐng  # 景\nU+6670: xī  # 晰\nU+6671: shǎn  # 晱\nU+6674: qíng  # 晴\nU+6676: jīng  # 晶\nU+6677: guǐ  # 晷\nU+667A: zhì  # 智\nU+667E: liàng  # 晾\nU+6682: zàn  # 暂\nU+6684: xuān  # 暄\nU+6685: gèng  # 暅 -> xuǎn\nU+6687: xiá  # 暇\nU+668C: kuí  # 暌\nU+6691: shǔ  # 暑\nU+6695: jiǎn  # 暕\nU+6696: nuǎn  # 暖\nU+6697: àn  # 暗\nU+669D: míng  # 暝\nU+66A7: ài  # 暧\nU+66A8: jì  # 暨\nU+66AE: mù  # 暮\nU+66B2: zhāng  # 暲\nU+66B4: bào  # 暴\nU+66B5: hàn  # 暵\nU+66B6: xuán  # 暶\nU+66B9: xiān  # 暹\nU+66BE: tūn  # 暾\nU+66BF: xǐ  # 暿 -> xī\nU+66C8: tóng  # 曈\nU+66CC: zhào  # 曌\nU+66D9: shǔ  # 曙\nU+66DB: xūn  # 曛\nU+66DC: yào  # 曜\nU+66DD: pù  # 曝\nU+66E6: xī  # 曦\nU+66E9: nǎng  # 曩\nU+66F0: yuē  # 曰\nU+66F2: qū  # 曲\nU+66F3: yè  # 曳\nU+66F4: gèng  # 更 -> gēng\nU+66F7: hé  # 曷\nU+66F9: cáo  # 曹\nU+66FC: màn  # 曼\nU+66FE: céng  # 曾 -> zēng\nU+66FF: tì  # 替\nU+6700: zuì  # 最\nU+6708: yuè  # 月\nU+6709: yǒu  # 有\nU+670B: péng  # 朋\nU+670D: fú  # 服\nU+670F: fěi  # 朏\nU+6710: qú  # 朐\nU+6713: tiǎo  # 朓\nU+6714: shuò  # 朔\nU+6715: zhèn  # 朕\nU+6717: lǎng  # 朗\nU+671B: wàng  # 望\nU+671D: cháo  # 朝 -> zhāo\nU+671F: qī  # 期\nU+6726: méng  # 朦\nU+6728: mù  # 木\nU+672A: wèi  # 未\nU+672B: mò  # 末\nU+672C: běn  # 本\nU+672D: zhá  # 札\nU+672F: shù  # 术\nU+6731: zhū  # 朱\nU+6733: bā  # 朳\nU+6734: pǔ  # 朴\nU+6735: duǒ  # 朵\nU+6738: lì  # 朸\nU+673A: jī  # 机\nU+673D: xiǔ  # 朽\nU+6740: shā  # 杀\nU+6742: zá  # 杂\nU+6743: quán  # 权\nU+6744: qiān  # 杄\nU+6746: gān  # 杆\nU+6748: chā  # 杈\nU+6749: shān  # 杉\nU+674C: wù  # 杌\nU+674E: lǐ  # 李\nU+674F: xìng  # 杏\nU+6750: cái  # 材\nU+6751: cūn  # 村\nU+6753: biāo  # 杓 -> sháo\nU+6755: dì  # 杕\nU+6756: zhàng  # 杖\nU+6759: yì  # 杙\nU+675C: dù  # 杜\nU+675E: qǐ  # 杞\nU+675F: shù  # 束\nU+6760: gāng  # 杠 -> gàng\nU+6761: tiáo  # 条\nU+6765: lái  # 来\nU+6767: máng  # 杧\nU+6768: yáng  # 杨\nU+6769: mà  # 杩\nU+676A: miǎo  # 杪\nU+676D: háng  # 杭\nU+676F: bēi  # 杯\nU+6770: jié  # 杰\nU+6772: gǎo  # 杲\nU+6773: yǎo  # 杳\nU+6775: chǔ  # 杵\nU+6777: pá  # 杷\nU+677B: chǒu  # 杻 -> niǔ\nU+677C: zhù  # 杼\nU+677E: sōng  # 松\nU+677F: bǎn  # 板\nU+6781: jí  # 极\nU+6784: gòu  # 构\nU+6785: jī  # 枅\nU+6787: pí  # 枇\nU+6789: wǎng  # 枉\nU+678B: fāng  # 枋\nU+678D: yì  # 枍\nU+6790: xī  # 析\nU+6795: zhěn  # 枕\nU+6797: lín  # 林\nU+6798: ruì  # 枘\nU+679A: méi  # 枚\nU+679C: guǒ  # 果\nU+679D: zhī  # 枝\nU+679E: cōng  # 枞\nU+67A2: shū  # 枢\nU+67A3: zǎo  # 枣\nU+67A5: lì  # 枥\nU+67A7: jiǎn  # 枧\nU+67A8: chéng  # 枨\nU+67AA: qiāng  # 枪\nU+67AB: fēng  # 枫\nU+67AD: xiāo  # 枭\nU+67AF: kū  # 枯\nU+67B0: píng  # 枰\nU+67B2: xǐ  # 枲\nU+67B3: zhǐ  # 枳\nU+67B5: xiāo  # 枵\nU+67B6: jià  # 架\nU+67B7: jiā  # 枷\nU+67B8: gǒu  # 枸 -> jǔ\nU+67B9: bāo  # 枹\nU+67C1: duò  # 柁 -> tuó\nU+67C3: líng  # 柃\nU+67C4: bǐng  # 柄\nU+67C8: bàn  # 柈 -> pán\nU+67CA: zhōng  # 柊\nU+67CF: bǎi  # 柏\nU+67D0: mǒu  # 某\nU+67D1: gān  # 柑\nU+67D2: qī  # 柒\nU+67D3: rǎn  # 染\nU+67D4: róu  # 柔\nU+67D6: sháo  # 柖\nU+67D8: zhè  # 柘\nU+67D9: xiá  # 柙\nU+67DA: yòu  # 柚\nU+67DC: guì  # 柜\nU+67DD: tuò  # 柝\nU+67DE: zhà  # 柞 -> zuò\nU+67E0: níng  # 柠\nU+67E2: dǐ  # 柢\nU+67E5: chá  # 查\nU+67E9: jiù  # 柩\nU+67EC: jiǎn  # 柬\nU+67EF: kē  # 柯\nU+67F0: nài  # 柰\nU+67F1: zhù  # 柱\nU+67F3: liǔ  # 柳\nU+67F4: chái  # 柴\nU+67F7: chù  # 柷 -> zhù\nU+67FD: chēng  # 柽\nU+67FF: shì  # 柿\nU+6800: zhī  # 栀\nU+6805: zhà  # 栅\nU+6807: biāo  # 标\nU+6808: zhàn  # 栈\nU+6809: zhì  # 栉\nU+680A: lóng  # 栊\nU+680B: dòng  # 栋\nU+680C: lú  # 栌\nU+680E: lì  # 栎\nU+680F: lán  # 栏\nU+6810: yǒng  # 栐\nU+6811: shù  # 树\nU+6812: xún  # 栒\nU+6813: shuān  # 栓\nU+6816: qī  # 栖\nU+6817: lì  # 栗\nU+681D: guā  # 栝\nU+681F: bēn  # 栟\nU+6821: xiào  # 校\nU+6829: xǔ  # 栩\nU+682A: zhū  # 株\nU+6832: kǎo  # 栲\nU+6833: lǎo  # 栳\nU+6834: zhān  # 栴\nU+6837: yàng  # 样\nU+6838: hé  # 核\nU+6839: gēn  # 根\nU+683B: shì  # 栻\nU+683C: gé  # 格\nU+683D: zāi  # 栽\nU+683E: luán  # 栾\nU+6840: jié  # 桀\nU+6841: héng  # 桁\nU+6842: guì  # 桂\nU+6843: táo  # 桃\nU+6844: guāng  # 桄\nU+6845: wéi  # 桅\nU+6846: kuāng  # 框 -> kuàng\nU+6848: àn  # 案\nU+6849: ān  # 桉\nU+684A: juàn  # 桊\nU+684C: zhuō  # 桌\nU+684E: zhì  # 桎\nU+6850: tóng  # 桐\nU+6851: sāng  # 桑\nU+6853: huán  # 桓\nU+6854: jú  # 桔 -> jié\nU+6855: jiù  # 桕\nU+6860: yā  # 桠\nU+6861: ráo  # 桡\nU+6862: zhēn  # 桢\nU+6863: dàng  # 档\nU+6864: qī  # 桤\nU+6865: qiáo  # 桥\nU+6866: huà  # 桦\nU+6867: guì  # 桧\nU+6868: jiǎng  # 桨\nU+6869: zhuāng  # 桩\nU+686B: suō  # 桫\nU+686F: tīng  # 桯\nU+6872: po  # 桲 -> bó\nU+6874: fú  # 桴\nU+6876: tǒng  # 桶\nU+6877: jué  # 桷\nU+6879: láng  # 桹\nU+6881: liáng  # 梁\nU+6883: tǐng  # 梃\nU+6885: méi  # 梅\nU+6886: bāng  # 梆\nU+688C: tú  # 梌\nU+688F: gù  # 梏\nU+6893: zǐ  # 梓\nU+6897: gěng  # 梗\nU+68A0: lǚ  # 梠\nU+68A2: shāo  # 梢\nU+68A3: cén  # 梣 -> chén\nU+68A6: mèng  # 梦\nU+68A7: wú  # 梧\nU+68A8: lí  # 梨\nU+68AD: suō  # 梭\nU+68AF: tī  # 梯\nU+68B0: xiè  # 械\nU+68B3: shū  # 梳\nU+68B4: chān  # 梴\nU+68B5: fàn  # 梵\nU+68BC: táo  # 梼 -> chóu\nU+68BD: zhì  # 梽\nU+68BE: lái  # 梾\nU+68BF: lián  # 梿\nU+68C0: jiǎn  # 检\nU+68C1: zhuō  # 棁\nU+68C2: líng  # 棂\nU+68C9: mián  # 棉\nU+68CB: qí  # 棋\nU+68CD: gùn  # 棍\nU+68D0: fěi  # 棐\nU+68D2: bàng  # 棒\nU+68D3: bàng  # 棓\nU+68D5: zōng  # 棕\nU+68D8: jí  # 棘\nU+68DA: péng  # 棚\nU+68E0: táng  # 棠\nU+68E3: dì  # 棣\nU+68E4: cuò  # 棤 -> què\nU+68E8: qǐ  # 棨\nU+68EA: yǎn  # 棪\nU+68EB: yù  # 棫\nU+68EC: quān  # 棬\nU+68EE: sēn  # 森\nU+68F0: chuí  # 棰\nU+68F1: léng  # 棱\nU+68F5: kē  # 棵\nU+68F9: zhào  # 棹\nU+68FA: guān  # 棺\nU+68FB: fēn  # 棻\nU+68FC: fén  # 棼\nU+68FD: shēn  # 棽 -> chēn\nU+6900: wǎn  # 椀\nU+6901: guǒ  # 椁\nU+6905: yǐ  # 椅\nU+6906: chóu  # 椆\nU+690B: liáng  # 椋\nU+690D: zhí  # 植\nU+690E: chuí  # 椎 -> zhuī\nU+6910: jū  # 椐\nU+6911: bēi  # 椑\nU+6912: jiāo  # 椒\nU+6913: zhuó  # 椓\nU+691F: dú  # 椟\nU+6920: qiàn  # 椠\nU+6924: luó  # 椤\nU+692A: pèng  # 椪\nU+692D: tuǒ  # 椭\nU+6930: yē  # 椰\nU+6934: duàn  # 椴\nU+6938: yí  # 椸\nU+6939: shèn  # 椹 -> zhēn\nU+693D: chuán  # 椽\nU+693F: chūn  # 椿\nU+6942: zhā  # 楂\nU+6952: sī  # 楒\nU+6954: xiē  # 楔\nU+6957: jiàn  # 楗\nU+6959: mào  # 楙 -> máo\nU+695A: chǔ  # 楚\nU+695D: liàn  # 楝\nU+695E: léng  # 楞\nU+6960: nán  # 楠\nU+6963: méi  # 楣\nU+6966: xuàn  # 楦\nU+6969: pián  # 楩\nU+696A: yè  # 楪 -> dié\nU+696B: jí  # 楫\nU+696E: chǔ  # 楮\nU+696F: dùn  # 楯 -> shǔn\nU+6977: kǎi  # 楷\nU+6978: qiū  # 楸\nU+6979: yíng  # 楹\nU+697C: lóu  # 楼\nU+6982: gài  # 概\nU+6983: tán  # 榃\nU+6984: lǎn  # 榄\nU+6985: wēn  # 榅\nU+6986: yú  # 榆\nU+6987: chèn  # 榇\nU+6988: lǘ  # 榈\nU+6989: jǔ  # 榉\nU+698D: xiè  # 榍\nU+6991: fú  # 榑\nU+6994: láng  # 榔\nU+6995: róng  # 榕\nU+6996: gǔ  # 榖\nU+699B: zhēn  # 榛\nU+699C: bǎng  # 榜\nU+69A7: fěi  # 榧\nU+69A8: zhà  # 榨\nU+69AB: sǔn  # 榫\nU+69AD: xiè  # 榭\nU+69B0: zhī  # 榰\nU+69B1: cuī  # 榱\nU+69B4: liú  # 榴\nU+69B7: què  # 榷\nU+69BB: tà  # 榻\nU+69C1: gǎo  # 槁\nU+69C3: pán  # 槃\nU+69CA: shuò  # 槊\nU+69CC: chuí  # 槌\nU+69CE: chá  # 槎\nU+69D0: huái  # 槐\nU+69D4: gāo  # 槔\nU+69DA: jiǎ  # 槚\nU+69DB: kǎn  # 槛 -> jiàn\nU+69DC: zuì  # 槜\nU+69DF: bīn  # 槟\nU+69E0: zhū  # 槠\nU+69ED: qī  # 槭 -> qì\nU+69F1: yǒu  # 槱\nU+69F2: hú  # 槲\nU+69FD: cáo  # 槽\nU+69FF: jǐn  # 槿\nU+6A0A: fán  # 樊\nU+6A17: chū  # 樗\nU+6A18: táng  # 樘\nU+6A1F: zhāng  # 樟\nU+6A21: mó  # 模\nU+6A28: xī  # 樨\nU+6A2A: héng  # 横\nU+6A2F: qiáng  # 樯\nU+6A31: yīng  # 樱\nU+6A35: qiáo  # 樵\nU+6A3D: zūn  # 樽\nU+6A3E: yuè  # 樾\nU+6A44: gǎn  # 橄\nU+6A47: qiāo  # 橇\nU+6A50: tuó  # 橐\nU+6A51: lǎo  # 橑 -> liáo\nU+6A58: jú  # 橘\nU+6A59: chéng  # 橙\nU+6A5B: jué  # 橛\nU+6A5E: huì  # 橞\nU+6A61: xiàng  # 橡\nU+6A65: zhū  # 橥\nU+6A66: tóng  # 橦\nU+6A71: chú  # 橱\nU+6A79: lǔ  # 橹\nU+6A7C: yuán  # 橼\nU+6A80: tán  # 檀\nU+6A84: xí  # 檄\nU+6A8E: qín  # 檎\nU+6A90: yán  # 檐\nU+6A91: léi  # 檑\nU+6A97: bò  # 檗\nU+6A9E: jiě  # 檞\nU+6AA0: qíng  # 檠\nU+6AA9: lǐn  # 檩\nU+6AAB: chá  # 檫\nU+6AAC: méng  # 檬\nU+6AC6: kuí  # 櫆\nU+6B02: bó  # 欂\nU+6B20: qiàn  # 欠\nU+6B21: cì  # 次\nU+6B22: huān  # 欢\nU+6B23: xīn  # 欣\nU+6B24: yú  # 欤\nU+6B27: ōu  # 欧\nU+6B32: yù  # 欲\nU+6B38: āi  # 欸 -> èi\nU+6B39: yī  # 欹 -> qī\nU+6B3A: qī  # 欺\nU+6B3B: chuā  # 欻 -> xū\nU+6B3E: kuǎn  # 款\nU+6B43: shà  # 歃\nU+6B45: yīn  # 歅 -> yān\nU+6B46: xīn  # 歆\nU+6B47: xiē  # 歇\nU+6B49: qiàn  # 歉\nU+6B4C: gē  # 歌\nU+6B59: shè  # 歙 -> xī\nU+6B62: zhǐ  # 止\nU+6B63: zhèng  # 正\nU+6B64: cǐ  # 此\nU+6B65: bù  # 步\nU+6B66: wǔ  # 武\nU+6B67: qí  # 歧\nU+6B6A: wāi  # 歪\nU+6B79: dǎi  # 歹\nU+6B7B: sǐ  # 死\nU+6B7C: jiān  # 歼\nU+6B81: mò  # 殁\nU+6B82: cú  # 殂\nU+6B83: yāng  # 殃\nU+6B84: tiǎn  # 殄\nU+6B86: dài  # 殆\nU+6B87: shāng  # 殇\nU+6B89: xùn  # 殉\nU+6B8A: shū  # 殊\nU+6B8B: cán  # 残\nU+6B8D: piǎo  # 殍\nU+6B92: yǔn  # 殒\nU+6B93: liàn  # 殓\nU+6B96: zhí  # 殖\nU+6B9A: dān  # 殚\nU+6B9B: jí  # 殛\nU+6BA1: bìn  # 殡\nU+6BA3: jìn  # 殣\nU+6BAA: yì  # 殪\nU+6BB3: shū  # 殳\nU+6BB4: ōu  # 殴\nU+6BB5: duàn  # 段\nU+6BB7: yīn  # 殷\nU+6BBF: diàn  # 殿\nU+6BC1: huǐ  # 毁\nU+6BC2: gǔ  # 毂\nU+6BC5: yì  # 毅\nU+6BCB: wú  # 毋\nU+6BCC: guàn  # 毌\nU+6BCD: mǔ  # 母\nU+6BCF: měi  # 每\nU+6BD0: ǎi  # 毐\nU+6BD2: dú  # 毒\nU+6BD3: yù  # 毓\nU+6BD4: bǐ  # 比\nU+6BD5: bì  # 毕\nU+6BD6: bì  # 毖\nU+6BD7: pí  # 毗\nU+6BD9: bì  # 毙\nU+6BDB: máo  # 毛\nU+6BE1: zhān  # 毡\nU+6BEA: mú  # 毪\nU+6BEB: háo  # 毫\nU+6BEF: tǎn  # 毯\nU+6BF3: cuì  # 毳\nU+6BF5: sān  # 毵\nU+6BF9: shū  # 毹\nU+6BFD: jiàn  # 毽\nU+6C05: chǎng  # 氅\nU+6C06: pǔ  # 氆\nU+6C07: lu  # 氇 -> lǔ\nU+6C0D: qú  # 氍\nU+6C0F: shì  # 氏\nU+6C10: dī  # 氐\nU+6C11: mín  # 民\nU+6C13: máng  # 氓 -> méng\nU+6C14: qì  # 气\nU+6C15: piē  # 氕\nU+6C16: nǎi  # 氖\nU+6C18: dāo  # 氘\nU+6C19: xiān  # 氙\nU+6C1A: chuān  # 氚\nU+6C1B: fēn  # 氛\nU+6C1F: fú  # 氟\nU+6C21: dōng  # 氡\nU+6C22: qīng  # 氢\nU+6C24: yīn  # 氤\nU+6C26: hài  # 氦\nU+6C27: yǎng  # 氧\nU+6C28: ān  # 氨\nU+6C29: yà  # 氩\nU+6C2A: kè  # 氪\nU+6C2E: dàn  # 氮\nU+6C2F: lǜ  # 氯\nU+6C30: qíng  # 氰\nU+6C32: yūn  # 氲\nU+6C34: shuǐ  # 水\nU+6C38: yǒng  # 永\nU+6C3E: fàn  # 氾\nU+6C3F: guǐ  # 氿\nU+6C40: tīng  # 汀\nU+6C41: zhī  # 汁\nU+6C42: qiú  # 求\nU+6C46: cuān  # 汆\nU+6C47: huì  # 汇\nU+6C48: diāo  # 汈\nU+6C49: hàn  # 汉\nU+6C4A: chà  # 汊\nU+6C4B: zhuó  # 汋\nU+6C50: xī  # 汐\nU+6C54: qì  # 汔\nU+6C55: shàn  # 汕\nU+6C57: hàn  # 汗\nU+6C5B: xùn  # 汛\nU+6C5C: sì  # 汜\nU+6C5D: rǔ  # 汝\nU+6C5E: gǒng  # 汞\nU+6C5F: jiāng  # 江\nU+6C60: chí  # 池\nU+6C61: wū  # 污\nU+6C64: tāng  # 汤\nU+6C67: qiān  # 汧\nU+6C68: mì  # 汨\nU+6C69: gǔ  # 汩\nU+6C6A: wāng  # 汪\nU+6C6B: jǐng  # 汫\nU+6C6D: ruì  # 汭\nU+6C70: tài  # 汰\nU+6C72: jí  # 汲\nU+6C74: biàn  # 汴\nU+6C76: wèn  # 汶\nU+6C79: xiōng  # 汹\nU+6C7D: qì  # 汽\nU+6C7E: fén  # 汾\nU+6C81: qìn  # 沁\nU+6C82: yí  # 沂\nU+6C83: wò  # 沃\nU+6C84: yún  # 沄\nU+6C85: yuán  # 沅\nU+6C86: hàng  # 沆\nU+6C87: yǎn  # 沇\nU+6C88: shěn  # 沈\nU+6C89: chén  # 沉\nU+6C8C: dùn  # 沌\nU+6C8F: qī  # 沏\nU+6C90: mù  # 沐\nU+6C93: dá  # 沓 -> tà\nU+6C94: miǎn  # 沔\nU+6C98: bǐ  # 沘\nU+6C99: shā  # 沙\nU+6C9A: zhǐ  # 沚\nU+6C9B: pèi  # 沛\nU+6C9F: gōu  # 沟\nU+6CA1: méi  # 没\nU+6CA3: fēng  # 沣\nU+6CA4: ōu  # 沤 -> òu\nU+6CA5: lì  # 沥\nU+6CA6: lún  # 沦\nU+6CA7: cāng  # 沧\nU+6CA8: fēng  # 沨\nU+6CA9: wéi  # 沩\nU+6CAA: hù  # 沪\nU+6CAB: mò  # 沫\nU+6CAD: shù  # 沭\nU+6CAE: jǔ  # 沮\nU+6CB1: tuó  # 沱\nU+6CB3: hé  # 河\nU+6CB8: fèi  # 沸\nU+6CB9: yóu  # 油\nU+6CBA: tián  # 沺\nU+6CBB: zhì  # 治\nU+6CBC: zhǎo  # 沼\nU+6CBD: gū  # 沽\nU+6CBE: zhān  # 沾\nU+6CBF: yán  # 沿\nU+6CC2: jiǒng  # 泂\nU+6CC3: jū  # 泃\nU+6CC4: xiè  # 泄\nU+6CC5: qiú  # 泅\nU+6CC7: jiā  # 泇\nU+6CC9: quán  # 泉\nU+6CCA: pō  # 泊 -> bó\nU+6CCC: mì  # 泌\nU+6CD0: lè  # 泐\nU+6CD3: hóng  # 泓\nU+6CD4: gān  # 泔\nU+6CD5: fǎ  # 法\nU+6CD6: mǎo  # 泖\nU+6CD7: sì  # 泗\nU+6CD9: píng  # 泙 -> pēng\nU+6CDA: cǐ  # 泚\nU+6CDB: fàn  # 泛\nU+6CDC: zhī  # 泜\nU+6CDE: nìng  # 泞\nU+6CE0: líng  # 泠\nU+6CE1: pào  # 泡\nU+6CE2: bō  # 波\nU+6CE3: qì  # 泣\nU+6CE5: ní  # 泥\nU+6CE8: zhù  # 注\nU+6CEA: lèi  # 泪\nU+6CEB: xuàn  # 泫\nU+6CEE: pàn  # 泮\nU+6CEF: mǐn  # 泯\nU+6CF0: tài  # 泰\nU+6CF1: yāng  # 泱\nU+6CF3: yǒng  # 泳\nU+6CF5: bèng  # 泵\nU+6CF7: lóng  # 泷\nU+6CF8: lú  # 泸\nU+6CFA: luò  # 泺\nU+6CFB: xiè  # 泻\nU+6CFC: pō  # 泼\nU+6CFD: zé  # 泽\nU+6CFE: jīng  # 泾\nU+6D01: jié  # 洁\nU+6D04: huí  # 洄\nU+6D07: yīn  # 洇\nU+6D08: wéi  # 洈\nU+6D0B: yáng  # 洋\nU+6D0C: liè  # 洌\nU+6D0E: jì  # 洎\nU+6D11: fú  # 洑\nU+6D12: sǎ  # 洒\nU+6D13: sè  # 洓\nU+6D17: xǐ  # 洗\nU+6D18: kǎo  # 洘\nU+6D19: zhū  # 洙\nU+6D1A: jiàng  # 洚\nU+6D1B: luò  # 洛\nU+6D1E: dòng  # 洞\nU+6D22: yī  # 洢\nU+6D23: mǐ  # 洣\nU+6D25: jīn  # 津\nU+6D27: wěi  # 洧\nU+6D28: xiáo  # 洨\nU+6D2A: hóng  # 洪\nU+6D2B: xù  # 洫\nU+6D2D: kuāng  # 洭\nU+6D2E: táo  # 洮\nU+6D31: ěr  # 洱\nU+6D32: zhōu  # 洲\nU+6D33: rù  # 洳\nU+6D34: píng  # 洴\nU+6D35: xún  # 洵\nU+6D38: guāng  # 洸\nU+6D39: huán  # 洹\nU+6D3A: míng  # 洺\nU+6D3B: huó  # 活\nU+6D3C: wā  # 洼\nU+6D3D: qià  # 洽\nU+6D3E: pài  # 派\nU+6D3F: wū  # 洿\nU+6D41: liú  # 流\nU+6D43: jiā  # 浃\nU+6D45: qiǎn  # 浅\nU+6D46: jiāng  # 浆\nU+6D47: jiāo  # 浇\nU+6D48: zhēn  # 浈\nU+6D49: shī  # 浉\nU+6D4A: zhuó  # 浊\nU+6D4B: cè  # 测\nU+6D4D: huì  # 浍 -> kuài\nU+6D4E: jì  # 济\nU+6D4F: liú  # 浏\nU+6D50: chǎn  # 浐\nU+6D51: hún  # 浑\nU+6D52: hǔ  # 浒\nU+6D53: nóng  # 浓\nU+6D54: xún  # 浔\nU+6D55: jìn  # 浕\nU+6D59: zhè  # 浙\nU+6D5A: jùn  # 浚\nU+6D5B: hán  # 浛\nU+6D5C: bāng  # 浜\nU+6D5E: zhuó  # 浞\nU+6D5F: yóu  # 浟 -> yōu\nU+6D60: xī  # 浠\nU+6D61: bó  # 浡\nU+6D63: huàn  # 浣\nU+6D65: yì  # 浥\nU+6D66: pǔ  # 浦\nU+6D69: hào  # 浩\nU+6D6A: làng  # 浪\nU+6D6C: lǐ  # 浬\nU+6D6D: gēng  # 浭\nU+6D6E: fú  # 浮\nU+6D6F: wú  # 浯\nU+6D70: liàn  # 浰 -> lì\nU+6D72: féng  # 浲\nU+6D74: yù  # 浴\nU+6D77: hǎi  # 海\nU+6D78: jìn  # 浸\nU+6D7C: měi  # 浼\nU+6D82: tú  # 涂\nU+6D84: pīng  # 涄\nU+6D85: niè  # 涅\nU+6D88: xiāo  # 消\nU+6D89: shè  # 涉\nU+6D8C: yǒng  # 涌\nU+6D8D: xiào  # 涍\nU+6D8E: xián  # 涎\nU+6D90: é  # 涐\nU+6D91: sù  # 涑\nU+6D93: juān  # 涓\nU+6D94: cén  # 涔\nU+6D95: tì  # 涕\nU+6D98: sì  # 涘\nU+6D9B: tāo  # 涛\nU+6D9D: lào  # 涝\nU+6D9E: lái  # 涞\nU+6D9F: lián  # 涟\nU+6DA0: wéi  # 涠\nU+6DA1: wō  # 涡\nU+6DA2: yún  # 涢\nU+6DA3: huàn  # 涣\nU+6DA4: dí  # 涤\nU+6DA6: rùn  # 润\nU+6DA7: jiàn  # 涧\nU+6DA8: zhǎng  # 涨\nU+6DA9: sè  # 涩\nU+6DAA: fú  # 涪\nU+6DAB: guàn  # 涫 -> guān\nU+6DAE: shuàn  # 涮\nU+6DAF: yá  # 涯\nU+6DB2: yè  # 液\nU+6DB4: wò  # 涴 -> wǎn\nU+6DB5: hán  # 涵\nU+6DB8: hé  # 涸\nU+6DBF: zhuō  # 涿\nU+6DC0: diàn  # 淀\nU+6DC4: zī  # 淄\nU+6DC5: xī  # 淅\nU+6DC6: xiáo  # 淆\nU+6DC7: qí  # 淇\nU+6DCB: lín  # 淋\nU+6DCC: tǎng  # 淌\nU+6DCF: hào  # 淏\nU+6DD1: shū  # 淑\nU+6DD6: nào  # 淖\nU+6DD8: táo  # 淘\nU+6DD9: cóng  # 淙\nU+6DDC: píng  # 淜\nU+6DDD: féi  # 淝\nU+6DDE: sōng  # 淞\nU+6DDF: tiǎn  # 淟\nU+6DE0: pì  # 淠\nU+6DE1: dàn  # 淡\nU+6DE4: yū  # 淤\nU+6DE6: gàn  # 淦\nU+6DEB: yín  # 淫\nU+6DEC: cuì  # 淬\nU+6DEE: huái  # 淮\nU+6DEF: yù  # 淯\nU+6DF1: shēn  # 深\nU+6DF3: chún  # 淳\nU+6DF4: hū  # 淴\nU+6DF7: hùn  # 混\nU+6DF9: yān  # 淹\nU+6DFB: tiān  # 添\nU+6DFC: miǎo  # 淼\nU+6E05: qīng  # 清\nU+6E0A: yuān  # 渊\nU+6E0C: lù  # 渌\nU+6E0D: zì  # 渍\nU+6E0E: dú  # 渎\nU+6E10: jiàn  # 渐\nU+6E11: miǎn  # 渑\nU+6E14: yú  # 渔\nU+6E17: shèn  # 渗\nU+6E1A: zhǔ  # 渚\nU+6E1D: yú  # 渝\nU+6E1F: tíng  # 渟\nU+6E20: qú  # 渠\nU+6E21: dù  # 渡\nU+6E23: zhā  # 渣\nU+6E24: bó  # 渤\nU+6E25: wò  # 渥\nU+6E29: wēn  # 温\nU+6E2B: xiè  # 渫\nU+6E2D: wèi  # 渭\nU+6E2F: gǎng  # 港\nU+6E30: yǎn  # 渰 -> yān\nU+6E32: xuàn  # 渲\nU+6E34: kě  # 渴\nU+6E38: yóu  # 游\nU+6E3A: miǎo  # 渺\nU+6E3C: měi  # 渼\nU+6E43: pài  # 湃\nU+6E44: méi  # 湄\nU+6E49: tián  # 湉\nU+6E4D: tuān  # 湍\nU+6E4E: miǎn  # 湎\nU+6E51: xū  # 湑 -> xǔ\nU+6E53: pén  # 湓\nU+6E54: jiān  # 湔\nU+6E56: hú  # 湖\nU+6E58: xiāng  # 湘\nU+6E5B: zhàn  # 湛\nU+6E5C: shí  # 湜\nU+6E5D: jiē  # 湝\nU+6E5F: huáng  # 湟\nU+6E63: mǐn  # 湣\nU+6E6B: jiǎo  # 湫 -> qiū\nU+6E6E: yān  # 湮\nU+6E72: yuán  # 湲\nU+6E74: bàn  # 湴\nU+6E7E: wān  # 湾\nU+6E7F: shī  # 湿\nU+6E81: yíng  # 溁\nU+6E83: kuì  # 溃\nU+6E85: jiàn  # 溅\nU+6E86: xù  # 溆\nU+6E87: lóu  # 溇\nU+6E89: gài  # 溉\nU+6E8D: jìn  # 溍\nU+6E8F: táng  # 溏\nU+6E90: yuán  # 源\nU+6E98: kè  # 溘\nU+6E9A: tǎ  # 溚 -> dá\nU+6E9C: liū  # 溜\nU+6E9E: sāo  # 溞\nU+6E9F: míng  # 溟\nU+6EA0: zhà  # 溠\nU+6EA2: yì  # 溢\nU+6EA5: pǔ  # 溥\nU+6EA6: wēi  # 溦\nU+6EA7: lì  # 溧\nU+6EAA: xī  # 溪\nU+6EAF: sù  # 溯\nU+6EB1: qín  # 溱 -> zhēn\nU+6EB2: sōu  # 溲\nU+6EB4: xiù  # 溴\nU+6EB5: yīn  # 溵\nU+6EB6: róng  # 溶\nU+6EB7: hùn  # 溷\nU+6EB9: suò  # 溹\nU+6EBA: nì  # 溺\nU+6EBB: tā  # 溻\nU+6EBD: rù  # 溽\nU+6EC1: chú  # 滁\nU+6EC2: pāng  # 滂\nU+6EC3: wēng  # 滃 -> wěng\nU+6EC6: gé  # 滆\nU+6EC7: diān  # 滇\nU+6EC9: huàng  # 滉\nU+6ECB: zī  # 滋\nU+6ECD: zhì  # 滍\nU+6ECF: fǔ  # 滏\nU+6ED1: huá  # 滑\nU+6ED3: zǐ  # 滓\nU+6ED4: tāo  # 滔\nU+6ED5: téng  # 滕\nU+6ED7: bì  # 滗\nU+6ED8: jiào  # 滘\nU+6EDA: gǔn  # 滚\nU+6EDE: zhì  # 滞\nU+6EDF: yàn  # 滟\nU+6EE0: shè  # 滠\nU+6EE1: mǎn  # 满\nU+6EE2: yíng  # 滢\nU+6EE4: lǜ  # 滤\nU+6EE5: làn  # 滥\nU+6EE6: luán  # 滦\nU+6EE7: yáo  # 滧\nU+6EE8: bīn  # 滨\nU+6EE9: tān  # 滩\nU+6EEA: yù  # 滪\nU+6EEB: xiǔ  # 滫\nU+6EF4: dī  # 滴\nU+6EF9: hū  # 滹\nU+6F02: piāo  # 漂\nU+6F06: qī  # 漆\nU+6F08: jì  # 漈\nU+6F09: lù  # 漉\nU+6F0B: lóng  # 漋\nU+6F0F: lòu  # 漏\nU+6F13: lí  # 漓\nU+6F14: yǎn  # 演\nU+6F15: cáo  # 漕\nU+6F16: jiào  # 漖\nU+6F20: mò  # 漠\nU+6F24: lǎn  # 漤\nU+6F26: chí  # 漦\nU+6F29: xuán  # 漩\nU+6F2A: yī  # 漪\nU+6F2B: màn  # 漫\nU+6F2D: mǎng  # 漭\nU+6F2F: luò  # 漯\nU+6F31: shù  # 漱\nU+6F33: zhāng  # 漳\nU+6F34: zhuàng  # 漴 -> chóng\nU+6F36: huàn  # 漶\nU+6F37: huǒ  # 漷\nU+6F39: yān  # 漹\nU+6F3B: liáo  # 漻\nU+6F3C: cuǐ  # 漼\nU+6F3E: yàng  # 漾\nU+6F46: yíng  # 潆\nU+6F47: xiāo  # 潇\nU+6F4B: liàn  # 潋\nU+6F4D: wéi  # 潍\nU+6F4F: yù  # 潏\nU+6F56: pá  # 潖\nU+6F58: pān  # 潘\nU+6F5C: qián  # 潜\nU+6F5E: lù  # 潞\nU+6F5F: xì  # 潟\nU+6F62: huáng  # 潢\nU+6F66: lǎo  # 潦\nU+6F69: yì  # 潩\nU+6F6D: tán  # 潭\nU+6F6E: cháo  # 潮\nU+6F72: shào  # 潲\nU+6F74: zhū  # 潴\nU+6F75: sǎ  # 潵 -> sàn\nU+6F78: shān  # 潸\nU+6F7A: chán  # 潺\nU+6F7C: tóng  # 潼\nU+6F7D: pū  # 潽\nU+6F7E: lín  # 潾\nU+6F82: chéng  # 澂\nU+6F84: chéng  # 澄\nU+6F88: chè  # 澈\nU+6F89: gǎn  # 澉\nU+6F8C: sī  # 澌\nU+6F8D: shù  # 澍\nU+6F8E: pēng  # 澎 -> péng\nU+6F9B: lǔ  # 澛\nU+6F9C: lán  # 澜\nU+6FA1: zǎo  # 澡\nU+6FA5: xiè  # 澥\nU+6FA7: lǐ  # 澧\nU+6FAA: líng  # 澪\nU+6FAD: yōng  # 澭\nU+6FB3: ào  # 澳\nU+6FB4: huán  # 澴\nU+6FB6: chán  # 澶\nU+6FB9: dàn  # 澹\nU+6FBC: pì  # 澼\nU+6FBD: jù  # 澽\nU+6FC0: jī  # 激\nU+6FC2: lián  # 濂\nU+6FC9: suī  # 濉\nU+6FCB: chǔ  # 濋\nU+6FD1: lài  # 濑\nU+6FD2: bīn  # 濒\nU+6FDE: bì  # 濞\nU+6FE0: háo  # 濠\nU+6FE1: rú  # 濡\nU+6FE9: huò  # 濩\nU+6FEE: pú  # 濮\nU+6FEF: zhuó  # 濯\nU+700C: biāo  # 瀌\nU+700D: chán  # 瀍\nU+7011: pù  # 瀑\nU+7014: gǔ  # 瀔\nU+701A: hàn  # 瀚\nU+701B: yíng  # 瀛\nU+7023: xiè  # 瀣\nU+7031: jì  # 瀱\nU+7035: fèn  # 瀵\nU+7039: yuè  # 瀹\nU+703C: ráng  # 瀼\nU+7048: qú  # 灈\nU+704C: guàn  # 灌\nU+704F: hào  # 灏\nU+705E: bà  # 灞\nU+706B: huǒ  # 火\nU+706D: miè  # 灭\nU+706F: dēng  # 灯\nU+7070: huī  # 灰\nU+7075: líng  # 灵\nU+7076: zào  # 灶\nU+7078: jiǔ  # 灸\nU+707C: zhuó  # 灼\nU+707E: zāi  # 灾\nU+707F: càn  # 灿\nU+7080: yáng  # 炀\nU+7085: jiǒng  # 炅\nU+7086: wén  # 炆\nU+7089: lú  # 炉\nU+708A: chuī  # 炊\nU+708C: kài  # 炌\nU+708E: yán  # 炎\nU+7092: chǎo  # 炒\nU+7094: guì  # 炔 -> quē\nU+7095: kàng  # 炕\nU+7096: dùn  # 炖\nU+7098: xīn  # 炘 -> xìn\nU+7099: zhì  # 炙\nU+709C: wěi  # 炜\nU+709D: qiàng  # 炝\nU+709F: dá  # 炟\nU+70A3: kě  # 炣\nU+70AB: xuàn  # 炫\nU+70AC: jù  # 炬\nU+70AD: tàn  # 炭\nU+70AE: pào  # 炮 -> páo\nU+70AF: jiǒng  # 炯\nU+70B1: tái  # 炱\nU+70B3: bǐng  # 炳\nU+70B7: zhù  # 炷\nU+70B8: zhà  # 炸\nU+70B9: diǎn  # 点\nU+70BB: shí  # 炻\nU+70BC: liàn  # 炼\nU+70BD: chì  # 炽\nU+70C0: hū  # 烀\nU+70C1: shuò  # 烁\nU+70C2: làn  # 烂\nU+70C3: tīng  # 烃\nU+70C8: liè  # 烈\nU+70CA: yáng  # 烊\nU+70D4: tóng  # 烔\nU+70D8: hōng  # 烘\nU+70D9: lào  # 烙\nU+70DB: zhú  # 烛\nU+70DC: xuǎn  # 烜\nU+70DD: zhēng  # 烝\nU+70DF: yān  # 烟\nU+70E0: huí  # 烠\nU+70E4: kǎo  # 烤\nU+70E6: fán  # 烦\nU+70E7: shāo  # 烧\nU+70E8: yè  # 烨\nU+70E9: huì  # 烩\nU+70EB: tàng  # 烫\nU+70EC: jìn  # 烬\nU+70ED: rè  # 热\nU+70EF: xī  # 烯\nU+70F6: tǐng  # 烶\nU+70F7: wán  # 烷\nU+70F9: pēng  # 烹\nU+70FA: lǎng  # 烺\nU+70FB: yàn  # 烻\nU+70FD: fēng  # 烽\nU+7106: juān  # 焆\nU+7109: yān  # 焉\nU+710A: hàn  # 焊\nU+710C: jùn  # 焌 -> qū\nU+7110: wù  # 焐\nU+7113: hán  # 焓\nU+7115: huàn  # 焕\nU+7116: mèn  # 焖\nU+7117: jú  # 焗\nU+7118: dào  # 焘\nU+7119: bèi  # 焙\nU+711A: fén  # 焚\nU+711C: kūn  # 焜\nU+711E: tūn  # 焞\nU+7126: jiāo  # 焦\nU+712F: chāo  # 焯 -> zhuō\nU+7130: yàn  # 焰\nU+7131: yàn  # 焱\nU+7136: rán  # 然\nU+7141: chén  # 煁\nU+7143: kuǐ  # 煃\nU+7145: duàn  # 煅\nU+714A: xuān  # 煊\nU+714B: xīng  # 煋\nU+714C: huáng  # 煌\nU+714E: jiān  # 煎\nU+7153: tuān  # 煓\nU+715C: yù  # 煜\nU+715E: shā  # 煞\nU+715F: wèi  # 煟\nU+7164: méi  # 煤\nU+7166: xù  # 煦\nU+7167: zhào  # 照\nU+7168: wēi  # 煨\nU+716E: zhǔ  # 煮\nU+7172: bāo  # 煲\nU+7173: hú  # 煳\nU+7174: yūn  # 煴\nU+7178: biān  # 煸\nU+717A: tuì  # 煺\nU+717D: shān  # 煽\nU+7184: xī  # 熄\nU+7187: hè  # 熇\nU+718A: xióng  # 熊\nU+718F: xūn  # 熏\nU+7194: róng  # 熔\nU+7198: liū  # 熘\nU+7199: xī  # 熙\nU+719B: biāo  # 熛\nU+719C: cōng  # 熜\nU+719F: shú  # 熟\nU+71A0: yì  # 熠\nU+71A5: tēng  # 熥\nU+71A8: yùn  # 熨\nU+71AC: áo  # 熬 -> āo\nU+71B5: shāng  # 熵\nU+71B9: xī  # 熹\nU+71BB: xī  # 熻\nU+71C3: rán  # 燃\nU+71CA: shēn  # 燊\nU+71CB: jiāo  # 燋\nU+71CE: liáo  # 燎\nU+71CF: yù  # 燏\nU+71D4: fán  # 燔\nU+71D5: yàn  # 燕\nU+71DA: yì  # 燚\nU+71E0: yù  # 燠\nU+71E5: zào  # 燥\nU+71E7: suì  # 燧\nU+71EE: xiè  # 燮\nU+71F9: xiǎn  # 燹\nU+7206: bào  # 爆\nU+7207: ruò  # 爇\nU+7214: xī  # 爔\nU+721A: yuè  # 爚\nU+721D: jué  # 爝\nU+721F: guàn  # 爟\nU+7228: cuàn  # 爨\nU+722A: zhǎo  # 爪\nU+722C: pá  # 爬\nU+7230: yuán  # 爰\nU+7231: ài  # 爱\nU+7235: jué  # 爵\nU+7236: fù  # 父\nU+7237: yé  # 爷\nU+7238: bà  # 爸\nU+7239: diē  # 爹\nU+723B: yáo  # 爻\nU+723D: shuǎng  # 爽\nU+723F: pán  # 爿\nU+7241: kē  # 牁\nU+7242: zāng  # 牂\nU+7247: piàn  # 片\nU+7248: bǎn  # 版\nU+724C: pái  # 牌\nU+724D: dú  # 牍\nU+7252: dié  # 牒\nU+7256: yǒu  # 牖\nU+7259: yá  # 牙\nU+725A: chēng  # 牚\nU+725B: niú  # 牛\nU+725D: pìn  # 牝\nU+725F: móu  # 牟\nU+7261: mǔ  # 牡\nU+7262: láo  # 牢\nU+7264: māng  # 牤\nU+7265: fāng  # 牥\nU+7266: máo  # 牦\nU+7267: mù  # 牧\nU+7269: wù  # 物\nU+726E: jiàn  # 牮\nU+726F: gǔ  # 牯\nU+7272: shēng  # 牲\nU+7275: qiān  # 牵\nU+7279: tè  # 特\nU+727A: xī  # 牺\nU+727B: máng  # 牻\nU+727E: wǔ  # 牾\nU+727F: gù  # 牿\nU+7280: xī  # 犀\nU+7281: lí  # 犁\nU+7284: jī  # 犄\nU+7287: bēn  # 犇\nU+728A: dú  # 犊\nU+728B: jù  # 犋\nU+728D: jiān  # 犍\nU+728F: piān  # 犏\nU+7292: kào  # 犒\nU+729F: jiàng  # 犟\nU+72A8: chōu  # 犨\nU+72AC: quǎn  # 犬\nU+72AF: fàn  # 犯\nU+72B0: qiú  # 犰\nU+72B4: àn  # 犴 -> hān\nU+72B6: zhuàng  # 状\nU+72B7: guǎng  # 犷\nU+72B8: mà  # 犸 -> mǎ\nU+72B9: yóu  # 犹\nU+72C1: yǔn  # 狁\nU+72C2: kuáng  # 狂\nU+72C3: niǔ  # 狃\nU+72C4: dí  # 狄\nU+72C8: bèi  # 狈\nU+72C9: pī  # 狉\nU+72CD: páo  # 狍\nU+72CE: xiá  # 狎\nU+72D0: hú  # 狐\nU+72D2: fèi  # 狒\nU+72D7: gǒu  # 狗\nU+72D9: jū  # 狙\nU+72DD: xiǎn  # 狝\nU+72DE: níng  # 狞\nU+72E0: hěn  # 狠\nU+72E1: jiǎo  # 狡\nU+72E8: róng  # 狨\nU+72E9: shòu  # 狩\nU+72EC: dú  # 独\nU+72ED: xiá  # 狭\nU+72EE: shī  # 狮\nU+72EF: kuài  # 狯\nU+72F0: zhēng  # 狰\nU+72F1: yù  # 狱\nU+72F2: sūn  # 狲\nU+72F3: yú  # 狳\nU+72F4: bì  # 狴\nU+72F7: juàn  # 狷\nU+72F8: lí  # 狸\nU+72FA: yín  # 狺\nU+72FB: suān  # 狻\nU+72FC: láng  # 狼\nU+7301: lì  # 猁\nU+7303: xiǎn  # 猃\nU+7304: jīng  # 猄\nU+7307: xiāo  # 猇\nU+730A: ní  # 猊\nU+730E: liè  # 猎\nU+7315: mí  # 猕\nU+7316: chāng  # 猖\nU+7317: yī  # 猗\nU+731B: měng  # 猛\nU+731C: cāi  # 猜\nU+731D: cù  # 猝\nU+731E: shē  # 猞\nU+7321: luó  # 猡\nU+7322: hú  # 猢\nU+7325: wěi  # 猥\nU+7329: xīng  # 猩\nU+732A: zhū  # 猪\nU+732B: māo  # 猫\nU+732C: wèi  # 猬\nU+732E: xiàn  # 献\nU+732F: tuān  # 猯\nU+7330: yà  # 猰\nU+7331: náo  # 猱\nU+7334: hóu  # 猴\nU+7337: yóu  # 猷\nU+7339: chá  # 猹\nU+733A: yáo  # 猺\nU+733E: huá  # 猾\nU+733F: yuán  # 猿\nU+734D: jìng  # 獍\nU+7350: zhāng  # 獐\nU+7352: áo  # 獒\nU+7357: jué  # 獗\nU+7360: liáo  # 獠\nU+736C: xiè  # 獬\nU+736D: tǎ  # 獭\nU+736F: xūn  # 獯\nU+7374: měng  # 獴 -> méng\nU+737E: huān  # 獾\nU+7383: jué  # 玃\nU+7384: xuán  # 玄\nU+7387: lǜ  # 率 -> shuài\nU+7389: yù  # 玉\nU+738B: wáng  # 王\nU+738E: dīng  # 玎\nU+7391: jī  # 玑\nU+7392: hóng  # 玒\nU+7393: dì  # 玓\nU+7395: gān  # 玕\nU+7396: jiǔ  # 玖\nU+7398: qǐ  # 玘\nU+7399: yú  # 玙\nU+739A: chàng  # 玚\nU+739B: mǎ  # 玛\nU+739E: fū  # 玞\nU+739F: wén  # 玟 -> mín\nU+73A0: jiè  # 玠\nU+73A1: yá  # 玡 -> yà\nU+73A2: bīn  # 玢\nU+73A4: bàng  # 玤\nU+73A5: yuè  # 玥\nU+73A6: jué  # 玦\nU+73A9: wán  # 玩\nU+73AB: méi  # 玫\nU+73AD: pín  # 玭\nU+73AE: wěi  # 玮\nU+73AF: huán  # 环\nU+73B0: xiàn  # 现\nU+73B1: qiāng  # 玱\nU+73B2: líng  # 玲\nU+73B3: dài  # 玳\nU+73B6: píng  # 玶\nU+73B7: diàn  # 玷\nU+73B9: xuán  # 玹\nU+73BA: xǐ  # 玺\nU+73BB: bō  # 玻\nU+73BC: cǐ  # 玼 -> cī\nU+73BF: sháo  # 玿\nU+73C0: pò  # 珀\nU+73C2: kē  # 珂\nU+73C5: shēn  # 珅\nU+73C7: zǔ  # 珇\nU+73C8: jiā  # 珈\nU+73C9: mín  # 珉\nU+73CA: shān  # 珊\nU+73CB: liǔ  # 珋\nU+73CC: bì  # 珌\nU+73CD: zhēn  # 珍\nU+73CF: jué  # 珏\nU+73D0: fà  # 珐\nU+73D1: lóng  # 珑\nU+73D2: jīn  # 珒\nU+73D5: lì  # 珕\nU+73D6: guāng  # 珖\nU+73D9: gǒng  # 珙\nU+73DB: xiù  # 珛\nU+73DD: xǔ  # 珝\nU+73DE: luò  # 珞\nU+73E0: zhū  # 珠\nU+73E2: yín  # 珢\nU+73E3: xún  # 珣\nU+73E5: ěr  # 珥\nU+73E6: xiàng  # 珦\nU+73E7: yáo  # 珧\nU+73E9: háng  # 珩 -> héng\nU+73EA: guī  # 珪\nU+73EB: chōng  # 珫\nU+73ED: bān  # 班\nU+73F0: dāng  # 珰\nU+73F2: huī  # 珲 -> hún\nU+73F5: chéng  # 珵\nU+73F7: wǔ  # 珷\nU+73F8: wú  # 珸\nU+73F9: chéng  # 珹\nU+73FA: jùn  # 珺\nU+73FD: tǐng  # 珽\nU+7400: hán  # 琀\nU+7403: qiú  # 球\nU+7404: xuàn  # 琄\nU+7405: láng  # 琅\nU+7406: lǐ  # 理\nU+7407: xiù  # 琇\nU+7408: fú  # 琈\nU+7409: liú  # 琉\nU+740A: yá  # 琊\nU+740E: jìn  # 琎 -> jīn\nU+740F: liǎn  # 琏\nU+7410: suǒ  # 琐\nU+7414: diàn  # 琔\nU+741A: jū  # 琚\nU+741B: chēn  # 琛\nU+741F: wéi  # 琟\nU+7421: chù  # 琡 -> shū\nU+7422: zuó  # 琢 -> zhuó\nU+7424: chēng  # 琤\nU+7425: hǔ  # 琥\nU+7426: qí  # 琦\nU+7428: kūn  # 琨\nU+742A: qí  # 琪\nU+742B: běng  # 琫\nU+742C: wǎn  # 琬\nU+742D: lù  # 琭\nU+742E: cóng  # 琮\nU+742F: guǎn  # 琯\nU+7430: yǎn  # 琰\nU+7432: bèi  # 琲\nU+7433: lín  # 琳\nU+7434: qín  # 琴\nU+7435: pí  # 琵\nU+7436: pá  # 琶\nU+743C: qióng  # 琼\nU+7440: yǔ  # 瑀\nU+7441: mào  # 瑁\nU+7442: méi  # 瑂\nU+7443: chūn  # 瑃\nU+7444: xuān  # 瑄\nU+7445: tí  # 瑅\nU+7446: xīng  # 瑆\nU+7451: zhuàn  # 瑑\nU+7453: liàn  # 瑓\nU+7454: quán  # 瑔\nU+7455: xiá  # 瑕\nU+7456: duàn  # 瑖\nU+7457: yuàn  # 瑗\nU+7459: nǎo  # 瑙\nU+745A: hú  # 瑚\nU+745B: yīng  # 瑛\nU+745C: yú  # 瑜\nU+745D: huáng  # 瑝\nU+745E: ruì  # 瑞\nU+745F: sè  # 瑟\nU+7462: róng  # 瑢\nU+7467: zhēn  # 瑧\nU+7468: jìn  # 瑨\nU+746C: liú  # 瑬\nU+746D: táng  # 瑭\nU+7470: guī  # 瑰\nU+7471: zhèn  # 瑱 -> tiàn\nU+7473: cuō  # 瑳\nU+7476: yáo  # 瑶\nU+7477: ài  # 瑷\nU+747E: jǐn  # 瑾\nU+7480: cuǐ  # 璀\nU+7481: cōng  # 璁\nU+7483: lí  # 璃\nU+7486: qiú  # 璆\nU+7487: xuán  # 璇\nU+7488: áo  # 璈\nU+748B: zhāng  # 璋\nU+748E: yīng  # 璎\nU+7490: lù  # 璐\nU+7492: dēng  # 璒\nU+7498: lín  # 璘\nU+749C: huáng  # 璜\nU+749E: pú  # 璞\nU+749F: jǐng  # 璟\nU+74A0: fán  # 璠\nU+74A5: jǐng  # 璥\nU+74A7: bì  # 璧\nU+74A8: càn  # 璨\nU+74A9: qú  # 璩\nU+74AA: zǎo  # 璪\nU+74AC: jiǎo  # 璬\nU+74AE: tǎn  # 璮\nU+74B1: sè  # 璱\nU+74B2: suì  # 璲\nU+74BA: wèn  # 璺\nU+74C0: ruǎn  # 瓀\nU+74D2: zàn  # 瓒\nU+74D6: xiāng  # 瓖\nU+74D8: guàn  # 瓘\nU+74DC: guā  # 瓜\nU+74DE: dié  # 瓞\nU+74E0: hù  # 瓠\nU+74E2: piáo  # 瓢\nU+74E3: bàn  # 瓣\nU+74E4: ráng  # 瓤\nU+74E6: wǎ  # 瓦\nU+74EE: wèng  # 瓮\nU+74EF: ōu  # 瓯\nU+74F4: líng  # 瓴\nU+74F6: píng  # 瓶\nU+74F7: cí  # 瓷\nU+74FB: chī  # 瓻\nU+74FF: bù  # 瓿\nU+7504: zhēn  # 甄\nU+750D: méng  # 甍\nU+750F: bèng  # 甏\nU+7511: zèng  # 甑\nU+7513: pì  # 甓\nU+7517: yǎn  # 甗\nU+7518: gān  # 甘\nU+751A: shèn  # 甚\nU+751C: tián  # 甜\nU+751F: shēng  # 生\nU+7521: shēn  # 甡\nU+7525: shēng  # 甥\nU+7526: sū  # 甦\nU+7528: yòng  # 用\nU+7529: shuǎi  # 甩\nU+752A: lù  # 甪\nU+752B: fǔ  # 甫\nU+752C: yǒng  # 甬\nU+752D: béng  # 甭\nU+752F: níng  # 甯\nU+7530: tián  # 田\nU+7531: yóu  # 由\nU+7532: jiǎ  # 甲\nU+7533: shēn  # 申\nU+7535: diàn  # 电\nU+7537: nán  # 男\nU+7538: diān  # 甸 -> diàn\nU+753A: tīng  # 町 -> tǐng\nU+753B: huà  # 画\nU+753E: zāi  # 甾\nU+7540: bì  # 畀\nU+7545: chàng  # 畅\nU+7548: fàn  # 畈\nU+754B: tián  # 畋\nU+754C: jiè  # 界\nU+754E: quǎn  # 畎\nU+754F: wèi  # 畏\nU+7554: pàn  # 畔\nU+7556: wā  # 畖\nU+7559: liú  # 留\nU+755A: běn  # 畚\nU+755B: zhěn  # 畛\nU+755C: chù  # 畜 -> xù\nU+7564: zhì  # 畤\nU+7565: lüè  # 略\nU+7566: qí  # 畦\nU+756A: fān  # 番\nU+756C: shē  # 畬\nU+756F: jùn  # 畯\nU+7572: shē  # 畲\nU+7574: chóu  # 畴\nU+7578: jī  # 畸\nU+7579: wǎn  # 畹\nU+757F: jī  # 畿\nU+7581: liú  # 疁\nU+7583: tuǎn  # 疃\nU+7586: jiāng  # 疆\nU+758D: dàn  # 疍\nU+758F: shū  # 疏\nU+7590: zhì  # 疐\nU+7591: yí  # 疑\nU+7594: dīng  # 疔\nU+7596: jiē  # 疖\nU+7597: liáo  # 疗\nU+7599: gē  # 疙\nU+759A: jiù  # 疚\nU+759D: shàn  # 疝\nU+759F: nüè  # 疟\nU+75A0: lì  # 疠\nU+75A1: yáng  # 疡\nU+75A2: chèn  # 疢\nU+75A3: yóu  # 疣\nU+75A4: bā  # 疤\nU+75A5: jiè  # 疥\nU+75AB: yì  # 疫\nU+75AC: lì  # 疬\nU+75AD: zòng  # 疭\nU+75AE: chuāng  # 疮\nU+75AF: fēng  # 疯\nU+75B0: zhù  # 疰\nU+75B1: pào  # 疱\nU+75B2: pí  # 疲\nU+75B3: gān  # 疳\nU+75B4: kē  # 疴\nU+75B5: cī  # 疵\nU+75B8: dǎn  # 疸\nU+75B9: zhěn  # 疹\nU+75BC: téng  # 疼\nU+75BD: jū  # 疽\nU+75BE: jí  # 疾\nU+75C2: jiā  # 痂\nU+75C3: xuán  # 痃\nU+75C4: zhà  # 痄\nU+75C5: bìng  # 病\nU+75C7: zhèng  # 症\nU+75C8: yōng  # 痈\nU+75C9: jìng  # 痉\nU+75CA: quán  # 痊\nU+75CD: yí  # 痍\nU+75D2: yǎng  # 痒\nU+75D3: chì  # 痓 -> zhì\nU+75D4: zhì  # 痔\nU+75D5: hén  # 痕\nU+75D8: dòu  # 痘\nU+75DB: tòng  # 痛\nU+75DE: pǐ  # 痞\nU+75E2: lì  # 痢\nU+75E3: zhì  # 痣\nU+75E4: cuó  # 痤\nU+75E6: wù  # 痦\nU+75E7: shā  # 痧\nU+75E8: láo  # 痨\nU+75EA: huàn  # 痪\nU+75EB: xián  # 痫\nU+75F0: tán  # 痰\nU+75F1: fèi  # 痱\nU+75F4: chī  # 痴\nU+75F9: bì  # 痹\nU+75FC: gù  # 痼\nU+75FF: wěi  # 痿\nU+7600: yū  # 瘀\nU+7601: cuì  # 瘁\nU+7603: zhú  # 瘃\nU+7605: dān  # 瘅 -> dàn\nU+7606: shèn  # 瘆\nU+760A: hóu  # 瘊\nU+760C: là  # 瘌\nU+7610: yǔ  # 瘐\nU+7615: jiǎ  # 瘕\nU+7617: yì  # 瘗\nU+7618: lòu  # 瘘\nU+7619: sào  # 瘙\nU+761B: chì  # 瘛\nU+761F: wēn  # 瘟\nU+7620: jí  # 瘠\nU+7622: bān  # 瘢\nU+7624: liú  # 瘤\nU+7625: chài  # 瘥\nU+7626: shòu  # 瘦\nU+7629: dā  # 瘩 -> dá\nU+762A: biě  # 瘪 -> biē\nU+762B: tān  # 瘫\nU+762D: biāo  # 瘭\nU+7630: luǒ  # 瘰\nU+7633: chōu  # 瘳\nU+7634: zhàng  # 瘴\nU+7635: zhài  # 瘵\nU+7638: qué  # 瘸\nU+763C: mò  # 瘼\nU+763E: yǐn  # 瘾\nU+763F: yǐng  # 瘿\nU+7640: huáng  # 癀\nU+7643: lóng  # 癃\nU+764C: ái  # 癌\nU+764D: bān  # 癍\nU+7654: yì  # 癔\nU+7656: pǐ  # 癖\nU+7657: lěi  # 癗\nU+765C: diàn  # 癜\nU+765E: lài  # 癞\nU+7663: xuǎn  # 癣\nU+766B: diān  # 癫\nU+766F: qú  # 癯\nU+7678: guǐ  # 癸\nU+767B: dēng  # 登\nU+767D: bái  # 白\nU+767E: bǎi  # 百\nU+767F: qié  # 癿\nU+7682: zào  # 皂\nU+7684: de  # 的\nU+7686: jiē  # 皆\nU+7687: huáng  # 皇\nU+7688: guī  # 皈\nU+768B: gāo  # 皋\nU+768E: jiǎo  # 皎\nU+7691: ái  # 皑\nU+7693: hào  # 皓\nU+7695: bì  # 皕\nU+7696: wǎn  # 皖\nU+7699: xī  # 皙\nU+769B: xiǎo  # 皛\nU+769E: hào  # 皞\nU+76A4: pó  # 皤\nU+76A6: jiǎo  # 皦\nU+76AD: jiào  # 皭\nU+76AE: pí  # 皮\nU+76B1: zhòu  # 皱\nU+76B2: jūn  # 皲\nU+76B4: cūn  # 皴\nU+76BF: mǐn  # 皿\nU+76C2: yú  # 盂\nU+76C5: zhōng  # 盅\nU+76C6: pén  # 盆\nU+76C8: yíng  # 盈\nU+76C9: hé  # 盉\nU+76CA: yì  # 益\nU+76CD: hé  # 盍\nU+76CE: àng  # 盎\nU+76CF: zhǎn  # 盏\nU+76D0: yán  # 盐\nU+76D1: jiān  # 监\nU+76D2: hé  # 盒\nU+76D4: kuī  # 盔\nU+76D6: gài  # 盖\nU+76D7: dào  # 盗\nU+76D8: pán  # 盘\nU+76DB: shèng  # 盛\nU+76DF: méng  # 盟\nU+76E5: guàn  # 盥\nU+76E6: ān  # 盦\nU+76EE: mù  # 目\nU+76EF: dīng  # 盯\nU+76F1: xū  # 盱\nU+76F2: máng  # 盲\nU+76F4: zhí  # 直\nU+76F7: tián  # 盷 -> xián\nU+76F8: xiāng  # 相\nU+76F9: dǔn  # 盹\nU+76FC: pàn  # 盼\nU+76FE: dùn  # 盾\nU+7701: shěng  # 省\nU+7704: miǎn  # 眄\nU+7707: miǎo  # 眇\nU+7708: dān  # 眈\nU+7709: méi  # 眉\nU+770A: mào  # 眊\nU+770B: kàn  # 看\nU+770D: kōu  # 眍\nU+7719: yí  # 眙\nU+771A: shěng  # 眚\nU+771F: zhēn  # 真\nU+7720: mián  # 眠\nU+7722: yuān  # 眢\nU+7726: zì  # 眦\nU+7728: zhǎ  # 眨\nU+7729: xuàn  # 眩\nU+772C: lóng  # 眬\nU+772D: suī  # 眭 -> guì\nU+772F: mī  # 眯\nU+7735: chī  # 眵\nU+7736: kuàng  # 眶\nU+7737: juàn  # 眷\nU+7738: móu  # 眸\nU+773A: tiào  # 眺\nU+773C: yǎn  # 眼\nU+7740: zhe  # 着\nU+7741: zhēng  # 睁\nU+7743: suō  # 睃\nU+7744: shào  # 睄 -> qiáo\nU+7747: dì  # 睇\nU+774E: xī  # 睎\nU+7750: lài  # 睐\nU+7751: jiǎn  # 睑\nU+775A: yá  # 睚\nU+775B: jīng  # 睛\nU+7761: shuì  # 睡\nU+7762: suī  # 睢 -> huī\nU+7763: dū  # 督\nU+7765: pì  # 睥 -> bì\nU+7766: mù  # 睦\nU+7768: nì  # 睨\nU+776B: jié  # 睫\nU+776C: cǎi  # 睬\nU+7779: dǔ  # 睹\nU+777D: kuí  # 睽\nU+777E: gāo  # 睾\nU+777F: ruì  # 睿\nU+7780: mào  # 瞀\nU+7784: miáo  # 瞄\nU+7785: chǒu  # 瞅\nU+778B: chēn  # 瞋\nU+778C: kē  # 瞌\nU+778D: sǒu  # 瞍\nU+778E: xiā  # 瞎\nU+7791: míng  # 瞑\nU+7792: mán  # 瞒\nU+779F: piǎo  # 瞟\nU+77A0: chēng  # 瞠\nU+77A2: méng  # 瞢\nU+77A5: piē  # 瞥\nU+77A7: qiáo  # 瞧\nU+77A9: zhǔ  # 瞩\nU+77AA: dèng  # 瞪\nU+77AB: shěn  # 瞫\nU+77AC: shùn  # 瞬\nU+77AD: liǎo  # 瞭\nU+77B0: kàn  # 瞰\nU+77B3: tóng  # 瞳\nU+77B5: lín  # 瞵\nU+77BB: zhān  # 瞻\nU+77BD: gǔ  # 瞽\nU+77BF: qú  # 瞿\nU+77CD: jué  # 矍\nU+77D7: chù  # 矗\nU+77DB: máo  # 矛\nU+77DC: jīn  # 矜\nU+77DE: yù  # 矞\nU+77E2: shǐ  # 矢\nU+77E3: yǐ  # 矣\nU+77E5: zhī  # 知\nU+77E7: shěn  # 矧\nU+77E9: jǔ  # 矩\nU+77EB: jiǎo  # 矫\nU+77EC: cuó  # 矬\nU+77ED: duǎn  # 短\nU+77EE: ǎi  # 矮\nU+77F0: zēng  # 矰\nU+77F3: shí  # 石\nU+77F6: jī  # 矶\nU+77F8: gān  # 矸\nU+77FB: kū  # 矻\nU+77FC: gāng  # 矼\nU+77FE: fán  # 矾\nU+77FF: kuàng  # 矿\nU+7800: dàng  # 砀\nU+7801: mǎ  # 码\nU+7802: shā  # 砂\nU+7804: jué  # 砄\nU+7806: fū  # 砆\nU+7809: huò  # 砉 -> xū\nU+780C: qì  # 砌\nU+780D: kǎn  # 砍\nU+7811: yà  # 砑\nU+7812: pī  # 砒\nU+7814: yán  # 研\nU+7816: zhuān  # 砖\nU+7817: chē  # 砗\nU+7818: dùn  # 砘\nU+781A: yàn  # 砚\nU+781C: fēng  # 砜\nU+781D: fá  # 砝 -> fǎ\nU+781F: zhǎ  # 砟\nU+7820: jū  # 砠\nU+7823: tuó  # 砣\nU+7825: dǐ  # 砥\nU+7827: zhēn  # 砧\nU+782B: zhù  # 砫\nU+782C: lá  # 砬 -> lì\nU+782D: biān  # 砭\nU+782E: nǔ  # 砮\nU+7830: pēng  # 砰\nU+7834: pò  # 破\nU+7835: bō  # 砵\nU+7837: shēn  # 砷\nU+7838: zá  # 砸\nU+7839: ài  # 砹\nU+783A: lì  # 砺\nU+783B: lóng  # 砻\nU+783C: tóng  # 砼\nU+783E: lì  # 砾\nU+7840: chǔ  # 础\nU+7841: kēng  # 硁\nU+7845: guī  # 硅\nU+7847: náo  # 硇\nU+784A: wěi  # 硊\nU+784C: gè  # 硌 -> luò\nU+784D: xiàn  # 硍 -> kèn\nU+784E: xíng  # 硎\nU+7850: dòng  # 硐\nU+7852: xī  # 硒\nU+7854: hóng  # 硔\nU+7855: shuò  # 硕\nU+7856: xiá  # 硖\nU+7857: qiāo  # 硗\nU+7859: wéi  # 硙\nU+785A: qiáo  # 硚\nU+785D: xiāo  # 硝\nU+786A: wò  # 硪\nU+786B: liú  # 硫\nU+786C: yìng  # 硬\nU+786D: máng  # 硭\nU+786E: què  # 确\nU+787C: péng  # 硼\nU+787F: kōng  # 硿\nU+7883: qìng  # 碃\nU+7887: dìng  # 碇\nU+7888: mín  # 碈\nU+7889: diāo  # 碉\nU+788C: lù  # 碌\nU+788D: ài  # 碍\nU+788E: suì  # 碎\nU+788F: què  # 碏\nU+7891: bēi  # 碑\nU+7893: duì  # 碓\nU+7897: wǎn  # 碗\nU+7898: diǎn  # 碘\nU+789A: bèi  # 碚\nU+789B: qì  # 碛\nU+789C: chěn  # 碜\nU+789F: dié  # 碟\nU+78A1: dú  # 碡 -> zhóu\nU+78A3: jié  # 碣\nU+78A5: biǎn  # 碥\nU+78A7: bì  # 碧\nU+78A8: wèi  # 碨 -> wěi\nU+78B0: pèng  # 碰\nU+78B1: jiǎn  # 碱\nU+78B2: dì  # 碲\nU+78B3: tàn  # 碳\nU+78B4: chá  # 碴\nU+78B6: qì  # 碶\nU+78B9: xuàn  # 碹\nU+78BE: niǎn  # 碾\nU+78C1: cí  # 磁\nU+78C5: bàng  # 磅\nU+78C9: sǎng  # 磉\nU+78CA: lěi  # 磊\nU+78CB: cuō  # 磋\nU+78CF: lián  # 磏\nU+78D0: pán  # 磐\nU+78D4: zhé  # 磔\nU+78D5: kē  # 磕\nU+78D9: gǔn  # 磙\nU+78DC: qì  # 磜\nU+78E1: kàn  # 磡\nU+78E8: mó  # 磨\nU+78EC: qìng  # 磬\nU+78F2: qú  # 磲\nU+78F4: dèng  # 磴\nU+78F7: lín  # 磷\nU+78F9: tán  # 磹 -> diàn\nU+78FB: pán  # 磻\nU+7901: jiāo  # 礁\nU+7905: dūn  # 礅\nU+790C: léi  # 礌\nU+7913: jiāng  # 礓\nU+791E: méng  # 礞\nU+7934: bó  # 礴\nU+7935: shuāng  # 礵\nU+793A: shì  # 示\nU+793C: lǐ  # 礼\nU+793E: shè  # 社\nU+7940: sì  # 祀\nU+7941: qí  # 祁\nU+7943: mà  # 祃\nU+7946: xiān  # 祆\nU+7947: qí  # 祇 -> zhǐ\nU+7948: qí  # 祈\nU+7949: zhǐ  # 祉\nU+794A: bēng  # 祊\nU+794B: duì  # 祋\nU+794E: yī  # 祎\nU+794F: shí  # 祏\nU+7950: yòu  # 祐\nU+7953: fú  # 祓\nU+7955: mì  # 祕\nU+7956: zǔ  # 祖\nU+7957: zhī  # 祗\nU+795A: zuò  # 祚\nU+795B: qū  # 祛\nU+795C: hù  # 祜\nU+795D: zhù  # 祝\nU+795E: shén  # 神\nU+795F: suì  # 祟\nU+7960: cí  # 祠\nU+7962: mí  # 祢\nU+7965: xiáng  # 祥\nU+7967: tiāo  # 祧\nU+7968: piào  # 票\nU+796D: jì  # 祭\nU+796F: zhēn  # 祯\nU+7972: jìn  # 祲\nU+7977: dǎo  # 祷\nU+7978: huò  # 祸\nU+797A: qí  # 祺\nU+797C: guàn  # 祼\nU+797E: líng  # 祾\nU+7980: bǐng  # 禀\nU+7981: jìn  # 禁 -> jīn\nU+7984: lù  # 禄\nU+7985: chán  # 禅\nU+798A: xì  # 禊\nU+798B: yīn  # 禋\nU+798F: fú  # 福\nU+7992: xiǎn  # 禒\nU+7994: zhī  # 禔 -> tí\nU+7998: dì  # 禘\nU+799A: zhuó  # 禚\nU+799B: zhēn  # 禛\nU+79A4: xuān  # 禤\nU+79A7: xǐ  # 禧\nU+79B3: ráng  # 禳\nU+79B9: yǔ  # 禹\nU+79BA: yú  # 禺\nU+79BB: lí  # 离\nU+79BD: qín  # 禽\nU+79BE: hé  # 禾\nU+79C0: xiù  # 秀\nU+79C1: sī  # 私\nU+79C3: tū  # 秃\nU+79C6: gǎn  # 秆\nU+79C9: bǐng  # 秉\nU+79CB: qiū  # 秋\nU+79CD: zhǒng  # 种\nU+79D1: kē  # 科\nU+79D2: miǎo  # 秒\nU+79D5: bǐ  # 秕\nU+79D8: mì  # 秘\nU+79DF: zū  # 租\nU+79E3: mò  # 秣\nU+79E4: chèng  # 秤\nU+79E6: qín  # 秦\nU+79E7: yāng  # 秧\nU+79E9: zhì  # 秩\nU+79EB: shú  # 秫\nU+79EC: jù  # 秬\nU+79ED: zǐ  # 秭\nU+79EF: jī  # 积\nU+79F0: chēng  # 称\nU+79F8: jiē  # 秸\nU+79FB: yí  # 移\nU+79FD: huì  # 秽\nU+79FE: nóng  # 秾\nU+7A00: xī  # 稀\nU+7A02: láng  # 稂\nU+7A03: fū  # 稃\nU+7A06: lǚ  # 稆\nU+7A0B: chéng  # 程\nU+7A0C: tú  # 稌\nU+7A0D: shāo  # 稍\nU+7A0E: shuì  # 税\nU+7A11: lù  # 稑\nU+7A14: rěn  # 稔\nU+7A17: bài  # 稗\nU+7A19: zhī  # 稙\nU+7A1A: zhì  # 稚\nU+7A1E: kē  # 稞\nU+7A20: chóu  # 稠\nU+7A23: sū  # 稣\nU+7A33: wěn  # 稳\nU+7A37: jì  # 稷\nU+7A39: zhěn  # 稹\nU+7A3B: dào  # 稻\nU+7A3C: jià  # 稼\nU+7A3D: jī  # 稽\nU+7A3F: gǎo  # 稿\nU+7A44: jì  # 穄\nU+7A46: mù  # 穆\nU+7A51: sè  # 穑\nU+7A57: suì  # 穗\nU+7A59: pú  # 穙\nU+7A5C: zhǒng  # 穜 -> tóng\nU+7A5F: suì  # 穟\nU+7A70: ráng  # 穰\nU+7A74: xué  # 穴\nU+7A76: jiū  # 究\nU+7A77: qióng  # 穷\nU+7A78: xī  # 穸\nU+7A79: qióng  # 穹\nU+7A7A: kōng  # 空\nU+7A7F: chuān  # 穿\nU+7A80: zhūn  # 窀\nU+7A81: tū  # 突\nU+7A83: qiè  # 窃\nU+7A84: zhǎi  # 窄\nU+7A85: yǎo  # 窅\nU+7A88: yǎo  # 窈\nU+7A8A: wā  # 窊\nU+7A8D: qiào  # 窍\nU+7A8E: diào  # 窎\nU+7A91: yáo  # 窑\nU+7A92: zhì  # 窒\nU+7A95: tiǎo  # 窕\nU+7A96: jiào  # 窖\nU+7A97: chuāng  # 窗\nU+7A98: jiǒng  # 窘\nU+7A9C: cuàn  # 窜\nU+7A9D: wō  # 窝\nU+7A9F: kū  # 窟\nU+7AA0: kē  # 窠\nU+7AA3: sū  # 窣\nU+7AA5: kuī  # 窥\nU+7AA6: dòu  # 窦\nU+7AA8: xūn  # 窨 -> yìn\nU+7AAC: yú  # 窬\nU+7AAD: jù  # 窭\nU+7AB3: yǔ  # 窳\nU+7AB8: xī  # 窸\nU+7ABF: lóng  # 窿\nU+7ACB: lì  # 立\nU+7AD1: hóng  # 竑\nU+7AD6: shù  # 竖\nU+7AD8: qǔ  # 竘\nU+7AD9: zhàn  # 站\nU+7ADE: jìng  # 竞\nU+7ADF: jìng  # 竟\nU+7AE0: zhāng  # 章\nU+7AE3: jùn  # 竣\nU+7AE5: tóng  # 童\nU+7AE6: sǒng  # 竦\nU+7AEB: jìng  # 竫\nU+7AED: jié  # 竭\nU+7AEF: duān  # 端\nU+7AF9: zhú  # 竹\nU+7AFA: zhú  # 竺\nU+7AFD: yú  # 竽\nU+7AFF: gān  # 竿\nU+7B03: dǔ  # 笃\nU+7B04: jī  # 笄\nU+7B06: bā  # 笆\nU+7B08: jí  # 笈\nU+7B0A: zhào  # 笊\nU+7B0B: sǔn  # 笋\nU+7B0F: hù  # 笏\nU+7B11: xiào  # 笑\nU+7B14: bǐ  # 笔\nU+7B15: jiǎn  # 笕\nU+7B19: shēng  # 笙\nU+7B1B: dí  # 笛\nU+7B1E: chī  # 笞\nU+7B20: lì  # 笠\nU+7B24: tiáo  # 笤\nU+7B25: sì  # 笥\nU+7B26: fú  # 符\nU+7B28: bèn  # 笨\nU+7B2A: dá  # 笪\nU+7B2B: zǐ  # 笫\nU+7B2C: dì  # 第\nU+7B2E: zé  # 笮 -> zuó\nU+7B2F: nú  # 笯\nU+7B31: gǒu  # 笱\nU+7B33: jiā  # 笳\nU+7B38: pǒ  # 笸\nU+7B3A: jiān  # 笺\nU+7B3C: lóng  # 笼\nU+7B3E: biān  # 笾\nU+7B40: guì  # 筀\nU+7B45: xiǎn  # 筅\nU+7B47: qióng  # 筇\nU+7B49: děng  # 等\nU+7B4B: jīn  # 筋\nU+7B4C: quán  # 筌\nU+7B4F: fá  # 筏\nU+7B50: kuāng  # 筐\nU+7B51: zhù  # 筑\nU+7B52: tǒng  # 筒\nU+7B54: dá  # 答\nU+7B56: cè  # 策\nU+7B58: kòu  # 筘\nU+7B5A: bì  # 筚\nU+7B5B: shāi  # 筛\nU+7B5C: dāng  # 筜\nU+7B5D: zhēng  # 筝\nU+7B60: yún  # 筠\nU+7B62: pá  # 筢\nU+7B64: láng  # 筤\nU+7B65: jǔ  # 筥\nU+7B66: guǎn  # 筦\nU+7B6E: shì  # 筮\nU+7B71: xiǎo  # 筱\nU+7B72: shāo  # 筲\nU+7B75: yán  # 筵\nU+7B76: gào  # 筶\nU+7B77: kuài  # 筷\nU+7B79: chóu  # 筹\nU+7B7B: gàng  # 筻\nU+7B7C: yún  # 筼\nU+7B7E: qiān  # 签\nU+7B80: jiǎn  # 简\nU+7B85: bì  # 箅\nU+7B8D: gū  # 箍\nU+7B90: qìng  # 箐\nU+7B93: lù  # 箓\nU+7B94: bó  # 箔\nU+7B95: jī  # 箕\nU+7B96: lín  # 箖\nU+7B97: suàn  # 算\nU+7B9C: kōng  # 箜\nU+7BA1: guǎn  # 管\nU+7BA2: yuān  # 箢 -> wǎn\nU+7BA6: zé  # 箦\nU+7BA7: qiè  # 箧\nU+7BA8: tuò  # 箨\nU+7BA9: luó  # 箩\nU+7BAA: dān  # 箪\nU+7BAB: xiāo  # 箫\nU+7BAC: ruò  # 箬\nU+7BAD: jiàn  # 箭\nU+7BB1: xiāng  # 箱\nU+7BB4: zhēn  # 箴\nU+7BB8: zhù  # 箸\nU+7BC1: huáng  # 篁\nU+7BC6: zhuàn  # 篆\nU+7BC7: piān  # 篇\nU+7BCC: hóu  # 篌\nU+7BD1: kuì  # 篑\nU+7BD3: lǒu  # 篓\nU+7BD9: gāo  # 篙\nU+7BDA: fěi  # 篚\nU+7BDD: gōu  # 篝\nU+7BE1: cuàn  # 篡\nU+7BE5: lì  # 篥\nU+7BE6: bì  # 篦\nU+7BEA: chí  # 篪\nU+7BEE: lán  # 篮\nU+7BEF: jiān  # 篯 -> jiǎn\nU+7BF1: lí  # 篱\nU+7BF7: péng  # 篷\nU+7BFC: dōu  # 篼\nU+7BFE: miè  # 篾\nU+7C03: yí  # 簃\nU+7C07: cù  # 簇\nU+7C09: zào  # 簉\nU+7C0B: guǐ  # 簋\nU+7C0C: sù  # 簌\nU+7C0F: lù  # 簏\nU+7C15: lè  # 簕\nU+7C16: duàn  # 簖\nU+7C1D: liáo  # 簝\nU+7C1F: diàn  # 簟\nU+7C20: fǔ  # 簠\nU+7C27: huáng  # 簧\nU+7C2A: zān  # 簪\nU+7C30: pái  # 簰\nU+7C38: bǒ  # 簸 -> bò\nU+7C3F: bù  # 簿\nU+7C40: zhòu  # 籀\nU+7C41: lài  # 籁\nU+7C4D: jí  # 籍\nU+7C65: yuè  # 籥\nU+7C73: mǐ  # 米\nU+7C74: dí  # 籴\nU+7C7B: lèi  # 类\nU+7C7C: xiān  # 籼\nU+7C7D: zǐ  # 籽\nU+7C89: fěn  # 粉\nU+7C91: bā  # 粑\nU+7C92: lì  # 粒\nU+7C95: pò  # 粕\nU+7C97: cū  # 粗\nU+7C98: zhān  # 粘 -> nián\nU+7C9C: tiào  # 粜\nU+7C9D: lì  # 粝\nU+7C9E: xī  # 粞\nU+7C9F: sù  # 粟\nU+7CA2: zī  # 粢\nU+7CA4: yuè  # 粤\nU+7CA5: zhōu  # 粥\nU+7CAA: fèn  # 粪\nU+7CAE: liáng  # 粮\nU+7CB1: liáng  # 粱\nU+7CB2: càn  # 粲\nU+7CB3: jīng  # 粳\nU+7CB9: cuì  # 粹\nU+7CBC: lín  # 粼\nU+7CBD: zòng  # 粽\nU+7CBE: jīng  # 精\nU+7CBF: guǒ  # 粿\nU+7CC1: sǎn  # 糁\nU+7CC5: róu  # 糅\nU+7CC7: hóu  # 糇\nU+7CC8: xǔ  # 糈\nU+7CCA: hú  # 糊 -> hū\nU+7CCC: zān  # 糌\nU+7CCD: cí  # 糍\nU+7CD2: bèi  # 糒\nU+7CD5: gāo  # 糕\nU+7CD6: táng  # 糖\nU+7CD7: qiǔ  # 糗\nU+7CD9: cāo  # 糙\nU+7CDC: mí  # 糜\nU+7CDF: zāo  # 糟\nU+7CE0: kāng  # 糠\nU+7CE8: jiàng  # 糨\nU+7CEF: nuò  # 糯\nU+7CF5: niè  # 糵\nU+7CFB: xì  # 系\nU+7D0A: wěn  # 紊\nU+7D20: sù  # 素\nU+7D22: suǒ  # 索\nU+7D27: jǐn  # 紧\nU+7D2B: zǐ  # 紫\nU+7D2F: lèi  # 累 -> léi\nU+7D5C: jié  # 絜 -> xié\nU+7D6E: xù  # 絮\nU+7D77: zhí  # 絷\nU+7DA6: qí  # 綦\nU+7DAE: qǐ  # 綮 -> qìng\nU+7E20: hú  # 縠\nU+7E22: téng  # 縢\nU+7E3B: mí  # 縻\nU+7E41: fán  # 繁\nU+7E44: yī  # 繄\nU+7E47: yáo  # 繇\nU+7E82: zuǎn  # 纂\nU+7E9B: dào  # 纛\nU+7EA0: jiū  # 纠\nU+7EA1: yū  # 纡\nU+7EA2: hóng  # 红\nU+7EA3: zhòu  # 纣\nU+7EA4: xiān  # 纤\nU+7EA5: gē  # 纥 -> hé\nU+7EA6: yuē  # 约\nU+7EA7: jí  # 级\nU+7EA8: wán  # 纨\nU+7EA9: kuàng  # 纩\nU+7EAA: jì  # 纪\nU+7EAB: rèn  # 纫\nU+7EAC: wěi  # 纬\nU+7EAD: yún  # 纭\nU+7EAE: hóng  # 纮\nU+7EAF: chún  # 纯\nU+7EB0: pī  # 纰\nU+7EB1: shā  # 纱\nU+7EB2: gāng  # 纲\nU+7EB3: nà  # 纳\nU+7EB4: rèn  # 纴\nU+7EB5: zòng  # 纵\nU+7EB6: lún  # 纶\nU+7EB7: fēn  # 纷\nU+7EB8: zhǐ  # 纸\nU+7EB9: wén  # 纹\nU+7EBA: fǎng  # 纺\nU+7EBB: zhù  # 纻\nU+7EBC: zhèn  # 纼\nU+7EBD: niǔ  # 纽\nU+7EBE: shū  # 纾\nU+7EBF: xiàn  # 线\nU+7EC0: gàn  # 绀\nU+7EC1: xiè  # 绁\nU+7EC2: fú  # 绂\nU+7EC3: liàn  # 练\nU+7EC4: zǔ  # 组\nU+7EC5: shēn  # 绅\nU+7EC6: xì  # 细\nU+7EC7: zhī  # 织\nU+7EC8: zhōng  # 终\nU+7EC9: zhòu  # 绉\nU+7ECA: bàn  # 绊\nU+7ECB: fú  # 绋\nU+7ECC: chù  # 绌\nU+7ECD: shào  # 绍\nU+7ECE: yì  # 绎\nU+7ECF: jīng  # 经\nU+7ED0: dài  # 绐\nU+7ED1: bǎng  # 绑\nU+7ED2: róng  # 绒\nU+7ED3: jié  # 结\nU+7ED4: kù  # 绔\nU+7ED5: rào  # 绕\nU+7ED6: dié  # 绖\nU+7ED7: háng  # 绗\nU+7ED8: huì  # 绘\nU+7ED9: gěi  # 给\nU+7EDA: xuàn  # 绚\nU+7EDB: jiàng  # 绛\nU+7EDC: luò  # 络\nU+7EDD: jué  # 绝\nU+7EDE: jiǎo  # 绞\nU+7EDF: tǒng  # 统\nU+7EE0: gěng  # 绠\nU+7EE1: xiāo  # 绡\nU+7EE2: juàn  # 绢\nU+7EE3: xiù  # 绣\nU+7EE4: xì  # 绤\nU+7EE5: suí  # 绥\nU+7EE6: tāo  # 绦\nU+7EE7: jì  # 继\nU+7EE8: tí  # 绨\nU+7EE9: jì  # 绩\nU+7EEA: xù  # 绪\nU+7EEB: líng  # 绫\nU+7EED: xù  # 续\nU+7EEE: qǐ  # 绮\nU+7EEF: fēi  # 绯\nU+7EF0: chuò  # 绰\nU+7EF1: shàng  # 绱\nU+7EF2: gǔn  # 绲\nU+7EF3: shéng  # 绳\nU+7EF4: wéi  # 维\nU+7EF5: mián  # 绵\nU+7EF6: shòu  # 绶\nU+7EF7: bēng  # 绷\nU+7EF8: chóu  # 绸\nU+7EF9: táo  # 绹\nU+7EFA: liǔ  # 绺\nU+7EFB: quǎn  # 绻\nU+7EFC: zōng  # 综\nU+7EFD: zhàn  # 绽\nU+7EFE: wǎn  # 绾\nU+7EFF: lǜ  # 绿\nU+7F00: zhuì  # 缀\nU+7F01: zī  # 缁\nU+7F02: kè  # 缂\nU+7F03: xiāng  # 缃\nU+7F04: jiān  # 缄\nU+7F05: miǎn  # 缅\nU+7F06: lǎn  # 缆\nU+7F07: tí  # 缇\nU+7F08: miǎo  # 缈\nU+7F09: jī  # 缉\nU+7F0A: yūn  # 缊 -> yùn\nU+7F0C: sī  # 缌\nU+7F0E: duàn  # 缎\nU+7F10: xiàn  # 缐\nU+7F11: gōu  # 缑\nU+7F12: zhuì  # 缒\nU+7F13: huǎn  # 缓\nU+7F14: dì  # 缔\nU+7F15: lǚ  # 缕\nU+7F16: biān  # 编\nU+7F17: mín  # 缗\nU+7F18: yuán  # 缘\nU+7F19: jìn  # 缙\nU+7F1A: fù  # 缚\nU+7F1B: rù  # 缛\nU+7F1C: zhěn  # 缜\nU+7F1D: fèng  # 缝 -> féng\nU+7F1E: cuī  # 缞\nU+7F1F: gǎo  # 缟\nU+7F20: chán  # 缠\nU+7F21: lí  # 缡\nU+7F22: yì  # 缢\nU+7F23: jiān  # 缣\nU+7F24: bīn  # 缤\nU+7F25: piāo  # 缥 -> piǎo\nU+7F26: màn  # 缦\nU+7F27: léi  # 缧\nU+7F28: yīng  # 缨\nU+7F29: suō  # 缩\nU+7F2A: móu  # 缪\nU+7F2B: sāo  # 缫\nU+7F2C: xié  # 缬\nU+7F2D: liáo  # 缭\nU+7F2E: shàn  # 缮\nU+7F2F: zēng  # 缯\nU+7F30: jiāng  # 缰\nU+7F31: qiǎn  # 缱\nU+7F32: qiāo  # 缲\nU+7F33: huán  # 缳\nU+7F34: jiǎo  # 缴\nU+7F35: zuǎn  # 缵\nU+7F36: fǒu  # 缶\nU+7F38: gāng  # 缸\nU+7F3A: quē  # 缺\nU+7F42: yīng  # 罂\nU+7F44: qìng  # 罄\nU+7F45: xià  # 罅\nU+7F4D: léi  # 罍\nU+7F50: guàn  # 罐\nU+7F51: wǎng  # 网\nU+7F54: wǎng  # 罔\nU+7F55: hǎn  # 罕\nU+7F57: luó  # 罗\nU+7F58: fú  # 罘\nU+7F5A: fá  # 罚\nU+7F5F: gǔ  # 罟\nU+7F61: gāng  # 罡\nU+7F62: bà  # 罢\nU+7F68: yǎn  # 罨\nU+7F69: zhào  # 罩\nU+7F6A: zuì  # 罪\nU+7F6E: zhì  # 置\nU+7F71: lǎn  # 罱\nU+7F72: shǔ  # 署\nU+7F74: pí  # 罴\nU+7F76: liǔ  # 罶\nU+7F79: lí  # 罹\nU+7F7D: jì  # 罽\nU+7F7E: zēng  # 罾\nU+7F81: jī  # 羁\nU+7F8A: yáng  # 羊\nU+7F8C: qiāng  # 羌\nU+7F8E: měi  # 美\nU+7F91: yǒu  # 羑\nU+7F93: bā  # 羓\nU+7F94: gāo  # 羔\nU+7F95: yàng  # 羕\nU+7F96: gǔ  # 羖\nU+7F9A: líng  # 羚\nU+7F9D: dī  # 羝\nU+7F9E: xiū  # 羞\nU+7F9F: qiǎng  # 羟\nU+7FA1: xiàn  # 羡\nU+7FA4: qún  # 群\nU+7FA7: suō  # 羧\nU+7FAF: jié  # 羯\nU+7FB0: tāng  # 羰\nU+7FB1: yuán  # 羱\nU+7FB2: xī  # 羲\nU+7FB8: léi  # 羸\nU+7FB9: gēng  # 羹\nU+7FBC: chàn  # 羼\nU+7FBD: yǔ  # 羽\nU+7FBF: yì  # 羿\nU+7FC0: chōng  # 翀\nU+7FC1: wēng  # 翁\nU+7FC2: fēn  # 翂\nU+7FC3: hóng  # 翃\nU+7FC5: chì  # 翅\nU+7FC8: xiá  # 翈\nU+7FCA: yì  # 翊\nU+7FCC: yì  # 翌\nU+7FCE: líng  # 翎\nU+7FD4: xiáng  # 翔\nU+7FD5: xī  # 翕\nU+7FD8: qiào  # 翘 -> qiáo\nU+7FD9: huì  # 翙\nU+7FDA: huī  # 翚\nU+7FDB: xiāo  # 翛\nU+7FDF: dí  # 翟\nU+7FE0: cuì  # 翠\nU+7FE1: fěi  # 翡\nU+7FE5: zhù  # 翥\nU+7FE6: jiǎn  # 翦\nU+7FE9: piān  # 翩\nU+7FEE: hé  # 翮\nU+7FEF: hè  # 翯\nU+7FF0: hàn  # 翰\nU+7FF1: áo  # 翱\nU+7FF3: yì  # 翳\nU+7FF7: lín  # 翷\nU+7FFB: fān  # 翻\nU+7FFC: yì  # 翼\nU+7FFE: xuān  # 翾\nU+8000: yào  # 耀\nU+8001: lǎo  # 老\nU+8003: kǎo  # 考\nU+8004: mào  # 耄\nU+8005: zhě  # 者\nU+8006: qí  # 耆\nU+8007: gǒu  # 耇\nU+800B: dié  # 耋\nU+800C: ér  # 而\nU+800D: shuǎ  # 耍\nU+800F: nài  # 耏 -> ér\nU+8010: nài  # 耐\nU+8011: duān  # 耑\nU+8012: lěi  # 耒\nU+8014: zǐ  # 耔\nU+8015: gēng  # 耕\nU+8016: chào  # 耖\nU+8017: hào  # 耗\nU+8018: yún  # 耘\nU+8019: bà  # 耙\nU+801C: sì  # 耜\nU+8020: huō  # 耠\nU+8022: lào  # 耢\nU+8024: jí  # 耤\nU+8025: tāng  # 耥 -> tǎng\nU+8026: ǒu  # 耦\nU+8027: lóu  # 耧\nU+8028: nòu  # 耨\nU+8029: jiǎng  # 耩\nU+802A: pǎng  # 耪\nU+8030: yōu  # 耰\nU+8031: mò  # 耱\nU+8033: ěr  # 耳\nU+8035: dīng  # 耵\nU+8036: yé  # 耶\nU+8037: dā  # 耷\nU+8038: sǒng  # 耸\nU+803B: chǐ  # 耻\nU+803D: dān  # 耽\nU+803F: gěng  # 耿\nU+8042: niè  # 聂\nU+8043: dān  # 聃\nU+8046: líng  # 聆\nU+804A: liáo  # 聊\nU+804B: lóng  # 聋\nU+804C: zhí  # 职\nU+804D: níng  # 聍\nU+8052: guā  # 聒 -> guō\nU+8054: lián  # 联\nU+8058: pìn  # 聘\nU+805A: jù  # 聚\nU+8069: kuì  # 聩\nU+806A: cōng  # 聪\nU+8071: áo  # 聱\nU+807F: yù  # 聿\nU+8083: sù  # 肃\nU+8084: yì  # 肄\nU+8086: sì  # 肆\nU+8087: zhào  # 肇\nU+8089: ròu  # 肉\nU+808B: lē  # 肋 -> lèi\nU+808C: jī  # 肌\nU+8093: huāng  # 肓\nU+8096: xiào  # 肖 -> xiāo\nU+8098: zhǒu  # 肘\nU+809A: dù  # 肚\nU+809B: gāng  # 肛\nU+809D: gān  # 肝\nU+809F: wò  # 肟\nU+80A0: cháng  # 肠\nU+80A1: gǔ  # 股\nU+80A2: zhī  # 肢\nU+80A4: fū  # 肤\nU+80A5: féi  # 肥\nU+80A9: jiān  # 肩\nU+80AA: fáng  # 肪\nU+80AB: zhūn  # 肫\nU+80AD: nà  # 肭\nU+80AE: āng  # 肮\nU+80AF: kěn  # 肯\nU+80B1: gōng  # 肱\nU+80B2: yù  # 育\nU+80B4: yáo  # 肴\nU+80B7: qiǎn  # 肷\nU+80B8: xī  # 肸\nU+80BA: fèi  # 肺\nU+80BC: jǐng  # 肼\nU+80BD: tài  # 肽\nU+80BE: shèn  # 肾\nU+80BF: zhǒng  # 肿\nU+80C0: zhàng  # 胀\nU+80C1: xié  # 胁\nU+80C2: shèn  # 胂\nU+80C3: wèi  # 胃\nU+80C4: zhòu  # 胄\nU+80C6: dǎn  # 胆\nU+80C8: bá  # 胈\nU+80CC: bèi  # 背\nU+80CD: guā  # 胍\nU+80CE: tāi  # 胎\nU+80D6: pàng  # 胖\nU+80D7: zhēn  # 胗\nU+80D9: zuò  # 胙\nU+80DA: pēi  # 胚\nU+80DB: jiǎ  # 胛\nU+80DC: shèng  # 胜\nU+80DD: zhī  # 胝\nU+80DE: bāo  # 胞\nU+80E0: qū  # 胠\nU+80E1: hú  # 胡\nU+80E3: chǐ  # 胣\nU+80E4: yìn  # 胤\nU+80E5: xū  # 胥\nU+80E7: lóng  # 胧\nU+80E8: dòng  # 胨\nU+80E9: kǎ  # 胩\nU+80EA: lú  # 胪\nU+80EB: jìng  # 胫\nU+80EC: nǔ  # 胬\nU+80ED: yān  # 胭\nU+80EF: kuà  # 胯\nU+80F0: yí  # 胰\nU+80F1: guāng  # 胱\nU+80F2: hǎi  # 胲\nU+80F3: gē  # 胳\nU+80F4: dòng  # 胴\nU+80F6: jiāo  # 胶\nU+80F8: xiōng  # 胸\nU+80FA: àn  # 胺\nU+80FC: pián  # 胼\nU+80FD: néng  # 能\nU+8102: zhī  # 脂\nU+8106: cuì  # 脆\nU+8109: mài  # 脉\nU+810A: jí  # 脊 -> jǐ\nU+810D: kuài  # 脍\nU+810E: sà  # 脎\nU+810F: zàng  # 脏\nU+8110: qí  # 脐\nU+8111: nǎo  # 脑\nU+8112: mǐ  # 脒\nU+8113: nóng  # 脓\nU+8114: luán  # 脔\nU+8116: bó  # 脖\nU+8118: wǎn  # 脘\nU+811A: jiǎo  # 脚\nU+811E: cuǒ  # 脞\nU+811F: liè  # 脟\nU+8129: xiū  # 脩\nU+812C: pāo  # 脬\nU+812F: pú  # 脯 -> fǔ\nU+8131: tuō  # 脱\nU+8132: niào  # 脲\nU+8136: luó  # 脶\nU+8138: liǎn  # 脸\nU+813E: pí  # 脾\nU+813F: biāo  # 脿\nU+8146: tiǎn  # 腆\nU+8148: jīng  # 腈\nU+814A: là  # 腊\nU+814B: yè  # 腋\nU+814C: yān  # 腌 -> ā\nU+8150: fǔ  # 腐\nU+8151: fǔ  # 腑\nU+8152: jū  # 腒\nU+8153: féi  # 腓\nU+8154: qiāng  # 腔\nU+8155: wàn  # 腕\nU+8158: guó  # 腘\nU+8159: zōng  # 腙\nU+815A: dìng  # 腚\nU+8160: còu  # 腠\nU+8165: xīng  # 腥\nU+8167: shù  # 腧\nU+8168: shuàn  # 腨\nU+8169: nǎn  # 腩\nU+816D: è  # 腭\nU+816E: sāi  # 腮\nU+816F: tú  # 腯\nU+8170: yāo  # 腰\nU+8171: jiàn  # 腱\nU+8174: yú  # 腴\nU+8179: fù  # 腹\nU+817A: xiàn  # 腺\nU+817B: nì  # 腻\nU+817C: miǎn  # 腼\nU+817D: wà  # 腽\nU+817E: téng  # 腾\nU+817F: tuǐ  # 腿\nU+8180: bǎng  # 膀\nU+8182: lǚ  # 膂\nU+8188: gé  # 膈\nU+818A: bó  # 膊\nU+818F: gāo  # 膏\nU+8191: bìn  # 膑\nU+8198: biāo  # 膘\nU+8199: jiǎng  # 膙\nU+819B: táng  # 膛\nU+819C: mó  # 膜\nU+819D: xī  # 膝\nU+81A6: lìn  # 膦\nU+81A8: péng  # 膨\nU+81B3: shàn  # 膳\nU+81BA: yīng  # 膺\nU+81BB: shān  # 膻\nU+81C0: tún  # 臀\nU+81C2: bì  # 臂\nU+81C3: yōng  # 臃\nU+81C6: yì  # 臆\nU+81CA: sāo  # 臊\nU+81CC: gǔ  # 臌\nU+81D1: nào  # 臑\nU+81DC: zā  # 臜\nU+81E3: chén  # 臣\nU+81E7: zāng  # 臧\nU+81EA: zì  # 自\nU+81EC: niè  # 臬\nU+81ED: chòu  # 臭\nU+81F3: zhì  # 至\nU+81F4: zhì  # 致\nU+81FB: zhēn  # 臻\nU+81FC: jiù  # 臼\nU+81FE: yú  # 臾\nU+8200: yǎo  # 舀\nU+8201: yú  # 舁\nU+8202: chōng  # 舂\nU+8204: xì  # 舄\nU+8205: jiù  # 舅\nU+8206: yú  # 舆\nU+820C: shé  # 舌\nU+820D: shě  # 舍\nU+8210: shì  # 舐\nU+8212: shū  # 舒\nU+8214: tiǎn  # 舔\nU+821B: chuǎn  # 舛\nU+821C: shùn  # 舜\nU+821E: wǔ  # 舞\nU+821F: zhōu  # 舟\nU+8220: dāo  # 舠\nU+8222: shān  # 舢\nU+8223: yǐ  # 舣\nU+8225: pā  # 舥\nU+822A: háng  # 航\nU+822B: fǎng  # 舫\nU+822C: bān  # 般\nU+822D: bǐ  # 舭\nU+822F: zhōng  # 舯\nU+8230: jiàn  # 舰\nU+8231: cāng  # 舱\nU+8232: líng  # 舲\nU+8233: zhú  # 舳\nU+8234: zé  # 舴\nU+8235: duò  # 舵\nU+8236: bó  # 舶\nU+8237: xián  # 舷\nU+8238: gě  # 舸\nU+8239: chuán  # 船\nU+823B: lú  # 舻\nU+823E: xī  # 舾\nU+8244: shāo  # 艄\nU+8245: yú  # 艅\nU+8247: tǐng  # 艇\nU+8249: wěi  # 艉\nU+824B: měng  # 艋\nU+824E: huáng  # 艎\nU+824F: shǒu  # 艏\nU+8258: sōu  # 艘\nU+825A: cáo  # 艚\nU+825F: chōng  # 艟\nU+8268: méng  # 艨\nU+826E: gěn  # 艮 -> gèn\nU+826F: liáng  # 良\nU+8270: jiān  # 艰\nU+8272: sè  # 色\nU+8273: yàn  # 艳\nU+8274: fú  # 艴\nU+827A: yì  # 艺\nU+827D: jiāo  # 艽\nU+827E: ài  # 艾\nU+827F: nǎi  # 艿\nU+8282: jié  # 节\nU+8283: péng  # 芃\nU+8284: wán  # 芄\nU+8288: mǐ  # 芈\nU+828A: qiān  # 芊\nU+828B: yù  # 芋\nU+828D: sháo  # 芍\nU+828E: qiōng  # 芎 -> xiōng\nU+828F: dù  # 芏\nU+8291: qǐ  # 芑\nU+8292: máng  # 芒\nU+8297: xiāng  # 芗\nU+8298: pí  # 芘 -> bì\nU+8299: fú  # 芙\nU+829C: wú  # 芜\nU+829D: zhī  # 芝\nU+829F: shān  # 芟\nU+82A0: wén  # 芠\nU+82A1: qiàn  # 芡\nU+82A3: fú  # 芣\nU+82A4: kōu  # 芤\nU+82A5: jiè  # 芥\nU+82A6: lú  # 芦\nU+82A8: jī  # 芨\nU+82A9: qín  # 芩\nU+82AA: qí  # 芪\nU+82AB: yán  # 芫 -> yuán\nU+82AC: fēn  # 芬\nU+82AD: bā  # 芭\nU+82AE: ruì  # 芮\nU+82AF: xīn  # 芯\nU+82B0: jì  # 芰\nU+82B1: huā  # 花\nU+82B3: fāng  # 芳\nU+82B4: wù  # 芴\nU+82B7: zhǐ  # 芷\nU+82B8: yún  # 芸\nU+82B9: qín  # 芹\nU+82BC: mào  # 芼 -> máo\nU+82BD: yá  # 芽\nU+82BE: fèi  # 芾\nU+82C1: cōng  # 苁\nU+82C4: biàn  # 苄\nU+82C7: wěi  # 苇\nU+82C8: lì  # 苈\nU+82C9: pǐ  # 苉\nU+82CA: è  # 苊\nU+82CB: xiàn  # 苋\nU+82CC: cháng  # 苌\nU+82CD: cāng  # 苍\nU+82CE: zhù  # 苎\nU+82CF: sū  # 苏\nU+82D1: yuàn  # 苑\nU+82D2: rǎn  # 苒\nU+82D3: líng  # 苓\nU+82D4: tái  # 苔\nU+82D5: sháo  # 苕 -> tiáo\nU+82D7: miáo  # 苗\nU+82D8: qǐng  # 苘\nU+82DB: kē  # 苛\nU+82DC: mù  # 苜\nU+82DE: bāo  # 苞\nU+82DF: gǒu  # 苟\nU+82E0: mín  # 苠\nU+82E1: yǐ  # 苡\nU+82E3: jù  # 苣\nU+82E4: piě  # 苤\nU+82E5: ruò  # 若\nU+82E6: kǔ  # 苦\nU+82E7: níng  # 苧 -> zhù\nU+82EB: shān  # 苫\nU+82EF: běn  # 苯\nU+82F1: yīng  # 英\nU+82F4: jū  # 苴\nU+82F7: gān  # 苷\nU+82F9: píng  # 苹\nU+82FB: fú  # 苻\nU+82FE: bì  # 苾\nU+8300: fú  # 茀\nU+8301: zhuó  # 茁\nU+8302: mào  # 茂\nU+8303: fàn  # 范\nU+8304: jiā  # 茄 -> qié\nU+8305: máo  # 茅\nU+8306: máo  # 茆\nU+8308: cí  # 茈 -> zǐ\nU+8309: mò  # 茉\nU+830B: zhǐ  # 茋\nU+830C: chí  # 茌\nU+830E: jīng  # 茎\nU+830F: lóng  # 茏\nU+8311: niǎo  # 茑\nU+8313: xué  # 茓\nU+8314: yíng  # 茔\nU+8315: qióng  # 茕\nU+8317: míng  # 茗\nU+831A: yìn  # 茚\nU+831B: gèn  # 茛\nU+831C: qiàn  # 茜\nU+831D: chǎi  # 茝\nU+8327: jiǎn  # 茧\nU+8328: cí  # 茨\nU+832B: máng  # 茫\nU+832C: chá  # 茬\nU+832D: jiāo  # 茭\nU+832F: fú  # 茯\nU+8331: zhū  # 茱\nU+8333: jiāng  # 茳\nU+8334: huí  # 茴\nU+8335: yīn  # 茵\nU+8336: chá  # 茶\nU+8338: rōng  # 茸 -> róng\nU+8339: rú  # 茹\nU+833A: chōng  # 茺\nU+833C: tóng  # 茼\nU+833D: zhòng  # 茽\nU+8340: xún  # 荀\nU+8341: huán  # 荁\nU+8343: quán  # 荃\nU+8344: gāi  # 荄\nU+8346: jīng  # 荆\nU+8347: xìng  # 荇\nU+8349: cǎo  # 草\nU+834F: rěn  # 荏\nU+8350: jiàn  # 荐\nU+8351: tí  # 荑 -> yí\nU+8352: huāng  # 荒\nU+8353: píng  # 荓\nU+8354: lì  # 荔\nU+8356: lǎo  # 荖\nU+8359: dá  # 荙\nU+835A: jiá  # 荚\nU+835B: ráo  # 荛\nU+835C: bì  # 荜\nU+835E: qiáo  # 荞\nU+835F: huì  # 荟\nU+8360: jì  # 荠\nU+8361: dàng  # 荡\nU+8363: róng  # 荣\nU+8364: hūn  # 荤\nU+8365: xíng  # 荥\nU+8366: luò  # 荦\nU+8367: yíng  # 荧\nU+8368: xún  # 荨 -> qián\nU+8369: jìn  # 荩\nU+836A: sūn  # 荪\nU+836B: yīn  # 荫\nU+836C: mǎi  # 荬\nU+836D: hóng  # 荭\nU+836E: zhòu  # 荮\nU+836F: yào  # 药\nU+8377: hé  # 荷\nU+8378: bí  # 荸\nU+837B: dí  # 荻\nU+837C: tú  # 荼\nU+837D: suī  # 荽\nU+8385: lì  # 莅\nU+8386: pú  # 莆\nU+8389: lì  # 莉\nU+838E: shā  # 莎 -> suō\nU+8392: jǔ  # 莒\nU+8393: méi  # 莓\nU+8398: shēn  # 莘\nU+8399: jūn  # 莙\nU+839B: tíng  # 莛\nU+839C: yóu  # 莜\nU+839D: cuò  # 莝\nU+839E: guǎn  # 莞 -> guān\nU+83A0: yǒu  # 莠\nU+83A8: làng  # 莨\nU+83A9: fú  # 莩\nU+83AA: é  # 莪\nU+83AB: mò  # 莫\nU+83B0: kǎn  # 莰\nU+83B1: lái  # 莱\nU+83B2: lián  # 莲\nU+83B3: shí  # 莳 -> shì\nU+83B4: wō  # 莴\nU+83B6: xiān  # 莶\nU+83B7: huò  # 获\nU+83B8: yóu  # 莸\nU+83B9: yíng  # 莹\nU+83BA: yīng  # 莺\nU+83BC: chún  # 莼\nU+83BD: mǎng  # 莽\nU+83BF: cì  # 莿\nU+83C0: wǎn  # 菀\nU+83C1: jīng  # 菁\nU+83C2: dì  # 菂\nU+83C5: jiān  # 菅\nU+83C7: gū  # 菇\nU+83C9: lù  # 菉\nU+83CA: jú  # 菊\nU+83CC: jūn  # 菌\nU+83CD: niè  # 菍\nU+83CF: hé  # 菏\nU+83D4: fú  # 菔\nU+83D6: chāng  # 菖\nU+83D8: sōng  # 菘\nU+83DC: cài  # 菜\nU+83DD: bá  # 菝\nU+83DF: tú  # 菟 -> tù\nU+83E0: bō  # 菠\nU+83E1: hàn  # 菡\nU+83E5: xī  # 菥\nU+83E9: pú  # 菩\nU+83EA: dàng  # 菪\nU+83F0: gū  # 菰\nU+83F1: líng  # 菱\nU+83F2: fēi  # 菲\nU+83F9: jū  # 菹 -> zū\nU+83FC: tǎn  # 菼\nU+83FD: shū  # 菽\nU+8401: qí  # 萁\nU+8403: cuì  # 萃\nU+8404: táo  # 萄\nU+8406: bì  # 萆\nU+840B: qī  # 萋\nU+840C: méng  # 萌\nU+840D: píng  # 萍\nU+840E: wēi  # 萎 -> wěi\nU+840F: dàn  # 萏\nU+8411: huán  # 萑\nU+8418: nài  # 萘\nU+841A: tuò  # 萚\nU+841C: tiē  # 萜\nU+841D: luó  # 萝\nU+8423: dìng  # 萣\nU+8424: yíng  # 萤\nU+8425: yíng  # 营\nU+8426: yíng  # 萦\nU+8427: xiāo  # 萧\nU+8428: sà  # 萨\nU+8429: qiū  # 萩\nU+8431: xuān  # 萱\nU+8433: nǎn  # 萳\nU+8438: yú  # 萸\nU+8439: biǎn  # 萹 -> biān\nU+843C: è  # 萼\nU+843D: luò  # 落 -> là\nU+8446: bǎo  # 葆\nU+844E: lǜ  # 葎\nU+8451: fēng  # 葑\nU+8456: tū  # 葖\nU+8457: zhù  # 著\nU+8459: xiāng  # 葙\nU+845A: rèn  # 葚 -> shèn\nU+845B: gé  # 葛\nU+845C: qiā  # 葜\nU+8461: pú  # 葡\nU+8463: dǒng  # 董\nU+8469: pā  # 葩\nU+846B: hú  # 葫\nU+846C: zàng  # 葬\nU+846D: jiā  # 葭\nU+8470: suī  # 葰 -> jùn\nU+8471: cōng  # 葱\nU+8473: wēi  # 葳\nU+8474: zhēn  # 葴\nU+8475: kuí  # 葵\nU+8476: tíng  # 葶\nU+8478: xǐ  # 葸\nU+847A: qì  # 葺\nU+8482: dì  # 蒂\nU+8484: guān  # 蒄\nU+8487: chǎn  # 蒇\nU+8488: kǎi  # 蒈\nU+8489: kuì  # 蒉\nU+848B: jiǎng  # 蒋\nU+848C: lóu  # 蒌\nU+848E: pài  # 蒎\nU+8490: sōu  # 蒐\nU+8497: làng  # 蒗\nU+8499: méng  # 蒙 -> mēng\nU+849C: suàn  # 蒜\nU+849F: jǔ  # 蒟\nU+84A1: bàng  # 蒡\nU+84A8: qiàn  # 蒨\nU+84AF: kuǎi  # 蒯\nU+84B1: pú  # 蒱\nU+84B2: pú  # 蒲\nU+84B4: shuò  # 蒴\nU+84B8: zhēng  # 蒸\nU+84B9: jiān  # 蒹\nU+84BA: jí  # 蒺\nU+84BB: ruò  # 蒻\nU+84BD: ēn  # 蒽\nU+84BF: hāo  # 蒿\nU+84C1: zhēn  # 蓁\nU+84C2: míng  # 蓂\nU+84C4: xù  # 蓄\nU+84C7: gǔ  # 蓇 -> gū\nU+84C9: róng  # 蓉\nU+84CA: wěng  # 蓊\nU+84CD: shī  # 蓍\nU+84CF: luǒ  # 蓏\nU+84D0: rù  # 蓐\nU+84D1: suō  # 蓑\nU+84D3: bèi  # 蓓\nU+84D6: bì  # 蓖\nU+84DD: lán  # 蓝\nU+84DF: jì  # 蓟\nU+84E0: lí  # 蓠\nU+84E2: lǎng  # 蓢\nU+84E3: yù  # 蓣\nU+84E5: yíng  # 蓥\nU+84E6: mò  # 蓦\nU+84EC: péng  # 蓬\nU+84F0: xǐ  # 蓰\nU+84FC: liǎo  # 蓼\nU+84FF: xu  # 蓿 -> xù\nU+8500: bù  # 蔀\nU+8503: qiáng  # 蔃\nU+8508: biāo  # 蔈\nU+850A: hǎn  # 蔊 -> hàn\nU+850C: sù  # 蔌\nU+8511: miè  # 蔑\nU+8513: màn  # 蔓\nU+8517: zhè  # 蔗\nU+851A: wèi  # 蔚\nU+851F: cù  # 蔟\nU+8521: cài  # 蔡\nU+852B: niān  # 蔫\nU+852C: shū  # 蔬\nU+8537: qiáng  # 蔷\nU+8538: dōu  # 蔸\nU+8539: liǎn  # 蔹\nU+853A: lìn  # 蔺\nU+853B: kòu  # 蔻\nU+853C: ǎi  # 蔼\nU+853D: bì  # 蔽\nU+8543: fān  # 蕃\nU+8548: xùn  # 蕈\nU+8549: jiāo  # 蕉\nU+854A: ruǐ  # 蕊\nU+8556: qú  # 蕖\nU+8557: lù  # 蕗\nU+8559: huì  # 蕙\nU+855E: zuì  # 蕞\nU+8564: ruí  # 蕤\nU+8568: jué  # 蕨\nU+8570: wēn  # 蕰 -> yùn\nU+8572: qí  # 蕲\nU+8574: yùn  # 蕴\nU+8579: wèng  # 蕹\nU+857A: jí  # 蕺\nU+857B: hóng  # 蕻 -> hòng\nU+857E: lěi  # 蕾\nU+8581: yù  # 薁\nU+8584: báo  # 薄\nU+8585: hāo  # 薅\nU+8587: wēi  # 薇\nU+858F: yì  # 薏\nU+859B: xuē  # 薛\nU+859C: bì  # 薜\nU+85A2: xiè  # 薢\nU+85A4: xiè  # 薤\nU+85A8: hōng  # 薨\nU+85AA: xīn  # 薪\nU+85AE: sǒu  # 薮\nU+85AF: shǔ  # 薯\nU+85B0: xūn  # 薰\nU+85B3: wěi  # 薳 -> yuǎn\nU+85B7: rú  # 薷\nU+85B8: piáo  # 薸\nU+85B9: tái  # 薹\nU+85BF: nǐ  # 薿\nU+85C1: gǎo  # 藁\nU+85C9: jí  # 藉 -> jiè\nU+85CF: cáng  # 藏\nU+85D0: miǎo  # 藐\nU+85D3: xiǎn  # 藓\nU+85D5: ǒu  # 藕\nU+85DC: lí  # 藜\nU+85DF: lěi  # 藟\nU+85E0: jiào  # 藠\nU+85E4: téng  # 藤\nU+85E6: mò  # 藦\nU+85E8: biāo  # 藨\nU+85E9: fān  # 藩\nU+85FB: zǎo  # 藻\nU+85FF: huò  # 藿\nU+8605: héng  # 蘅\nU+8611: mó  # 蘑\nU+8616: niè  # 蘖\nU+8618: ráng  # 蘘\nU+8627: qú  # 蘧\nU+8629: fán  # 蘩\nU+8638: zhàn  # 蘸\nU+863C: mí  # 蘼\nU+864E: hǔ  # 虎\nU+864F: lǔ  # 虏\nU+8650: nüè  # 虐\nU+8651: lǜ  # 虑\nU+8652: sī  # 虒\nU+8653: xiāo  # 虓\nU+8654: qián  # 虔\nU+865A: xū  # 虚\nU+865E: yú  # 虞\nU+8662: guó  # 虢\nU+8664: yán  # 虤\nU+866B: chóng  # 虫\nU+866C: qiú  # 虬\nU+866E: jǐ  # 虮\nU+8671: shī  # 虱\nU+8677: hán  # 虷\nU+8678: zǐ  # 虸\nU+8679: hóng  # 虹\nU+867A: huī  # 虺 -> huǐ\nU+867B: méng  # 虻\nU+867C: gè  # 虼\nU+867D: suī  # 虽\nU+867E: xiā  # 虾\nU+867F: chài  # 虿\nU+8680: shí  # 蚀\nU+8681: yǐ  # 蚁\nU+8682: mǎ  # 蚂\nU+8684: fāng  # 蚄\nU+8686: bā  # 蚆\nU+868A: wén  # 蚊\nU+868B: ruì  # 蚋\nU+868C: bàng  # 蚌\nU+868D: pí  # 蚍\nU+8693: yǐn  # 蚓\nU+8695: cán  # 蚕\nU+869C: yá  # 蚜\nU+869D: háo  # 蚝\nU+86A3: gōng  # 蚣\nU+86A4: zǎo  # 蚤\nU+86A7: jiè  # 蚧\nU+86A8: fú  # 蚨\nU+86A9: chī  # 蚩\nU+86AA: dǒu  # 蚪\nU+86AC: xiǎn  # 蚬\nU+86AF: qiū  # 蚯\nU+86B0: yóu  # 蚰\nU+86B1: zhà  # 蚱\nU+86B2: píng  # 蚲\nU+86B4: yòu  # 蚴\nU+86B6: hān  # 蚶\nU+86BA: rán  # 蚺\nU+86C0: zhù  # 蛀\nU+86C3: bǐng  # 蛃 -> bīng\nU+86C4: gū  # 蛄\nU+86C6: qū  # 蛆\nU+86C7: shé  # 蛇\nU+86C9: líng  # 蛉\nU+86CA: gǔ  # 蛊\nU+86CB: dàn  # 蛋\nU+86CE: lì  # 蛎\nU+86CF: chēng  # 蛏\nU+86D0: qū  # 蛐\nU+86D1: móu  # 蛑\nU+86D4: huí  # 蛔\nU+86D8: yáng  # 蛘\nU+86D9: wā  # 蛙\nU+86DB: zhū  # 蛛\nU+86DE: kuò  # 蛞\nU+86DF: jiāo  # 蛟\nU+86E4: há  # 蛤 -> gé\nU+86E9: qióng  # 蛩\nU+86ED: zhì  # 蛭\nU+86EE: mán  # 蛮\nU+86F0: zhé  # 蛰\nU+86F1: jiá  # 蛱\nU+86F2: náo  # 蛲\nU+86F3: sī  # 蛳\nU+86F4: qí  # 蛴\nU+86F8: shāo  # 蛸 -> xiāo\nU+86F9: yǒng  # 蛹\nU+86FE: é  # 蛾\nU+8700: shǔ  # 蜀\nU+8702: fēng  # 蜂\nU+8703: shèn  # 蜃\nU+8707: zhē  # 蜇 -> zhé\nU+8708: wú  # 蜈\nU+8709: fú  # 蜉\nU+870A: lí  # 蜊 -> lì\nU+870D: chú  # 蜍\nU+870E: yuān  # 蜎\nU+8710: jié  # 蜐\nU+8712: yán  # 蜒\nU+8713: tíng  # 蜓\nU+8715: tuì  # 蜕\nU+8717: wō  # 蜗\nU+8718: zhī  # 蜘\nU+871A: fēi  # 蜚\nU+871C: mì  # 蜜\nU+871E: qí  # 蜞\nU+8721: là  # 蜡\nU+8722: měng  # 蜢\nU+8723: qiāng  # 蜣\nU+8725: xī  # 蜥\nU+8729: tiáo  # 蜩\nU+872E: yù  # 蜮\nU+8731: pí  # 蜱\nU+8734: yì  # 蜴\nU+8737: quán  # 蜷\nU+873B: qīng  # 蜻\nU+873E: guǒ  # 蜾\nU+873F: wān  # 蜿\nU+8747: yíng  # 蝇\nU+8748: guō  # 蝈\nU+8749: chán  # 蝉\nU+874C: kē  # 蝌\nU+874E: xiē  # 蝎\nU+8753: yú  # 蝓\nU+8757: huáng  # 蝗\nU+8758: yǎn  # 蝘\nU+8759: biān  # 蝙\nU+8760: fú  # 蝠\nU+8763: yóu  # 蝣\nU+8764: qiú  # 蝤\nU+8765: máo  # 蝥\nU+876E: fù  # 蝮\nU+8770: kuí  # 蝰\nU+8772: là  # 蝲\nU+8774: hú  # 蝴\nU+8776: dié  # 蝶\nU+877B: nǎn  # 蝻\nU+877C: lóu  # 蝼\nU+877D: chūn  # 蝽\nU+877E: róng  # 蝾\nU+8782: láng  # 螂\nU+8783: páng  # 螃\nU+8785: xī  # 螅\nU+8788: yuán  # 螈\nU+878B: sōu  # 螋\nU+878D: róng  # 融\nU+8797: táng  # 螗\nU+879F: míng  # 螟\nU+87A0: yì  # 螠\nU+87A3: tè  # 螣 -> téng\nU+87A8: mǎn  # 螨\nU+87AB: shì  # 螫\nU+87AC: cáo  # 螬\nU+87AD: chī  # 螭\nU+87AF: áo  # 螯\nU+87B1: wèi  # 螱\nU+87B3: táng  # 螳\nU+87B5: piāo  # 螵\nU+87BA: luó  # 螺\nU+87BD: zhōng  # 螽\nU+87C0: shuài  # 蟀\nU+87C6: má  # 蟆\nU+87CA: máo  # 蟊\nU+87CB: xī  # 蟋\nU+87CF: xiāo  # 蟏\nU+87D1: zhāng  # 蟑\nU+87D2: mǎng  # 蟒\nU+87DB: péng  # 蟛\nU+87E0: pán  # 蟠\nU+87E5: huáng  # 蟥\nU+87EA: huì  # 蟪\nU+87EB: yín  # 蟫\nU+87EE: shàn  # 蟮\nU+87F9: xiè  # 蟹\nU+87FE: chán  # 蟾\nU+8803: luǒ  # 蠃\nU+880A: lián  # 蠊\nU+880B: zhú  # 蠋\nU+8813: měng  # 蠓\nU+8815: rú  # 蠕\nU+8816: huò  # 蠖\nU+8821: lí  # 蠡 -> lǐ\nU+8822: chǔn  # 蠢\nU+8832: juān  # 蠲\nU+8839: dù  # 蠹\nU+883C: qú  # 蠼\nU+8840: xuè  # 血 -> xiě\nU+8843: pēi  # 衃\nU+8844: nǜ  # 衄\nU+8845: xìn  # 衅\nU+884C: xíng  # 行 -> háng\nU+884D: yǎn  # 衍\nU+884E: kàn  # 衎\nU+8852: xuàn  # 衒\nU+8854: xián  # 衔\nU+8857: jiē  # 街\nU+8859: yá  # 衙\nU+8860: zhūn  # 衠\nU+8861: héng  # 衡\nU+8862: qú  # 衢\nU+8863: yī  # 衣\nU+8865: bǔ  # 补\nU+8868: biǎo  # 表\nU+8869: chǎ  # 衩 -> chà\nU+886B: shān  # 衫\nU+886C: chèn  # 衬\nU+886E: gǔn  # 衮\nU+8870: shuāi  # 衰\nU+8872: nà  # 衲\nU+8877: zhōng  # 衷\nU+887D: rèn  # 衽\nU+887E: qīn  # 衾\nU+887F: jīn  # 衿\nU+8881: yuán  # 袁\nU+8882: mèi  # 袂\nU+8884: ǎo  # 袄\nU+8885: niǎo  # 袅\nU+8886: huī  # 袆\nU+8888: jiā  # 袈\nU+888B: dài  # 袋\nU+888D: páo  # 袍\nU+8892: tǎn  # 袒\nU+8896: xiù  # 袖\nU+8897: zhěn  # 袗\nU+889C: wà  # 袜\nU+88A2: pàn  # 袢\nU+88A4: mào  # 袤\nU+88AA: qū  # 袪\nU+88AB: bèi  # 被\nU+88AD: xí  # 袭\nU+88AF: bó  # 袯\nU+88B1: fú  # 袱\nU+88B7: jiá  # 袷\nU+88BC: gē  # 袼\nU+88C1: cái  # 裁\nU+88C2: liè  # 裂\nU+88C5: zhuāng  # 装\nU+88C6: dāng  # 裆\nU+88C8: kūn  # 裈\nU+88C9: kèn  # 裉\nU+88CE: chéng  # 裎\nU+88D2: póu  # 裒\nU+88D4: yì  # 裔\nU+88D5: yù  # 裕\nU+88D8: qiú  # 裘\nU+88D9: qún  # 裙\nU+88DB: yì  # 裛\nU+88DF: shā  # 裟\nU+88E2: lián  # 裢\nU+88E3: liǎn  # 裣\nU+88E4: kù  # 裤\nU+88E5: jiǎn  # 裥\nU+88E8: bì  # 裨\nU+88F0: duō  # 裰\nU+88F1: biǎo  # 裱\nU+88F3: shang  # 裳 -> cháng\nU+88F4: péi  # 裴\nU+88F8: luǒ  # 裸\nU+88F9: guǒ  # 裹\nU+88FC: tì  # 裼 -> xī\nU+88FE: jū  # 裾\nU+8902: guà  # 褂\nU+890A: biǎn  # 褊\nU+8910: hè  # 褐\nU+8912: bāo  # 褒\nU+8913: bǎo  # 褓\nU+8915: yú  # 褕\nU+8919: bèi  # 褙\nU+891A: chǔ  # 褚 -> zhǔ\nU+891B: lǚ  # 褛\nU+891F: tā  # 褟\nU+8921: dā  # 褡\nU+8925: rù  # 褥\nU+892A: tuì  # 褪\nU+892B: chǐ  # 褫\nU+892F: jiè  # 褯\nU+8930: qiān  # 褰\nU+8934: lán  # 褴\nU+8936: zhě  # 褶\nU+8941: qiǎng  # 襁\nU+8944: xiāng  # 襄\nU+8955: lán  # 襕\nU+895A: suì  # 襚\nU+895C: chān  # 襜\nU+895E: bì  # 襞\nU+895F: jīn  # 襟\nU+8966: rú  # 襦\nU+896B: shì  # 襫\nU+897B: pàn  # 襻\nU+897F: xī  # 西\nU+8981: yào  # 要\nU+8983: tán  # 覃\nU+8986: fù  # 覆\nU+89C1: jiàn  # 见\nU+89C2: guān  # 观\nU+89C3: yàn  # 觃\nU+89C4: guī  # 规\nU+89C5: mì  # 觅\nU+89C6: shì  # 视\nU+89C7: chān  # 觇\nU+89C8: lǎn  # 览\nU+89C9: jué  # 觉\nU+89CA: jì  # 觊\nU+89CB: xí  # 觋\nU+89CC: dí  # 觌\nU+89CE: yú  # 觎\nU+89CF: gòu  # 觏\nU+89D0: jìn  # 觐\nU+89D1: qù  # 觑\nU+89D2: jiǎo  # 角\nU+89D6: jué  # 觖\nU+89DA: gū  # 觚\nU+89DC: zī  # 觜\nU+89DE: shāng  # 觞\nU+89DF: huà  # 觟\nU+89E3: jiě  # 解\nU+89E5: gōng  # 觥\nU+89E6: chù  # 触\nU+89EB: sù  # 觫\nU+89ED: jī  # 觭\nU+89EF: zhì  # 觯\nU+89F1: bì  # 觱\nU+89F3: hú  # 觳\nU+89FF: xī  # 觿\nU+8A00: yán  # 言\nU+8A04: qiú  # 訄\nU+8A07: hōng  # 訇\nU+8A1A: yín  # 訚\nU+8A3E: zī  # 訾\nU+8A48: lì  # 詈\nU+8A5F: zhé  # 詟\nU+8A79: zhān  # 詹\nU+8A89: yù  # 誉\nU+8A8A: téng  # 誊\nU+8A93: shì  # 誓\nU+8B07: jiǎn  # 謇\nU+8B66: jǐng  # 警\nU+8B6C: pì  # 譬\nU+8BA1: jì  # 计\nU+8BA2: dìng  # 订\nU+8BA3: fù  # 讣\nU+8BA4: rèn  # 认\nU+8BA5: jī  # 讥\nU+8BA6: jié  # 讦\nU+8BA7: hòng  # 讧\nU+8BA8: tǎo  # 讨\nU+8BA9: ràng  # 让\nU+8BAA: shàn  # 讪\nU+8BAB: qì  # 讫\nU+8BAD: xùn  # 训\nU+8BAE: yì  # 议\nU+8BAF: xùn  # 讯\nU+8BB0: jì  # 记\nU+8BB1: rèn  # 讱\nU+8BB2: jiǎng  # 讲\nU+8BB3: huì  # 讳\nU+8BB4: ōu  # 讴\nU+8BB5: jù  # 讵\nU+8BB6: yà  # 讶\nU+8BB7: nè  # 讷\nU+8BB8: xǔ  # 许\nU+8BB9: é  # 讹\nU+8BBA: lùn  # 论\nU+8BBB: xiōng  # 讻\nU+8BBC: sòng  # 讼\nU+8BBD: fěng  # 讽\nU+8BBE: shè  # 设\nU+8BBF: fǎng  # 访\nU+8BC0: jué  # 诀\nU+8BC1: zhèng  # 证\nU+8BC2: gǔ  # 诂\nU+8BC3: hē  # 诃\nU+8BC4: píng  # 评\nU+8BC5: zǔ  # 诅\nU+8BC6: shí  # 识\nU+8BC7: xiòng  # 诇\nU+8BC8: zhà  # 诈\nU+8BC9: sù  # 诉\nU+8BCA: zhěn  # 诊\nU+8BCB: dǐ  # 诋\nU+8BCC: zhōu  # 诌\nU+8BCD: cí  # 词\nU+8BCE: qū  # 诎\nU+8BCF: zhào  # 诏\nU+8BD0: bì  # 诐\nU+8BD1: yì  # 译\nU+8BD2: yí  # 诒\nU+8BD3: kuāng  # 诓\nU+8BD4: lěi  # 诔\nU+8BD5: shì  # 试\nU+8BD6: guà  # 诖\nU+8BD7: shī  # 诗\nU+8BD8: jí  # 诘 -> jié\nU+8BD9: huī  # 诙\nU+8BDA: chéng  # 诚\nU+8BDB: zhū  # 诛\nU+8BDC: shēn  # 诜\nU+8BDD: huà  # 话\nU+8BDE: dàn  # 诞\nU+8BDF: gòu  # 诟\nU+8BE0: quán  # 诠\nU+8BE1: guǐ  # 诡\nU+8BE2: xún  # 询\nU+8BE3: yì  # 诣\nU+8BE4: zhèng  # 诤\nU+8BE5: gāi  # 该\nU+8BE6: xiáng  # 详\nU+8BE7: chà  # 诧\nU+8BE8: hùn  # 诨\nU+8BE9: xǔ  # 诩\nU+8BEB: jiè  # 诫\nU+8BEC: wū  # 诬\nU+8BED: yǔ  # 语\nU+8BEE: qiào  # 诮\nU+8BEF: wù  # 误\nU+8BF0: gào  # 诰\nU+8BF1: yòu  # 诱\nU+8BF2: huì  # 诲\nU+8BF3: kuáng  # 诳\nU+8BF4: shuō  # 说\nU+8BF5: sòng  # 诵\nU+8BF7: qǐng  # 请\nU+8BF8: zhū  # 诸\nU+8BF9: zōu  # 诹\nU+8BFA: nuò  # 诺\nU+8BFB: dú  # 读\nU+8BFC: zhuó  # 诼\nU+8BFD: fěi  # 诽\nU+8BFE: kè  # 课\nU+8BFF: wěi  # 诿\nU+8C00: yú  # 谀\nU+8C01: shuí  # 谁\nU+8C02: shěn  # 谂\nU+8C03: diào  # 调 -> tiáo\nU+8C04: chǎn  # 谄\nU+8C05: liàng  # 谅\nU+8C06: zhūn  # 谆\nU+8C07: suì  # 谇\nU+8C08: tán  # 谈\nU+8C0A: yì  # 谊\nU+8C0B: móu  # 谋\nU+8C0C: chén  # 谌\nU+8C0D: dié  # 谍\nU+8C0E: huǎng  # 谎\nU+8C0F: jiàn  # 谏\nU+8C10: xié  # 谐\nU+8C11: xuè  # 谑\nU+8C12: yè  # 谒\nU+8C13: wèi  # 谓\nU+8C14: è  # 谔\nU+8C15: yù  # 谕\nU+8C16: xuān  # 谖\nU+8C17: chán  # 谗\nU+8C19: ān  # 谙\nU+8C1A: yàn  # 谚\nU+8C1B: dì  # 谛\nU+8C1C: mí  # 谜\nU+8C1D: pián  # 谝 -> piǎn\nU+8C1E: xū  # 谞\nU+8C1F: mó  # 谟\nU+8C20: dǎng  # 谠\nU+8C21: sù  # 谡\nU+8C22: xiè  # 谢\nU+8C23: yáo  # 谣\nU+8C24: bàng  # 谤\nU+8C25: shì  # 谥\nU+8C26: qiān  # 谦\nU+8C27: mì  # 谧\nU+8C28: jǐn  # 谨\nU+8C29: mán  # 谩\nU+8C2A: zhé  # 谪\nU+8C2B: jiǎn  # 谫\nU+8C2C: miù  # 谬\nU+8C2D: tán  # 谭\nU+8C2E: zèn  # 谮\nU+8C2F: qiáo  # 谯\nU+8C30: lán  # 谰\nU+8C31: pǔ  # 谱\nU+8C32: jué  # 谲\nU+8C33: yàn  # 谳\nU+8C34: qiǎn  # 谴\nU+8C35: zhān  # 谵\nU+8C36: chèn  # 谶\nU+8C37: gǔ  # 谷\nU+8C3C: hóng  # 谼\nU+8C3F: xī  # 谿\nU+8C41: huō  # 豁\nU+8C46: dòu  # 豆\nU+8C47: jiāng  # 豇\nU+8C49: shì  # 豉 -> chǐ\nU+8C4C: wān  # 豌\nU+8C55: shǐ  # 豕\nU+8C5A: tún  # 豚\nU+8C61: xiàng  # 象\nU+8C62: huàn  # 豢\nU+8C68: xī  # 豨\nU+8C6A: háo  # 豪\nU+8C6B: yù  # 豫\nU+8C6E: fén  # 豮\nU+8C73: bīn  # 豳\nU+8C78: zhì  # 豸\nU+8C79: bào  # 豹\nU+8C7A: chái  # 豺\nU+8C82: diāo  # 貂\nU+8C85: xiū  # 貅\nU+8C86: huán  # 貆\nU+8C89: háo  # 貉 -> hé\nU+8C8A: mò  # 貊\nU+8C8C: mào  # 貌\nU+8C94: pí  # 貔\nU+8C98: mò  # 貘\nU+8D1D: bèi  # 贝\nU+8D1E: zhēn  # 贞\nU+8D1F: fù  # 负\nU+8D21: gòng  # 贡\nU+8D22: cái  # 财\nU+8D23: zé  # 责\nU+8D24: xián  # 贤\nU+8D25: bài  # 败\nU+8D26: zhàng  # 账\nU+8D27: huò  # 货\nU+8D28: zhì  # 质\nU+8D29: fàn  # 贩\nU+8D2A: tān  # 贪\nU+8D2B: pín  # 贫\nU+8D2C: biǎn  # 贬\nU+8D2D: gòu  # 购\nU+8D2E: zhù  # 贮\nU+8D2F: guàn  # 贯\nU+8D30: èr  # 贰\nU+8D31: jiàn  # 贱\nU+8D32: bēn  # 贲 -> bì\nU+8D33: shì  # 贳\nU+8D34: tiē  # 贴\nU+8D35: guì  # 贵\nU+8D36: kuàng  # 贶\nU+8D37: dài  # 贷\nU+8D38: mào  # 贸\nU+8D39: fèi  # 费\nU+8D3A: hè  # 贺\nU+8D3B: yí  # 贻\nU+8D3C: zéi  # 贼\nU+8D3D: zhì  # 贽\nU+8D3E: jiǎ  # 贾 -> gǔ\nU+8D3F: huì  # 贿\nU+8D40: zī  # 赀\nU+8D41: lìn  # 赁\nU+8D42: lù  # 赂\nU+8D43: zāng  # 赃\nU+8D44: zī  # 资\nU+8D45: gāi  # 赅\nU+8D46: jìn  # 赆\nU+8D47: qiú  # 赇\nU+8D48: zhèn  # 赈\nU+8D49: lài  # 赉\nU+8D4A: shē  # 赊\nU+8D4B: fù  # 赋\nU+8D4C: dǔ  # 赌\nU+8D4D: jī  # 赍\nU+8D4E: shú  # 赎\nU+8D4F: shǎng  # 赏\nU+8D50: cì  # 赐\nU+8D51: bì  # 赑\nU+8D52: zhōu  # 赒\nU+8D53: gēng  # 赓\nU+8D54: péi  # 赔\nU+8D55: dǎn  # 赕\nU+8D56: lài  # 赖\nU+8D57: fèng  # 赗\nU+8D58: zhuì  # 赘\nU+8D59: fù  # 赙\nU+8D5A: zhuàn  # 赚\nU+8D5B: sài  # 赛\nU+8D5C: zé  # 赜\nU+8D5D: yàn  # 赝\nU+8D5E: zàn  # 赞\nU+8D5F: yūn  # 赟\nU+8D60: zèng  # 赠\nU+8D61: shàn  # 赡\nU+8D62: yíng  # 赢\nU+8D63: gàn  # 赣\nU+8D64: chì  # 赤\nU+8D66: shè  # 赦\nU+8D67: nǎn  # 赧\nU+8D6A: chēng  # 赪\nU+8D6B: hè  # 赫\nU+8D6D: zhě  # 赭\nU+8D70: zǒu  # 走\nU+8D73: jiū  # 赳\nU+8D74: fù  # 赴\nU+8D75: zhào  # 赵\nU+8D76: gǎn  # 赶\nU+8D77: qǐ  # 起\nU+8D81: chèn  # 趁\nU+8D84: jū  # 趄 -> qiè\nU+8D85: chāo  # 超\nU+8D8A: yuè  # 越\nU+8D8B: qū  # 趋\nU+8D91: zī  # 趑\nU+8D94: liè  # 趔\nU+8D9F: tàng  # 趟\nU+8DA3: qù  # 趣\nU+8DAF: tì  # 趯\nU+8DB1: zǎn  # 趱\nU+8DB3: zú  # 足\nU+8DB4: pā  # 趴\nU+8DB5: bào  # 趵\nU+8DB8: dǔn  # 趸\nU+8DBA: fū  # 趺\nU+8DBC: jiǎn  # 趼\nU+8DBE: zhǐ  # 趾\nU+8DBF: tā  # 趿\nU+8DC2: qí  # 跂\nU+8DC3: yuè  # 跃\nU+8DC4: qiāng  # 跄\nU+8DC6: tái  # 跆\nU+8DCB: bá  # 跋\nU+8DCC: diē  # 跌\nU+8DCE: tuó  # 跎\nU+8DCF: jiā  # 跏\nU+8DD0: cī  # 跐\nU+8DD1: pǎo  # 跑\nU+8DD6: zhí  # 跖\nU+8DD7: fū  # 跗\nU+8DDA: shān  # 跚\nU+8DDB: bǒ  # 跛\nU+8DDD: jù  # 距\nU+8DDE: lì  # 跞\nU+8DDF: gēn  # 跟\nU+8DE3: xiǎn  # 跣\nU+8DE4: jiāo  # 跤\nU+8DE8: kuà  # 跨\nU+8DEA: guì  # 跪\nU+8DEC: kuǐ  # 跬\nU+8DEF: lù  # 路\nU+8DF1: zhì  # 跱\nU+8DF3: tiào  # 跳\nU+8DF5: jiàn  # 践\nU+8DF6: dá  # 跶\nU+8DF7: qiāo  # 跷\nU+8DF8: bì  # 跸\nU+8DF9: xiān  # 跹\nU+8DFA: duò  # 跺\nU+8DFB: jī  # 跻\nU+8DFD: jì  # 跽\nU+8E05: xué  # 踅\nU+8E09: liáng  # 踉\nU+8E0A: yǒng  # 踊\nU+8E0C: chóu  # 踌\nU+8E0F: tà  # 踏\nU+8E12: wō  # 踒\nU+8E14: chuō  # 踔\nU+8E1D: huái  # 踝\nU+8E1E: jù  # 踞\nU+8E1F: chí  # 踟\nU+8E22: tī  # 踢\nU+8E23: bó  # 踣\nU+8E26: yǐ  # 踦 -> qī\nU+8E29: cǎi  # 踩\nU+8E2A: zōng  # 踪\nU+8E2C: zhì  # 踬\nU+8E2E: diǎn  # 踮\nU+8E2F: zhí  # 踯\nU+8E31: duó  # 踱\nU+8E35: zhǒng  # 踵\nU+8E36: dì  # 踶\nU+8E39: chuài  # 踹\nU+8E3A: jiàn  # 踺\nU+8E3D: jǔ  # 踽\nU+8E40: dié  # 蹀\nU+8E41: pián  # 蹁\nU+8E42: róu  # 蹂\nU+8E44: tí  # 蹄\nU+8E45: chǎ  # 蹅\nU+8E47: jiǎn  # 蹇\nU+8E48: dǎo  # 蹈\nU+8E49: cuō  # 蹉\nU+8E4A: qī  # 蹊\nU+8E4B: tà  # 蹋\nU+8E50: jí  # 蹐\nU+8E51: niè  # 蹑\nU+8E52: pán  # 蹒\nU+8E59: cù  # 蹙\nU+8E5A: tāng  # 蹚\nU+8E5C: sù  # 蹜\nU+8E62: dí  # 蹢\nU+8E66: bèng  # 蹦\nU+8E69: bié  # 蹩\nU+8E6C: dēng  # 蹬\nU+8E6D: cèng  # 蹭\nU+8E6F: fán  # 蹯\nU+8E70: chú  # 蹰\nU+8E72: dūn  # 蹲\nU+8E74: cù  # 蹴\nU+8E76: jué  # 蹶\nU+8E7C: pǔ  # 蹼\nU+8E7D: liāo  # 蹽\nU+8E7E: dūn  # 蹾\nU+8E7F: cuān  # 蹿\nU+8E81: zào  # 躁\nU+8E85: zhú  # 躅\nU+8E87: chú  # 躇\nU+8E8F: lìn  # 躏\nU+8E90: liè  # 躐\nU+8E94: chán  # 躔\nU+8E9C: zuān  # 躜\nU+8E9E: xiè  # 躞\nU+8EAB: shēn  # 身\nU+8EAC: gōng  # 躬\nU+8EAF: qū  # 躯\nU+8EB2: duǒ  # 躲\nU+8EBA: tǎng  # 躺\nU+8F66: chē  # 车\nU+8F67: yà  # 轧\nU+8F68: guǐ  # 轨\nU+8F69: xuān  # 轩\nU+8F6A: dài  # 轪\nU+8F6B: rèn  # 轫\nU+8F6C: zhuǎn  # 转\nU+8F6D: è  # 轭\nU+8F6E: lún  # 轮\nU+8F6F: ruǎn  # 软\nU+8F70: hōng  # 轰\nU+8F71: gū  # 轱\nU+8F72: kē  # 轲\nU+8F73: lú  # 轳\nU+8F74: zhóu  # 轴\nU+8F75: zhǐ  # 轵\nU+8F76: yì  # 轶\nU+8F77: hū  # 轷\nU+8F78: zhěn  # 轸\nU+8F79: lì  # 轹\nU+8F7A: yáo  # 轺\nU+8F7B: qīng  # 轻\nU+8F7C: shì  # 轼\nU+8F7D: zài  # 载 -> zǎi\nU+8F7E: zhì  # 轾\nU+8F7F: jiào  # 轿\nU+8F80: zhōu  # 辀\nU+8F81: quán  # 辁\nU+8F82: lù  # 辂\nU+8F83: jiào  # 较\nU+8F84: zhé  # 辄\nU+8F85: fǔ  # 辅\nU+8F86: liàng  # 辆\nU+8F87: niǎn  # 辇\nU+8F88: bèi  # 辈\nU+8F89: huī  # 辉\nU+8F8A: gǔn  # 辊\nU+8F8B: wǎng  # 辋\nU+8F8C: liáng  # 辌\nU+8F8D: chuò  # 辍\nU+8F8E: zī  # 辎\nU+8F8F: còu  # 辏\nU+8F90: fú  # 辐\nU+8F91: jí  # 辑\nU+8F92: wēn  # 辒\nU+8F93: shū  # 输\nU+8F94: pèi  # 辔\nU+8F95: yuán  # 辕\nU+8F96: xiá  # 辖\nU+8F97: niǎn  # 辗 -> zhǎn\nU+8F98: lù  # 辘\nU+8F99: zhé  # 辙\nU+8F9A: lín  # 辚\nU+8F9B: xīn  # 辛\nU+8F9C: gū  # 辜\nU+8F9E: cí  # 辞\nU+8F9F: pì  # 辟 -> bì\nU+8FA3: là  # 辣\nU+8FA8: biàn  # 辨\nU+8FA9: biàn  # 辩\nU+8FAB: biàn  # 辫\nU+8FB0: chén  # 辰\nU+8FB1: rǔ  # 辱\nU+8FB9: biān  # 边\nU+8FBD: liáo  # 辽\nU+8FBE: dá  # 达\nU+8FBF: chān  # 辿\nU+8FC1: qiān  # 迁\nU+8FC2: yū  # 迂\nU+8FC4: qì  # 迄\nU+8FC5: xùn  # 迅\nU+8FC7: guò  # 过\nU+8FC8: mài  # 迈\nU+8FCE: yíng  # 迎\nU+8FD0: yùn  # 运\nU+8FD1: jìn  # 近\nU+8FD3: yà  # 迓\nU+8FD4: fǎn  # 返\nU+8FD5: wù  # 迕 -> wǔ\nU+8FD8: hái  # 还\nU+8FD9: zhè  # 这\nU+8FDB: jìn  # 进\nU+8FDC: yuǎn  # 远\nU+8FDD: wéi  # 违\nU+8FDE: lián  # 连\nU+8FDF: chí  # 迟\nU+8FE2: tiáo  # 迢\nU+8FE4: yí  # 迤 -> yǐ\nU+8FE5: jiǒng  # 迥\nU+8FE6: jiā  # 迦\nU+8FE8: dài  # 迨\nU+8FE9: ěr  # 迩\nU+8FEA: dí  # 迪\nU+8FEB: pò  # 迫\nU+8FED: dié  # 迭\nU+8FEE: zé  # 迮\nU+8FF0: shù  # 述\nU+8FF3: jìng  # 迳\nU+8FF7: mí  # 迷\nU+8FF8: bèng  # 迸\nU+8FF9: jì  # 迹\nU+8FFA: nǎi  # 迺\nU+8FFD: zhuī  # 追\nU+9000: tuì  # 退\nU+9001: sòng  # 送\nU+9002: shì  # 适\nU+9003: táo  # 逃\nU+9004: páng  # 逄\nU+9005: hòu  # 逅\nU+9006: nì  # 逆\nU+9009: xuǎn  # 选\nU+900A: xùn  # 逊\nU+900B: bū  # 逋\nU+900D: xiāo  # 逍\nU+900F: tòu  # 透\nU+9010: zhú  # 逐\nU+9011: qiú  # 逑\nU+9012: dì  # 递\nU+9014: tú  # 途\nU+9016: tì  # 逖\nU+9017: dòu  # 逗\nU+901A: tōng  # 通\nU+901B: guàng  # 逛\nU+901D: shì  # 逝\nU+901E: chěng  # 逞\nU+901F: sù  # 速\nU+9020: zào  # 造\nU+9021: qūn  # 逡\nU+9022: féng  # 逢\nU+9026: lǐ  # 逦\nU+902D: huàn  # 逭\nU+902E: dǎi  # 逮 -> dài\nU+902F: lù  # 逯\nU+9034: chuō  # 逴\nU+9035: kuí  # 逵\nU+9036: wēi  # 逶\nU+9038: yì  # 逸\nU+903B: luó  # 逻\nU+903C: bī  # 逼\nU+903E: yú  # 逾\nU+9041: dùn  # 遁\nU+9042: suì  # 遂\nU+9044: chuán  # 遄\nU+9046: tí  # 遆 -> dì\nU+9047: yù  # 遇\nU+904D: biàn  # 遍\nU+904F: è  # 遏\nU+9050: xiá  # 遐\nU+9051: huáng  # 遑\nU+9052: qiú  # 遒\nU+9053: dào  # 道\nU+9057: yí  # 遗\nU+9058: gòu  # 遘\nU+905B: liú  # 遛 -> liù\nU+9062: tà  # 遢\nU+9063: qiǎn  # 遣\nU+9065: yáo  # 遥\nU+9068: áo  # 遨\nU+906D: zāo  # 遭\nU+906E: zhē  # 遮\nU+9074: lín  # 遴\nU+9075: zūn  # 遵\nU+9079: yù  # 遹\nU+907D: jù  # 遽\nU+907F: bì  # 避\nU+9080: yāo  # 邀\nU+9082: xiè  # 邂\nU+9083: suì  # 邃\nU+9088: miǎo  # 邈\nU+908B: lā  # 邋\nU+9091: yì  # 邑\nU+9093: dèng  # 邓\nU+9095: yōng  # 邕\nU+9097: hán  # 邗\nU+9098: yú  # 邘\nU+9099: máng  # 邙\nU+909B: qióng  # 邛\nU+909D: kuàng  # 邝\nU+90A0: bīn  # 邠\nU+90A1: fāng  # 邡\nU+90A2: xíng  # 邢\nU+90A3: nà  # 那\nU+90A6: bāng  # 邦\nU+90A8: cūn  # 邨\nU+90AA: xié  # 邪\nU+90AC: wū  # 邬\nU+90AE: yóu  # 邮\nU+90AF: hán  # 邯\nU+90B0: tái  # 邰\nU+90B1: qiū  # 邱\nU+90B2: bì  # 邲\nU+90B3: pī  # 邳\nU+90B4: bǐng  # 邴\nU+90B5: shào  # 邵\nU+90B6: bèi  # 邶\nU+90B8: dǐ  # 邸\nU+90B9: zōu  # 邹\nU+90BA: yè  # 邺\nU+90BB: lín  # 邻\nU+90BD: guī  # 邽\nU+90BE: zhū  # 邾\nU+90BF: shī  # 邿\nU+90C1: yù  # 郁\nU+90C3: hé  # 郃\nU+90C4: qiè  # 郄\nU+90C5: zhì  # 郅\nU+90C7: huán  # 郇 -> xún\nU+90C8: hòu  # 郈\nU+90CA: jiāo  # 郊\nU+90CE: láng  # 郎\nU+90CF: jiá  # 郏\nU+90D0: kuài  # 郐\nU+90D1: zhèng  # 郑\nU+90D3: yùn  # 郓\nU+90D7: xī  # 郗\nU+90DA: wú  # 郚\nU+90DB: fú  # 郛\nU+90DC: gào  # 郜\nU+90DD: hǎo  # 郝\nU+90E1: jùn  # 郡\nU+90E2: yǐng  # 郢\nU+90E4: xì  # 郤\nU+90E6: lì  # 郦\nU+90E7: yún  # 郧\nU+90E8: bù  # 部\nU+90EA: qī  # 郪\nU+90EB: pí  # 郫\nU+90ED: guō  # 郭\nU+90EF: tán  # 郯\nU+90F4: chēn  # 郴\nU+90F8: dān  # 郸\nU+90FD: dōu  # 都\nU+90FE: yǎn  # 郾\nU+90FF: méi  # 郿\nU+9100: ruò  # 鄀\nU+9102: è  # 鄂\nU+9103: shū  # 鄃\nU+9104: juàn  # 鄄\nU+9105: yǔ  # 鄅\nU+910C: táng  # 鄌\nU+9111: zī  # 鄑\nU+9117: hào  # 鄗\nU+9118: yōng  # 鄘\nU+9119: bǐ  # 鄙\nU+911A: mào  # 鄚\nU+911C: fū  # 鄜\nU+911E: yín  # 鄞\nU+9120: hù  # 鄠\nU+9122: yān  # 鄢\nU+9123: zhāng  # 鄣\nU+912B: zēng  # 鄫\nU+912F: shàn  # 鄯\nU+9131: pó  # 鄱\nU+9139: zōu  # 鄹\nU+9142: cuó  # 酂\nU+9143: líng  # 酃\nU+9145: xī  # 酅\nU+9146: fēng  # 酆\nU+9149: yǒu  # 酉\nU+914A: dīng  # 酊\nU+914B: qiú  # 酋\nU+914C: zhuó  # 酌\nU+914D: pèi  # 配\nU+914E: zhòu  # 酎\nU+914F: yǐ  # 酏\nU+9150: gān  # 酐\nU+9152: jiǔ  # 酒\nU+9157: xù  # 酗\nU+915A: fēn  # 酚\nU+915D: yùn  # 酝\nU+915E: tài  # 酞\nU+9161: tuó  # 酡\nU+9162: cù  # 酢 -> zuò\nU+9163: hān  # 酣\nU+9164: gū  # 酤\nU+9165: sū  # 酥\nU+9166: pò  # 酦 -> pō\nU+9169: mǐng  # 酩\nU+916A: lào  # 酪\nU+916C: chóu  # 酬\nU+916E: tóng  # 酮\nU+916F: zhǐ  # 酯\nU+9170: xiān  # 酰\nU+9171: jiàng  # 酱\nU+9172: chéng  # 酲\nU+9174: tú  # 酴\nU+9175: jiào  # 酵\nU+9176: méi  # 酶\nU+9177: kù  # 酷\nU+9178: suān  # 酸\nU+9179: lèi  # 酹\nU+917A: pú  # 酺\nU+917D: yàn  # 酽\nU+917E: shāi  # 酾 -> shī\nU+917F: niàng  # 酿\nU+9185: pēi  # 醅\nU+9187: chún  # 醇\nU+9189: zuì  # 醉\nU+918B: cù  # 醋\nU+918C: kūn  # 醌\nU+918D: tí  # 醍\nU+9190: hú  # 醐\nU+9191: xǔ  # 醑\nU+9192: xǐng  # 醒\nU+919A: mí  # 醚\nU+919B: quán  # 醛\nU+91A2: hǎi  # 醢\nU+91A8: lí  # 醨\nU+91AA: láo  # 醪\nU+91AD: bú  # 醭\nU+91AE: jiào  # 醮\nU+91AF: xī  # 醯\nU+91B4: lǐ  # 醴\nU+91B5: jù  # 醵\nU+91BA: xūn  # 醺\nU+91BE: mí  # 醾\nU+91C7: cǎi  # 采\nU+91C9: yòu  # 釉\nU+91CA: shì  # 释\nU+91CC: lǐ  # 里\nU+91CD: zhòng  # 重\nU+91CE: yě  # 野\nU+91CF: liàng  # 量 -> liáng\nU+91D0: lí  # 釐\nU+91D1: jīn  # 金\nU+91DC: fǔ  # 釜\nU+9274: jiàn  # 鉴\nU+928E: qióng  # 銎\nU+92AE: luán  # 銮\nU+92C6: yún  # 鋆\nU+92C8: wù  # 鋈\nU+933E: zàn  # 錾\nU+936A: móu  # 鍪\nU+938F: liú  # 鎏\nU+93CA: ào  # 鏊\nU+93D6: áo  # 鏖\nU+943E: bèi  # 鐾\nU+946B: xīn  # 鑫\nU+9486: gá  # 钆\nU+9487: yǐ  # 钇\nU+9488: zhēn  # 针\nU+9489: dīng  # 钉\nU+948A: zhāo  # 钊\nU+948B: pō  # 钋\nU+948C: liǎo  # 钌\nU+948D: tǔ  # 钍\nU+948E: qiān  # 钎\nU+948F: chuàn  # 钏\nU+9490: shān  # 钐\nU+9492: fán  # 钒\nU+9493: diào  # 钓\nU+9494: mén  # 钔\nU+9495: nǚ  # 钕\nU+9496: yáng  # 钖\nU+9497: chāi  # 钗\nU+9498: xíng  # 钘\nU+9499: gài  # 钙\nU+949A: bù  # 钚\nU+949B: tài  # 钛\nU+949C: jù  # 钜\nU+949D: dùn  # 钝\nU+949E: chāo  # 钞\nU+949F: zhōng  # 钟\nU+94A0: nà  # 钠\nU+94A1: bèi  # 钡\nU+94A2: gāng  # 钢\nU+94A3: bǎn  # 钣\nU+94A4: qián  # 钤\nU+94A5: yào  # 钥 -> yuè\nU+94A6: qīn  # 钦\nU+94A7: jūn  # 钧\nU+94A8: wū  # 钨\nU+94A9: gōu  # 钩\nU+94AA: kàng  # 钪\nU+94AB: fāng  # 钫\nU+94AC: huǒ  # 钬\nU+94AD: tǒu  # 钭 -> dǒu\nU+94AE: niǔ  # 钮\nU+94AF: bǎ  # 钯\nU+94B0: yù  # 钰\nU+94B1: qián  # 钱\nU+94B2: zhēng  # 钲\nU+94B3: qián  # 钳\nU+94B4: gǔ  # 钴\nU+94B5: bō  # 钵\nU+94B7: pǒ  # 钷\nU+94B9: bó  # 钹\nU+94BA: yuè  # 钺\nU+94BB: zuān  # 钻\nU+94BC: mù  # 钼\nU+94BD: tǎn  # 钽\nU+94BE: jiǎ  # 钾\nU+94BF: diàn  # 钿\nU+94C0: yóu  # 铀\nU+94C1: tiě  # 铁\nU+94C2: bó  # 铂\nU+94C3: líng  # 铃\nU+94C4: shuò  # 铄\nU+94C5: qiān  # 铅\nU+94C6: mǎo  # 铆\nU+94C8: shì  # 铈\nU+94C9: xuàn  # 铉\nU+94CA: tā  # 铊\nU+94CB: bì  # 铋\nU+94CC: ní  # 铌\nU+94CD: pī  # 铍 -> pí\nU+94CE: duó  # 铎\nU+94CF: xíng  # 铏\nU+94D0: kào  # 铐\nU+94D1: lǎo  # 铑\nU+94D2: ěr  # 铒\nU+94D5: yǒu  # 铕\nU+94D6: chéng  # 铖\nU+94D7: jiá  # 铗\nU+94D8: yé  # 铘\nU+94D9: náo  # 铙\nU+94DA: zhì  # 铚\nU+94DB: dāng  # 铛\nU+94DC: tóng  # 铜\nU+94DD: lǚ  # 铝\nU+94DE: diào  # 铞\nU+94DF: yīn  # 铟\nU+94E0: kǎi  # 铠\nU+94E1: zhá  # 铡\nU+94E2: zhū  # 铢\nU+94E3: xǐ  # 铣 -> xiǎn\nU+94E4: dìng  # 铤 -> tǐng\nU+94E5: diū  # 铥\nU+94E7: huá  # 铧\nU+94E8: quán  # 铨\nU+94E9: shā  # 铩\nU+94EA: hā  # 铪\nU+94EB: diào  # 铫\nU+94EC: gè  # 铬\nU+94ED: míng  # 铭\nU+94EE: zhēng  # 铮\nU+94EF: sè  # 铯\nU+94F0: jiǎo  # 铰\nU+94F1: yī  # 铱\nU+94F2: chǎn  # 铲\nU+94F3: chòng  # 铳\nU+94F4: tāng  # 铴 -> tàng\nU+94F5: ǎn  # 铵\nU+94F6: yín  # 银\nU+94F7: rú  # 铷\nU+94F8: zhù  # 铸\nU+94F9: láo  # 铹\nU+94FA: pù  # 铺 -> pū\nU+94FB: wú  # 铻\nU+94FC: lái  # 铼\nU+94FD: tè  # 铽\nU+94FE: liàn  # 链\nU+94FF: kēng  # 铿\nU+9500: xiāo  # 销\nU+9501: suǒ  # 锁\nU+9502: lǐ  # 锂\nU+9503: zèng  # 锃\nU+9504: chú  # 锄\nU+9505: guō  # 锅\nU+9506: gào  # 锆\nU+9507: é  # 锇\nU+9508: xiù  # 锈\nU+9509: cuò  # 锉\nU+950A: lüè  # 锊\nU+950B: fēng  # 锋\nU+950C: xīn  # 锌\nU+950D: liǔ  # 锍\nU+950E: kāi  # 锎\nU+950F: jiǎn  # 锏\nU+9510: ruì  # 锐\nU+9511: tī  # 锑\nU+9512: láng  # 锒\nU+9513: qǐn  # 锓\nU+9514: jū  # 锔\nU+9515: ā  # 锕\nU+9516: qiāng  # 锖\nU+9517: zhě  # 锗\nU+9518: nuò  # 锘\nU+9519: cuò  # 错\nU+951A: máo  # 锚\nU+951B: bēn  # 锛\nU+951C: qí  # 锜\nU+951D: dé  # 锝\nU+951E: kè  # 锞\nU+951F: kūn  # 锟\nU+9521: xī  # 锡\nU+9522: gù  # 锢\nU+9523: luó  # 锣\nU+9524: chuí  # 锤\nU+9525: zhuī  # 锥\nU+9526: jǐn  # 锦\nU+9527: zhì  # 锧\nU+9528: xiān  # 锨\nU+9529: juǎn  # 锩\nU+952A: huō  # 锪 -> huò\nU+952B: péi  # 锫\nU+952C: tán  # 锬\nU+952D: dìng  # 锭\nU+952E: jiàn  # 键\nU+952F: jù  # 锯\nU+9530: měng  # 锰\nU+9531: zī  # 锱\nU+9532: qiè  # 锲\nU+9533: yīng  # 锳\nU+9534: kǎi  # 锴\nU+9535: qiāng  # 锵\nU+9536: sī  # 锶\nU+9537: è  # 锷\nU+9538: chā  # 锸\nU+9539: qiāo  # 锹\nU+953A: zhōng  # 锺\nU+953B: duàn  # 锻\nU+953C: sōu  # 锼\nU+953D: huáng  # 锽\nU+953E: huán  # 锾\nU+953F: āi  # 锿\nU+9540: dù  # 镀\nU+9541: měi  # 镁\nU+9542: lòu  # 镂\nU+9543: zī  # 镃\nU+9544: fèi  # 镄\nU+9545: méi  # 镅\nU+9546: mò  # 镆\nU+9547: zhèn  # 镇\nU+9548: bó  # 镈\nU+9549: gé  # 镉\nU+954A: niè  # 镊\nU+954B: tǎng  # 镋\nU+954C: juān  # 镌\nU+954D: niè  # 镍\nU+954E: ná  # 镎\nU+954F: liú  # 镏\nU+9550: gǎo  # 镐\nU+9551: bàng  # 镑\nU+9552: yì  # 镒\nU+9553: jiā  # 镓\nU+9554: bīn  # 镔\nU+9555: róng  # 镕\nU+9556: biāo  # 镖\nU+9557: tāng  # 镗\nU+9558: màn  # 镘\nU+955A: bèng  # 镚\nU+955B: yōng  # 镛\nU+955C: jìng  # 镜\nU+955D: dī  # 镝 -> dí\nU+955E: zú  # 镞\nU+9560: liú  # 镠\nU+9561: chán  # 镡 -> xín\nU+9562: jué  # 镢\nU+9563: liào  # 镣\nU+9564: pú  # 镤\nU+9565: lǔ  # 镥\nU+9566: duì  # 镦 -> duī\nU+9567: lán  # 镧\nU+9568: pǔ  # 镨\nU+9569: cuān  # 镩\nU+956A: qiāng  # 镪 -> qiǎng\nU+956B: dèng  # 镫\nU+956C: huò  # 镬\nU+956D: léi  # 镭\nU+956E: huán  # 镮\nU+956F: zhuó  # 镯\nU+9570: lián  # 镰\nU+9571: yì  # 镱\nU+9572: chǎ  # 镲\nU+9573: biāo  # 镳\nU+9574: là  # 镴\nU+9575: chán  # 镵\nU+9576: xiāng  # 镶\nU+957F: zhǎng  # 长 -> cháng\nU+95E8: mén  # 门\nU+95E9: shuān  # 闩\nU+95EA: shǎn  # 闪\nU+95EB: yán  # 闫\nU+95ED: bì  # 闭\nU+95EE: wèn  # 问\nU+95EF: chuǎng  # 闯\nU+95F0: rùn  # 闰\nU+95F1: wéi  # 闱\nU+95F2: xián  # 闲\nU+95F3: hóng  # 闳\nU+95F4: jiān  # 间\nU+95F5: mǐn  # 闵\nU+95F6: kāng  # 闶 -> kàng\nU+95F7: mèn  # 闷\nU+95F8: zhá  # 闸\nU+95F9: nào  # 闹\nU+95FA: guī  # 闺\nU+95FB: wén  # 闻\nU+95FC: tà  # 闼\nU+95FD: mǐn  # 闽\nU+95FE: lǘ  # 闾\nU+95FF: kǎi  # 闿\nU+9600: fá  # 阀\nU+9601: gé  # 阁\nU+9602: hé  # 阂\nU+9603: kǔn  # 阃\nU+9604: jiū  # 阄\nU+9605: yuè  # 阅\nU+9606: láng  # 阆 -> làng\nU+9607: dū  # 阇\nU+9608: yù  # 阈\nU+9609: yān  # 阉\nU+960A: chāng  # 阊\nU+960B: xì  # 阋\nU+960C: wén  # 阌\nU+960D: hūn  # 阍\nU+960E: yán  # 阎\nU+960F: è  # 阏\nU+9610: chǎn  # 阐\nU+9611: lán  # 阑\nU+9612: qù  # 阒\nU+9614: kuò  # 阔\nU+9615: què  # 阕\nU+9616: hé  # 阖\nU+9617: tián  # 阗\nU+9618: dá  # 阘 -> tà\nU+9619: quē  # 阙\nU+961A: hǎn  # 阚 -> kàn\nU+961C: fù  # 阜\nU+961F: duì  # 队\nU+9621: qiān  # 阡\nU+962A: bǎn  # 阪\nU+962E: ruǎn  # 阮\nU+9631: jǐng  # 阱\nU+9632: fáng  # 防\nU+9633: yáng  # 阳\nU+9634: yīn  # 阴\nU+9635: zhèn  # 阵\nU+9636: jiē  # 阶\nU+963B: zǔ  # 阻\nU+963C: zuò  # 阼\nU+963D: diàn  # 阽\nU+963F: ā  # 阿\nU+9640: tuó  # 陀\nU+9642: bēi  # 陂\nU+9644: fù  # 附\nU+9645: jì  # 际\nU+9646: lù  # 陆\nU+9647: lǒng  # 陇\nU+9648: chén  # 陈\nU+9649: xíng  # 陉\nU+964B: lòu  # 陋\nU+964C: mò  # 陌\nU+964D: jiàng  # 降\nU+964E: shū  # 陎\nU+9650: xiàn  # 限\nU+9651: ér  # 陑\nU+9654: gāi  # 陔\nU+9655: shǎn  # 陕\nU+965B: bì  # 陛\nU+965E: shēng  # 陞\nU+965F: zhì  # 陟\nU+9661: dǒu  # 陡\nU+9662: yuàn  # 院\nU+9664: chú  # 除\nU+9667: niè  # 陧\nU+9668: yǔn  # 陨\nU+9669: xiǎn  # 险\nU+966A: péi  # 陪\nU+966C: zōu  # 陬\nU+9672: chuí  # 陲\nU+9674: pí  # 陴 -> pī\nU+9675: líng  # 陵\nU+9676: táo  # 陶\nU+9677: xiàn  # 陷\nU+9683: shù  # 隃 -> yú\nU+9685: yú  # 隅\nU+9686: lóng  # 隆\nU+9688: wēi  # 隈\nU+968B: suí  # 隋\nU+968D: huáng  # 隍\nU+968F: suí  # 随\nU+9690: yǐn  # 隐\nU+9694: gé  # 隔\nU+9697: kuí  # 隗 -> wěi\nU+9698: ài  # 隘\nU+9699: xì  # 隙\nU+969C: zhàng  # 障\nU+96A7: suì  # 隧\nU+96A9: ào  # 隩\nU+96B0: xí  # 隰\nU+96B3: huī  # 隳\nU+96B6: lì  # 隶\nU+96B9: zhuī  # 隹\nU+96BA: hú  # 隺\nU+96BC: sǔn  # 隼\nU+96BD: juàn  # 隽 -> jùn\nU+96BE: nán  # 难\nU+96C0: què  # 雀\nU+96C1: yàn  # 雁\nU+96C4: xióng  # 雄\nU+96C5: yǎ  # 雅\nU+96C6: jí  # 集\nU+96C7: gù  # 雇\nU+96C9: zhì  # 雉\nU+96CA: gòu  # 雊\nU+96CC: cí  # 雌\nU+96CD: yōng  # 雍\nU+96CE: jū  # 雎\nU+96CF: chú  # 雏\nU+96D2: luò  # 雒\nU+96D5: diāo  # 雕\nU+96E0: chóu  # 雠\nU+96E8: yǔ  # 雨\nU+96E9: yú  # 雩\nU+96EA: xuě  # 雪\nU+96EF: wén  # 雯\nU+96F1: pāng  # 雱\nU+96F3: lì  # 雳\nU+96F6: líng  # 零\nU+96F7: léi  # 雷\nU+96F9: báo  # 雹\nU+96FE: wù  # 雾\nU+9700: xū  # 需\nU+9701: jì  # 霁\nU+9704: xiāo  # 霄\nU+9705: zhà  # 霅 -> zhá\nU+9706: tíng  # 霆\nU+9707: zhèn  # 震\nU+9708: pèi  # 霈\nU+9709: méi  # 霉\nU+970D: huò  # 霍\nU+970E: shà  # 霎\nU+970F: fēi  # 霏\nU+9713: ní  # 霓\nU+9716: lín  # 霖\nU+971C: shuāng  # 霜\nU+971E: xiá  # 霞\nU+9728: wèi  # 霨\nU+972A: yín  # 霪\nU+972D: ǎi  # 霭\nU+9730: xiàn  # 霰\nU+9732: lù  # 露 -> lòu\nU+9738: bà  # 霸\nU+9739: pī  # 霹\nU+973E: mái  # 霾\nU+9752: qīng  # 青\nU+9753: jìng  # 靓\nU+9756: jìng  # 靖\nU+9759: jìng  # 静\nU+975B: diàn  # 靛\nU+975E: fēi  # 非\nU+9760: kào  # 靠\nU+9761: mí  # 靡\nU+9762: miàn  # 面\nU+9765: yè  # 靥\nU+9769: gé  # 革\nU+976C: qián  # 靬 -> jiān\nU+9770: wù  # 靰\nU+9773: jìn  # 靳\nU+9774: xuē  # 靴\nU+9776: bǎ  # 靶\nU+9778: sǎ  # 靸\nU+977A: mò  # 靺\nU+977C: dá  # 靼\nU+977D: bàn  # 靽\nU+977F: yào  # 靿\nU+9781: bèi  # 鞁\nU+9785: yāng  # 鞅\nU+978B: xié  # 鞋\nU+978D: ān  # 鞍\nU+9791: dá  # 鞑\nU+9792: qiáo  # 鞒\nU+9794: mán  # 鞔\nU+9798: qiào  # 鞘\nU+97A0: jū  # 鞠\nU+97A1: la  # 鞡\nU+97A3: róu  # 鞣\nU+97A7: qiū  # 鞧\nU+97A8: hé  # 鞨\nU+97AB: jū  # 鞫\nU+97AC: jiān  # 鞬 -> jiàn\nU+97AD: biān  # 鞭\nU+97AE: dī  # 鞮\nU+97AF: jiān  # 鞯\nU+97B2: gōu  # 鞲\nU+97B3: tà  # 鞳\nU+97B4: bèi  # 鞴\nU+97C2: chàn  # 韂\nU+97E6: wéi  # 韦\nU+97E7: rèn  # 韧\nU+97E8: fú  # 韨\nU+97E9: hán  # 韩\nU+97EA: wěi  # 韪\nU+97EB: yùn  # 韫\nU+97EC: tāo  # 韬\nU+97ED: jiǔ  # 韭\nU+97F3: yīn  # 音\nU+97F5: yùn  # 韵\nU+97F6: sháo  # 韶\nU+9875: yè  # 页\nU+9876: dǐng  # 顶\nU+9877: qǐng  # 顷\nU+9878: hān  # 顸\nU+9879: xiàng  # 项\nU+987A: shùn  # 顺\nU+987B: xū  # 须\nU+987C: xū  # 顼\nU+987D: wán  # 顽\nU+987E: gù  # 顾\nU+987F: dùn  # 顿\nU+9880: qí  # 颀\nU+9881: bān  # 颁\nU+9882: sòng  # 颂\nU+9883: háng  # 颃\nU+9884: yù  # 预\nU+9885: lú  # 颅\nU+9886: lǐng  # 领\nU+9887: pǒ  # 颇 -> pō\nU+9888: jǐng  # 颈\nU+9889: jié  # 颉\nU+988A: jiá  # 颊\nU+988B: tǐng  # 颋\nU+988C: hé  # 颌\nU+988D: yǐng  # 颍\nU+988E: jiǒng  # 颎\nU+988F: kē  # 颏\nU+9890: yí  # 颐\nU+9891: pín  # 频\nU+9893: tuí  # 颓\nU+9894: hàn  # 颔\nU+9896: yǐng  # 颖\nU+9897: kē  # 颗\nU+9898: tí  # 题\nU+9899: yóng  # 颙\nU+989A: è  # 颚\nU+989B: zhuān  # 颛\nU+989C: yán  # 颜\nU+989D: é  # 额\nU+989E: niè  # 颞\nU+989F: mān  # 颟\nU+98A0: diān  # 颠\nU+98A1: sǎng  # 颡\nU+98A2: hào  # 颢\nU+98A4: chàn  # 颤\nU+98A5: rú  # 颥\nU+98A6: pín  # 颦\nU+98A7: quán  # 颧\nU+98CE: fēng  # 风\nU+98CF: yáng  # 飏\nU+98D0: zhǎn  # 飐\nU+98D1: biāo  # 飑\nU+98D2: sà  # 飒\nU+98D3: jù  # 飓\nU+98D4: sī  # 飔\nU+98D5: sōu  # 飕\nU+98D7: liú  # 飗\nU+98D8: piāo  # 飘\nU+98D9: biāo  # 飙\nU+98DE: fēi  # 飞\nU+98DF: shí  # 食\nU+98E7: sūn  # 飧\nU+98E8: xiǎng  # 飨\nU+990D: yàn  # 餍\nU+9910: cān  # 餐\nU+992E: tiè  # 餮\nU+9954: yōng  # 饔\nU+9955: tāo  # 饕\nU+9965: jī  # 饥\nU+9967: táng  # 饧 -> xíng\nU+9968: tún  # 饨\nU+9969: xì  # 饩\nU+996A: rèn  # 饪\nU+996B: yù  # 饫\nU+996C: chì  # 饬\nU+996D: fàn  # 饭\nU+996E: yǐn  # 饮\nU+996F: jiàn  # 饯\nU+9970: shì  # 饰\nU+9971: bǎo  # 饱\nU+9972: sì  # 饲\nU+9973: duò  # 饳\nU+9974: yí  # 饴\nU+9975: ěr  # 饵\nU+9976: ráo  # 饶\nU+9977: xiǎng  # 饷\nU+9978: hé  # 饸\nU+9979: le  # 饹 -> gē\nU+997A: jiǎo  # 饺\nU+997B: xī  # 饻\nU+997C: bǐng  # 饼\nU+997D: bō  # 饽\nU+997F: è  # 饿\nU+9981: něi  # 馁\nU+9983: guǒ  # 馃\nU+9984: hún  # 馄\nU+9985: xiàn  # 馅\nU+9986: guǎn  # 馆\nU+9987: chā  # 馇\nU+9988: kuì  # 馈\nU+9989: gǔ  # 馉\nU+998A: sōu  # 馊\nU+998B: chán  # 馋\nU+998C: yè  # 馌\nU+998D: mó  # 馍\nU+998F: liú  # 馏 -> liù\nU+9990: xiū  # 馐\nU+9991: jǐn  # 馑\nU+9992: mán  # 馒\nU+9993: sǎn  # 馓\nU+9994: zhuàn  # 馔\nU+9995: náng  # 馕\nU+9996: shǒu  # 首\nU+9997: kuí  # 馗\nU+9998: guó  # 馘\nU+9999: xiāng  # 香\nU+999D: bì  # 馝\nU+999E: bó  # 馞\nU+99A5: fù  # 馥\nU+99A7: yūn  # 馧\nU+99A8: xīn  # 馨\nU+9A6C: mǎ  # 马\nU+9A6D: yù  # 驭\nU+9A6E: tuó  # 驮\nU+9A6F: xùn  # 驯\nU+9A70: chí  # 驰\nU+9A71: qū  # 驱\nU+9A72: rì  # 驲\nU+9A73: bó  # 驳\nU+9A74: lǘ  # 驴\nU+9A75: zǎng  # 驵\nU+9A76: shǐ  # 驶\nU+9A77: sì  # 驷\nU+9A78: fù  # 驸\nU+9A79: jū  # 驹\nU+9A7A: zōu  # 驺\nU+9A7B: zhù  # 驻\nU+9A7C: tuó  # 驼\nU+9A7D: nú  # 驽\nU+9A7E: jià  # 驾\nU+9A7F: yì  # 驿\nU+9A80: dài  # 骀 -> tái\nU+9A81: xiāo  # 骁\nU+9A82: mà  # 骂\nU+9A83: yīn  # 骃\nU+9A84: jiāo  # 骄\nU+9A85: huá  # 骅\nU+9A86: luò  # 骆\nU+9A87: hài  # 骇\nU+9A88: pián  # 骈\nU+9A89: biāo  # 骉\nU+9A8A: lí  # 骊\nU+9A8B: chěng  # 骋\nU+9A8C: yàn  # 验\nU+9A8D: xīng  # 骍\nU+9A8E: qīn  # 骎\nU+9A8F: jùn  # 骏\nU+9A90: qí  # 骐\nU+9A91: qí  # 骑\nU+9A92: kè  # 骒\nU+9A93: zhuī  # 骓\nU+9A95: sù  # 骕\nU+9A96: cān  # 骖\nU+9A97: piàn  # 骗\nU+9A98: zhì  # 骘\nU+9A99: kuí  # 骙\nU+9A9A: sāo  # 骚\nU+9A9B: wù  # 骛\nU+9A9C: ào  # 骜 -> áo\nU+9A9D: liú  # 骝\nU+9A9E: qiān  # 骞\nU+9A9F: shàn  # 骟\nU+9AA0: biāo  # 骠 -> piào\nU+9AA1: luó  # 骡\nU+9AA2: cōng  # 骢\nU+9AA3: chǎn  # 骣\nU+9AA4: zhòu  # 骤\nU+9AA5: jì  # 骥\nU+9AA6: shuāng  # 骦\nU+9AA7: xiāng  # 骧\nU+9AA8: gǔ  # 骨\nU+9AB0: tóu  # 骰\nU+9AB1: jiè  # 骱\nU+9AB6: dǐ  # 骶\nU+9AB7: kū  # 骷\nU+9AB8: hái  # 骸\nU+9ABA: hóu  # 骺\nU+9ABC: gé  # 骼\nU+9AC0: bì  # 髀\nU+9AC1: kē  # 髁\nU+9AC2: qià  # 髂\nU+9AC3: yú  # 髃\nU+9AC5: lóu  # 髅\nU+9ACB: kuān  # 髋\nU+9ACC: bìn  # 髌\nU+9ACE: liáo  # 髎\nU+9AD1: dú  # 髑\nU+9AD3: suǐ  # 髓\nU+9AD8: gāo  # 高\nU+9AE1: kūn  # 髡\nU+9AE2: dí  # 髢\nU+9AE6: máo  # 髦\nU+9AEB: tiáo  # 髫\nU+9AED: zī  # 髭\nU+9AEF: rán  # 髯\nU+9AF9: xiū  # 髹\nU+9AFB: jì  # 髻\nU+9AFD: zhuā  # 髽\nU+9B03: zōng  # 鬃\nU+9B08: quán  # 鬈\nU+9B0F: jiū  # 鬏\nU+9B12: zhěn  # 鬒\nU+9B13: bìn  # 鬓\nU+9B18: mán  # 鬘\nU+9B1F: huán  # 鬟\nU+9B23: liè  # 鬣\nU+9B2F: chàng  # 鬯\nU+9B32: gé  # 鬲\nU+9B36: guī  # 鬶\nU+9B37: zōng  # 鬷\nU+9B3B: yù  # 鬻\nU+9B3C: guǐ  # 鬼\nU+9B41: kuí  # 魁\nU+9B42: hún  # 魂\nU+9B43: bá  # 魃\nU+9B44: pò  # 魄\nU+9B45: mèi  # 魅\nU+9B46: xū  # 魆\nU+9B47: yǎn  # 魇\nU+9B48: xiāo  # 魈\nU+9B49: liǎng  # 魉\nU+9B4B: tuí  # 魋\nU+9B4D: wǎng  # 魍\nU+9B4F: wèi  # 魏\nU+9B51: chī  # 魑\nU+9B54: mó  # 魔\nU+9C7C: yú  # 鱼\nU+9C7D: dāo  # 鱽\nU+9C7E: jǐ  # 鱾\nU+9C7F: yóu  # 鱿\nU+9C80: tún  # 鲀\nU+9C81: lǔ  # 鲁\nU+9C82: fáng  # 鲂\nU+9C83: bā  # 鲃\nU+9C85: bà  # 鲅\nU+9C86: píng  # 鲆\nU+9C87: nián  # 鲇\nU+9C88: lú  # 鲈\nU+9C89: yóu  # 鲉\nU+9C8A: zhǎ  # 鲊\nU+9C8B: fù  # 鲋\nU+9C8C: bà  # 鲌 -> bó\nU+9C8D: bào  # 鲍\nU+9C8E: hòu  # 鲎\nU+9C8F: pí  # 鲏\nU+9C90: tái  # 鲐\nU+9C91: guī  # 鲑\nU+9C92: jié  # 鲒\nU+9C94: wěi  # 鲔\nU+9C95: ér  # 鲕\nU+9C96: tóng  # 鲖\nU+9C97: zéi  # 鲗\nU+9C98: hòu  # 鲘\nU+9C99: kuài  # 鲙\nU+9C9A: jì  # 鲚\nU+9C9B: jiāo  # 鲛\nU+9C9C: xiān  # 鲜\nU+9C9D: zhǎ  # 鲝\nU+9C9E: xiǎng  # 鲞\nU+9C9F: xún  # 鲟\nU+9CA0: gěng  # 鲠\nU+9CA1: lí  # 鲡\nU+9CA2: lián  # 鲢\nU+9CA3: jiān  # 鲣\nU+9CA4: lǐ  # 鲤\nU+9CA5: shí  # 鲥\nU+9CA6: tiáo  # 鲦\nU+9CA7: gǔn  # 鲧\nU+9CA8: shā  # 鲨\nU+9CA9: huàn  # 鲩\nU+9CAA: jūn  # 鲪\nU+9CAB: jì  # 鲫\nU+9CAC: yǒng  # 鲬\nU+9CAD: qīng  # 鲭\nU+9CAE: líng  # 鲮\nU+9CAF: qí  # 鲯\nU+9CB0: zōu  # 鲰\nU+9CB1: fēi  # 鲱\nU+9CB2: kūn  # 鲲\nU+9CB3: chāng  # 鲳\nU+9CB4: gù  # 鲴\nU+9CB5: ní  # 鲵\nU+9CB7: diāo  # 鲷\nU+9CB8: jīng  # 鲸\nU+9CB9: shēn  # 鲹\nU+9CBA: shī  # 鲺\nU+9CBB: zī  # 鲻\nU+9CBC: fèn  # 鲼\nU+9CBD: dié  # 鲽\nU+9CBE: bī  # 鲾\nU+9CBF: cháng  # 鲿\nU+9CC0: tí  # 鳀\nU+9CC1: wēn  # 鳁\nU+9CC2: wēi  # 鳂\nU+9CC3: sāi  # 鳃\nU+9CC4: è  # 鳄\nU+9CC5: qiū  # 鳅\nU+9CC7: huáng  # 鳇\nU+9CC8: quán  # 鳈\nU+9CC9: jiāng  # 鳉\nU+9CCA: biān  # 鳊\nU+9CCC: áo  # 鳌\nU+9CCD: qí  # 鳍\nU+9CCE: tǎ  # 鳎\nU+9CCF: guān  # 鳏\nU+9CD0: yáo  # 鳐\nU+9CD1: páng  # 鳑\nU+9CD2: jiān  # 鳒\nU+9CD3: lè  # 鳓\nU+9CD4: biào  # 鳔\nU+9CD5: xuě  # 鳕\nU+9CD6: biē  # 鳖\nU+9CD7: mán  # 鳗\nU+9CD8: mǐn  # 鳘\nU+9CD9: yōng  # 鳙\nU+9CDA: wèi  # 鳚\nU+9CDB: xí  # 鳛\nU+9CDC: guì  # 鳜\nU+9CDD: shàn  # 鳝\nU+9CDE: lín  # 鳞\nU+9CDF: zūn  # 鳟\nU+9CE0: hù  # 鳠\nU+9CE1: gǎn  # 鳡\nU+9CE2: lǐ  # 鳢\nU+9CE3: zhān  # 鳣\nU+9CE4: guǎn  # 鳤\nU+9E1F: niǎo  # 鸟\nU+9E20: jiū  # 鸠\nU+9E21: jī  # 鸡\nU+9E22: yuān  # 鸢\nU+9E23: míng  # 鸣\nU+9E24: shī  # 鸤\nU+9E25: ōu  # 鸥\nU+9E26: yā  # 鸦\nU+9E27: cāng  # 鸧\nU+9E28: bǎo  # 鸨\nU+9E29: zhèn  # 鸩\nU+9E2A: gū  # 鸪\nU+9E2B: dōng  # 鸫\nU+9E2C: lú  # 鸬\nU+9E2D: yā  # 鸭\nU+9E2E: xiāo  # 鸮\nU+9E2F: yāng  # 鸯\nU+9E30: líng  # 鸰\nU+9E31: chī  # 鸱\nU+9E32: qú  # 鸲\nU+9E33: yuān  # 鸳\nU+9E35: tuó  # 鸵\nU+9E36: sī  # 鸶\nU+9E37: zhì  # 鸷\nU+9E38: ér  # 鸸\nU+9E39: guā  # 鸹\nU+9E3A: xiū  # 鸺\nU+9E3B: héng  # 鸻\nU+9E3C: zhōu  # 鸼\nU+9E3D: gē  # 鸽\nU+9E3E: luán  # 鸾\nU+9E3F: hóng  # 鸿\nU+9E40: wú  # 鹀\nU+9E41: bó  # 鹁\nU+9E42: lí  # 鹂\nU+9E43: juān  # 鹃\nU+9E44: gǔ  # 鹄 -> hú\nU+9E45: é  # 鹅\nU+9E46: yù  # 鹆\nU+9E47: xián  # 鹇\nU+9E48: tí  # 鹈\nU+9E49: wǔ  # 鹉\nU+9E4A: què  # 鹊\nU+9E4B: miáo  # 鹋\nU+9E4C: ān  # 鹌\nU+9E4D: kūn  # 鹍\nU+9E4E: bēi  # 鹎\nU+9E4F: péng  # 鹏\nU+9E50: qiān  # 鹐\nU+9E51: chún  # 鹑\nU+9E52: gēng  # 鹒\nU+9E54: sù  # 鹔\nU+9E55: hú  # 鹕\nU+9E56: hé  # 鹖\nU+9E57: è  # 鹗\nU+9E58: gǔ  # 鹘\nU+9E59: qiū  # 鹙\nU+9E5A: cí  # 鹚\nU+9E5B: méi  # 鹛\nU+9E5C: wù  # 鹜\nU+9E5D: yì  # 鹝\nU+9E5E: yào  # 鹞\nU+9E5F: wēng  # 鹟\nU+9E60: liú  # 鹠\nU+9E61: jí  # 鹡 -> jī\nU+9E62: yì  # 鹢\nU+9E63: jiān  # 鹣\nU+9E64: hè  # 鹤\nU+9E66: yīng  # 鹦\nU+9E67: zhè  # 鹧\nU+9E68: liù  # 鹨\nU+9E69: liáo  # 鹩\nU+9E6A: jiāo  # 鹪\nU+9E6B: jiù  # 鹫\nU+9E6C: yù  # 鹬\nU+9E6D: lù  # 鹭\nU+9E6E: huán  # 鹮\nU+9E6F: zhān  # 鹯\nU+9E70: yīng  # 鹰\nU+9E71: hù  # 鹱\nU+9E72: méng  # 鹲\nU+9E73: guàn  # 鹳\nU+9E74: shuāng  # 鹴\nU+9E7E: cuó  # 鹾\nU+9E7F: lù  # 鹿\nU+9E80: yōu  # 麀\nU+9E82: jǐ  # 麂\nU+9E87: jūn  # 麇\nU+9E88: zhǔ  # 麈\nU+9E8B: mí  # 麋\nU+9E91: ní  # 麑\nU+9E92: qí  # 麒\nU+9E93: lù  # 麓\nU+9E96: jīng  # 麖\nU+9E9D: shè  # 麝\nU+9E9F: lín  # 麟\nU+9EA6: mài  # 麦\nU+9EB8: fū  # 麸\nU+9EB9: qū  # 麹\nU+9EBB: má  # 麻\nU+9EBD: mó  # 麽\nU+9EBE: huī  # 麾\nU+9EC4: huáng  # 黄\nU+9EC7: tiān  # 黇\nU+9EC9: hóng  # 黉\nU+9ECD: shǔ  # 黍\nU+9ECE: lí  # 黎\nU+9ECF: nián  # 黏\nU+9ED1: hēi  # 黑\nU+9ED4: qián  # 黔\nU+9ED8: mò  # 默\nU+9EDB: dài  # 黛\nU+9EDC: chù  # 黜\nU+9EDD: yǒu  # 黝\nU+9EDF: yī  # 黟\nU+9EE0: xiá  # 黠\nU+9EE1: yǎn  # 黡\nU+9EE2: qū  # 黢\nU+9EE5: qíng  # 黥\nU+9EE7: lí  # 黧\nU+9EE9: dú  # 黩\nU+9EEA: cǎn  # 黪\nU+9EEF: àn  # 黯\nU+9EF9: zhǐ  # 黹\nU+9EFB: fú  # 黻\nU+9EFC: fǔ  # 黼\nU+9EFE: mǐn  # 黾\nU+9F0B: yuán  # 鼋\nU+9F0D: tuó  # 鼍\nU+9F0E: dǐng  # 鼎\nU+9F10: nài  # 鼐\nU+9F12: zī  # 鼒\nU+9F13: gǔ  # 鼓\nU+9F17: táo  # 鼗\nU+9F19: pí  # 鼙\nU+9F20: shǔ  # 鼠\nU+9F22: fén  # 鼢\nU+9F29: qú  # 鼩\nU+9F2B: shí  # 鼫\nU+9F2C: yòu  # 鼬\nU+9F2F: wú  # 鼯\nU+9F31: jīng  # 鼱\nU+9F37: xī  # 鼷\nU+9F39: yǎn  # 鼹\nU+9F3B: bí  # 鼻\nU+9F3D: qiú  # 鼽\nU+9F3E: hān  # 鼾\nU+9F41: hōu  # 齁\nU+9F47: zhā  # 齇\nU+9F49: nàng  # 齉\nU+9F50: qí  # 齐\nU+9F51: jī  # 齑\nU+9F7F: chǐ  # 齿\nU+9F80: chèn  # 龀\nU+9F81: hé  # 龁\nU+9F82: yín  # 龂\nU+9F83: jǔ  # 龃\nU+9F84: líng  # 龄\nU+9F85: bāo  # 龅\nU+9F86: tiáo  # 龆\nU+9F87: zī  # 龇\nU+9F88: kěn  # 龈 -> yín\nU+9F89: yǔ  # 龉\nU+9F8A: chuò  # 龊\nU+9F8B: qǔ  # 龋\nU+9F8C: wò  # 龌\nU+9F99: lóng  # 龙\nU+9F9A: gōng  # 龚\nU+9F9B: kān  # 龛\nU+9F9F: guī  # 龟\nU+9FA0: yuè  # 龠\nU+9FA2: hé  # 龢\nU+9FCD: gàng  # 鿍\nU+9FCE: tǎ  # 鿎\nU+9FCF: mài  # 鿏\n# Extension A (77)\nU+3447: zhòu  # 㑇\nU+344A: yì  # 㑊\nU+356E: fǔ  # 㕮\nU+360E: hǎn  # 㘎\nU+364D: duō  # 㙍\nU+3658: yāo  # 㙘\nU+3666: xié  # 㙦\nU+36C3: jié  # 㛃\nU+36DA: tǒng  # 㛚\nU+36F9: pián  # 㛹\nU+37C3: sī  # 㟃\nU+3807: jiù  # 㠇\nU+3813: méng  # 㠓\nU+3918: zhòu  # 㤘 -> chù\nU+3944: líng  # 㥄\nU+39D0: sǒng  # 㧐\nU+39D1: huī  # 㧑 -> wéi\nU+39DF: kuǎi  # 㧟\nU+3AF0: làng  # 㫰 -> lǎng\nU+3B0A: huàn  # 㬊 -> huǎn\nU+3B0E: xiǎn  # 㬎\nU+3B1A: chè  # 㬚\nU+3B4E: gāng  # 㭎\nU+3B55: qū  # 㭕\nU+3BBE: lǎng  # 㮾\nU+3C00: lí  # 㰀\nU+3CC7: fù  # 㳇\nU+3CD8: chōng  # 㳘\nU+3CDA: xù  # 㳚 -> yù\nU+3D14: xī  # 㴔 -> sè\nU+3D50: jué  # 㵐\nU+3DB2: yòng  # 㶲\nU+3E06: kào  # 㸆\nU+3E0C: huò  # 㸌\nU+3E84: yǔ  # 㺄\nU+3EEC: tū  # 㻬 -> tú\nU+3F4F: gàn  # 㽏\nU+3FE0: huàng  # 㿠\nU+4056: lōu  # 䁖\nU+40AE: lüè  # 䂮\nU+40C5: dī  # 䃅\nU+40CE: zhà  # 䃎 -> zhǎ\nU+415F: cǎn  # 䅟\nU+4339: jiǒng  # 䌹\nU+4383: rǎn  # 䎃\nU+4396: zēng  # 䎖\nU+43DD: zhuān  # 䏝 -> chún\nU+43E1: shì  # 䏡\nU+43F2: dié  # 䏲\nU+4403: jùn  # 䐃 -> jiǒng\nU+44D6: qióng  # 䓖\nU+44DB: qū  # 䓛 -> fǔ\nU+44E8: yīng  # 䓨\nU+44EB: qí  # 䓫 -> jì\nU+44EC: zhuó  # 䓬\nU+45D6: dì  # 䗖\nU+45DB: xiū  # 䗛\nU+45EA: zhè  # 䗪\nU+45F4: tíng  # 䗴\nU+4723: xīn  # 䜣 -> xī\nU+4759: chū  # 䝙\nU+48BA: chū  # 䢺\nU+48BC: gōng  # 䢼\nU+48D8: táng  # 䣘\nU+497D: pō  # 䥽\nU+4983: zhuō  # 䦃\nU+4C9F: yìn  # 䲟\nU+4CA0: chūn  # 䲠\nU+4CA2: téng  # 䲢\nU+4D13: shī  # 䴓\nU+4D14: jiāo  # 䴔\nU+4D15: liè  # 䴕\nU+4D16: jīng  # 䴖\nU+4D17: jú  # 䴗\nU+4D18: tī  # 䴘\nU+4D19: pì  # 䴙\nU+4DAE: yǎn  # 䶮\n# Extension B (36)\nU+20164: xí  # 𠅤\nU+20676: ǒu  # 𠙶\nU+20CD0: bāng  # 𠳐\nU+2139A: piǎn  # 𡎚\nU+21413: kāng  # 𡐓\nU+235CB: dǎng  # 𣗋\nU+23C97: wéi  # 𣲗\nU+23C98: wǔ  # 𣲘\nU+23E23: fén  # 𣸣\nU+249DB: dì  # 𤧛\nU+24A7D: huán  # 𤩽\nU+24AC9: xiè  # 𤫉\nU+25532: è  # 𥔲\nU+25562: cáo  # 𥕢\nU+255A8: zào  # 𥖨\nU+25ED7: chá  # 𥻗\nU+26221: xū  # 𦈡\nU+2648D: tóng  # 𦒍\nU+26676: gǔ  # 𦙶\nU+2677C: lǘ  # 𦝼\nU+26B5C: zhī  # 𦭜\nU+26C21: nà  # 𦰡\nU+27FF9: mǔ  # 𧿹\nU+28408: guāng  # 𨐈\nU+28678: qí  # 𨙸\nU+28695: biàn  # 𨚕\nU+287E0: quān  # 𨟠 -> què\nU+28B49: bān  # 𨭉\nU+28C47: qiú  # 𨱇\nU+28C4F: dā  # 𨱏\nU+28C51: huáng  # 𨱑\nU+28C54: zūn  # 𨱔\nU+28E99: nì  # 𨺙\nU+29F7E: ān  # 𩽾 -> ān\nU+29F83: miǎn  # 𩾃\nU+29F8C: kāng  # 𩾌 -> kāng\n# Extension C (44)\nU+2A7DD: jì  # 𪟝\nU+2A8FB: lóu  # 𪣻\nU+2A917: liào  # 𪤗\nU+2AA30: qū  # 𪨰\nU+2AA36: shē  # 𪨶\nU+2AA58: yǎn  # 𪩘\nU+2AFA2: xiàn  # 𪾢\nU+2B127: yán  # 𫄧\nU+2B128: chī  # 𫄨\nU+2B137: yì  # 𫄷\nU+2B138: xūn  # 𫄸\nU+2B1ED: wěi  # 𫇭\nU+2B300: jī  # 𫌀\nU+2B363: tóng  # 𫍣\nU+2B36F: xián  # 𫍯\nU+2B372: xiǎo  # 𫍲\nU+2B37D: xuān  # 𫍽\nU+2B404: yuè  # 𫐄\nU+2B410: ní  # 𫐐\nU+2B413: bù  # 𫐓\nU+2B461: méng  # 𫑡\nU+2B4E7: fū  # 𫓧\nU+2B4EF: jī  # 𫓯\nU+2B4F6: xuān  # 𫓶\nU+2B4F9: jī  # 𫓹\nU+2B50D: fán  # 𫔍\nU+2B50E: jué  # 𫔎\nU+2B536: niè  # 𫔶\nU+2B5AE: yǐ  # 𫖮\nU+2B5AF: fǔ  # 𫖯\nU+2B5B3: yūn  # 𫖳\nU+2B5E7: sù  # 𫗧\nU+2B5F4: zhān  # 𫗴\nU+2B61C: wén  # 𫘜\nU+2B61D: jué  # 𫘝\nU+2B626: táo  # 𫘦  => U+9A0A\nU+2B627: lù  # 𫘧  => U+9A04\nU+2B628: tí  # 𫘨\nU+2B62A: yuán  # 𫘪  => U+9A35\nU+2B62C: xí  # 𫘬  => U+9A31\nU+2B695: shī  # 𫚕\nU+2B696: cǐ  # 𫚖  => U+9B86  ?-> jì\nU+2B6AD: liè  # 𫚭  => U+9C72\nU+2B6ED: kuáng  # 𫛭  => U+9D5F\n# Extension D (8)\nU+2B7A9: mén  # 𫞩\nU+2B7C5: liáng  # 𫟅\nU+2B7E6: suì  # 𫟦\nU+2B7F9: hóng  # 𫟹\nU+2B7FC: dá  # 𫟼\nU+2B806: kuǐ  # 𫠆\nU+2B80A: xuán  # 𫠊\nU+2B81C: ní  # 𫠜\n# Extension E (108)\nU+2B8B8: dàn  # 𫢸  => U+50E4\nU+2BAC7: ě  # 𫫇  => U+5641\nU+2BB5F: ōu  # 𫭟  => U+5878  ?-> qiū\nU+2BB62: lǔn  # 𫭢  => U+57E8\nU+2BB7C: láo  # 𫭼  => U+2144D\nU+2BB83: shàn  # 𫮃  => U+58A0\nU+2BC1B: xíng  # 𫰛  => U+5A19\nU+2BD77: lì  # 𫵷  => U+3823\nU+2BD87: dié  # 𫶇  => U+5D7D  ?-> dì\nU+2BDF7: xīn  # 𫷷  => U+5EDE\nU+2BE29: kōu  # 𫸩  => U+5F44\nU+2C029: wěi  # 𬀩  => U+6690\nU+2C02A: xiàn  # 𬀪  => U+665B\nU+2C0A9: jiā  # 𬂩  => U+689C\nU+2C0CA: zhì  # 𬃊  => U+6ACD\nU+2C1D5: wàn  # 𬇕  => U+6FAB  ?-> màn\nU+2C1D9: pèi  # 𬇙  => U+6D7F\nU+2C1F9: guó  # 𬇹  => U+6F0D\nU+2C27C: ōu  # 𬉼  => U+71B0  ?-> ǒu\nU+2C288: xún  # 𬊈  => U+71D6\nU+2C2A4: chǎn  # 𬊤  => U+71C0  ?-> dǎn,chàn\nU+2C317: hé  # 𬌗\nU+2C35B: lì  # 𬍛  => U+74C5\nU+2C361: dàng  # 𬍡  => U+7497\nU+2C364: xún  # 𬍤  => U+7495\nU+2C488: què  # 𬒈  => U+7910  ?-> hú\nU+2C494: gěng  # 𬒔\nU+2C497: lán  # 𬒗\nU+2C542: gōng  # 𬕂  => U+7BE2  ?-> gǎn,lǒng\nU+2C613: xún  # 𬘓  => U+7D03\nU+2C618: dǎn  # 𬘘  => U+7D1E\nU+2C621: yīn  # 𬘡  => U+7D6A\nU+2C629: tīng  # 𬘩  => U+7D8E\nU+2C62B: huán  # 𬘫  => U+7D84  ?-> huàn,wàn\nU+2C62C: qiàn  # 𬘬  => U+7DAA  ?-> qīng,zhēng\nU+2C62D: lín  # 𬘭  => U+7D9D  ?-> chēn\nU+2C62F: zhǔn  # 𬘯  => U+7DA7  ?-> zhùn\nU+2C642: yǎn  # 𬙂  => U+7E2F  ?-> yǐn\nU+2C64A: mò  # 𬙊  => U+7E86\nU+2C64B: xiāng  # 𬙋  => U+7E95  ?-> rǎng\nU+2C72C: màn  # 𬜬  => U+8504\nU+2C72F: liǎng  # 𬜯  => U+44E3\nU+2C79F: pín  # 𬞟  => U+860B  ?-> píng\nU+2C7C1: yì  # 𬟁  => U+8649\nU+2C7FD: dōng  # 𬟽  => U+8740\nU+2C8D9: xū  # 𬣙  => U+8A0F\nU+2C8DE: zhǔ  # 𬣞  => U+8A5D\nU+2C8E1: jiàn  # 𬣡  => U+8AD3\nU+2C8F3: hěn  # 𬣳  => U+8A6A\nU+2C907: yīn  # 𬤇  => U+8AF2\nU+2C90A: shì  # 𬤊  => U+8ADF  ?-> dì\nU+2C91D: huì  # 𬤝  => U+8B53\nU+2CA02: qí  # 𬨂  => U+8EDD\nU+2CA0E: yóu  # 𬨎  => U+8F36\nU+2CA7D: xún  # 𬩽  => U+9129\nU+2CAA9: nóng  # 𬪩  => U+91B2\nU+2CB29: yì  # 𬬩  => U+91F4\nU+2CB2D: lún  # 𬬭  => U+9300\nU+2CB2E: chǎng  # 𬬮  => U+92F9\nU+2CB31: jīn  # 𬬱  => U+91FF\nU+2CB38: shù  # 𬬸  => U+9265\nU+2CB39: shén  # 𬬹  => U+926E\nU+2CB3B: lú  # 𬬻  => U+946A\nU+2CB3F: zhāo  # 𬬿  => U+924A\nU+2CB41: mǔ  # 𬭁  => U+9267\nU+2CB4A: dù  # 𬭊  => U+289C0\nU+2CB4E: hóng  # 𬭎  => U+92D0\nU+2CB5A: chún  # 𬭚  => U+931E\nU+2CB5B: bō  # 𬭛  => U+28A0F\nU+2CB64: hóu  # 𬭤  => U+936D\nU+2CB69: wēng  # 𬭩  => U+9393\nU+2CB6C: wèi  # 𬭬  => U+93CF\nU+2CB6F: piě  # 𬭯  => U+4955\nU+2CB73: xǐ  # 𬭳  => U+28B4E\nU+2CB76: hēi  # 𬭶  => U+28B46\nU+2CB78: lín  # 𬭸  => U+93FB\nU+2CB7C: suì  # 𬭼  => U+9429\nU+2CBB1: yīn  # 𬮱  => U+95C9\nU+2CBBF: qí  # 𬮿  => U+9691  ?-> gāi,ái\nU+2CBC0: jī  # 𬯀  => U+96AE\nU+2CBCE: tuí  # 𬯎  => U+96A4\nU+2CC56: dí  # 𬱖  => U+9814\nU+2CC5F: wěi  # 𬱟  => U+9820\nU+2CCF5: pī  # 𬳵  => U+99D3\nU+2CCF6: jiōng  # 𬳶  => U+99C9\nU+2CCFD: shēn  # 𬳽  => U+99EA\nU+2CCFF: tú  # 𬳿  => U+99FC\nU+2CD02: fēi  # 𬴂  => U+9A11\nU+2CD03: huō  # 𬴃  => U+9A1E\nU+2CD0A: lín  # 𬴊  => U+9A4E\nU+2CD8B: jū  # 𬶋  => U+9B88\nU+2CD8D: tuó  # 𬶍  => U+9B80\nU+2CD8F: wéi  # 𬶏  => U+9BA0\nU+2CD90: zhào  # 𬶐  => U+9BA1\nU+2CD9F: là  # 𬶟  => U+9BFB\nU+2CDA0: liàn  # 𬶠  => U+9C0A\nU+2CDA8: jì  # 𬶨  => U+9C40\nU+2CDAD: jì  # 𬶭  => U+9C36\nU+2CDAE: xǐ  # 𬶮  => U+9C5A\nU+2CDD5: bū  # 𬷕  => U+9D4F\nU+2CE18: yǎn  # 𬸘  => U+9DA0\nU+2CE1A: yuè  # 𬸚  => U+9E11\nU+2CE23: xiān  # 𬸣  => U+9DB1\nU+2CE26: zhuó  # 𬸦  => U+9DDF\nU+2CE2A: fán  # 𬸪  => U+9DED\nU+2CE7C: xiè  # 𬹼  => U+9F58\nU+2CE88: yǐ  # 𬺈  =>  U+9F6E\nU+2CE93: chǔ  # 𬺓  =>  U+9F7C\n# EOF\n"
  },
  {
    "path": "src/common/utils/pinyin/parser.js",
    "content": "const fs = require('fs').promises\nconst path = require('path')\n\nconst sourceFilePath = path.join(__dirname, './kMandarin_8105.txt')\nconst distFilePath = path.join(__dirname, './pinyin.txt')\n\nconst yuanyin = [\n  ['ā', 'a'],\n  ['á', 'a'],\n  ['ǎ', 'a'],\n  ['à', 'a'],\n  ['ē', 'e'],\n  ['é', 'e'],\n  ['ě', 'e'],\n  ['è', 'e'],\n  ['ī', 'i'],\n  ['í', 'i'],\n  ['ǐ', 'i'],\n  ['ì', 'i'],\n  ['ō', 'o'],\n  ['ó', 'o'],\n  ['ǒ', 'o'],\n  ['ò', 'o'],\n  ['ū', 'u'],\n  ['ú', 'u'],\n  ['ǔ', 'u'],\n  ['ù', 'u'],\n  ['ǖ', 'v'],\n  ['ǘ', 'v'],\n  ['ǚ', 'v'],\n  ['ǜ', 'v'],\n]\n\nconst parse = async() => {\n  let datas = (await fs.readFile(sourceFilePath)).toString()\n  datas = datas.replace(/ +=> +(\\w|\\+)+ */gm, ' ')\n  for (const [y1, y2] of yuanyin) datas = datas.replaceAll(y1, y2)\n  // console.log(datas)\n  const lines = datas.split('\\n')\n  const dict = {}\n  for (let line of lines) {\n    if (!line || line.startsWith('#')) continue\n    line = line.trim().replace(/^[\\w+]+: */, '')\n    let [p1, comment] = line.split('#')\n    let [z, ps] = comment.split(/(?: *\\? *-> *| *-> *)/)\n    const ys = new Set([p1.trim()])\n    if (ps != null) ps.split(/(?: +| *, *)/).forEach(y => ys.add(y.trim()))\n    dict[z.trim()] = Array.from(ys)\n  }\n\n  fs.writeFile(distFilePath, JSON.stringify(dict))\n}\n\n\nparse()\n\n// let dict = {}\n// let line = 'U+2CBBF: qi  # 𬮿 ?-> gai,ai'\n\n// line = line.trim().replace(/^[\\w+]+: */, '')\n// let [p1, comment] = line.split('#')\n// let [z, ps] = comment.split(/ *\\? *-> */)\n// const ys = dict[z.trim()] = [p1.trim()]\n// console.log(ps)\n// if (ps != null) ys.push(...ps.split(/(?: +| *, *)/).map(y => y.trim()))\n// console.log(dict)\n\n\n"
  },
  {
    "path": "src/common/utils/pinyin/pinyin.json",
    "content": "{\"一\":[\"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\",\"ān\"],\"𩾃\":[\"mian\"],\"𩾌\":[\"kang\",\"kā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\"]}"
  },
  {
    "path": "src/common/utils/renderer.ts",
    "content": "\nconst easeInOutQuad = (t: number, b: number, c: number, d: number): number => {\n  t /= d / 2\n  if (t < 1) return (c / 2) * t * t + b\n  t--\n  return (-c / 2) * (t * (t - 2) - 1) + b\n}\n\ntype Noop = () => void\nconst noop: Noop = () => {}\ntype ScrollElement<T> = {\n  lx_scrollLockKey?: number\n  lx_scrollNextParams?: [ScrollElement<HTMLElement>, number, number, Noop]\n  lx_scrollTimeout?: number\n  lx_scrollDelayTimeout?: number\n} & T\n\nconst handleScrollY = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = noop): Noop => {\n  if (!element) {\n    fn()\n    return noop\n  }\n  const clean = () => {\n    element.lx_scrollLockKey = undefined\n    element.lx_scrollNextParams = undefined\n    if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout)\n    element.lx_scrollTimeout = undefined\n  }\n  if (element.lx_scrollLockKey) {\n    element.lx_scrollNextParams = [element, to, duration, fn]\n    element.lx_scrollLockKey = -1\n    return clean\n  }\n  // @ts-expect-error\n  const start = element.scrollTop ?? element.scrollY ?? 0\n  if (to > start) {\n    let maxScrollTop = element.scrollHeight - element.clientHeight\n    if (to > maxScrollTop) to = maxScrollTop\n  } else if (to < start) {\n    if (to < 0) to = 0\n  } else {\n    fn()\n    return noop\n  }\n  const change = to - start\n  const increment = 10\n  if (!change) {\n    fn()\n    return noop\n  }\n\n  let currentTime = 0\n  let val: number\n  let key = Math.random()\n\n  const animateScroll = () => {\n    element.lx_scrollTimeout = undefined\n    // if (element.lx_scrollLockKey != key) {\n    if (element.lx_scrollNextParams && currentTime > duration * 0.75) {\n      const [_element, to, duration, fn] = element.lx_scrollNextParams\n      clean()\n      handleScrollY(_element, to, duration, fn)\n      return\n    }\n\n    currentTime += increment\n    val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))\n    if (element.scrollTo) {\n      element.scrollTo(0, val)\n    } else {\n      element.scrollTop = val\n    }\n    if (currentTime < duration) {\n      element.lx_scrollTimeout = window.setTimeout(animateScroll, increment)\n    } else {\n      if (element.lx_scrollNextParams) {\n        const [_element, to, duration, fn] = element.lx_scrollNextParams\n        clean()\n        handleScrollY(_element, to, duration, fn)\n      } else {\n        clean()\n        fn()\n      }\n    }\n  }\n\n  element.lx_scrollLockKey = key\n  animateScroll()\n\n  return clean\n}\n/**\n  * 设置滚动条位置\n  * @param {*} element 要设置滚动的容器 dom\n  * @param {*} to 滚动的目标位置\n  * @param {*} duration 滚动完成时间 ms\n  * @param {*} fn 滚动完成后的回调\n  * @param {*} delay 延迟执行时间\n  */\nexport const scrollTo = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {\n  let cancelFn: () => void\n  if (element.lx_scrollDelayTimeout != null) {\n    window.clearTimeout(element.lx_scrollDelayTimeout)\n    element.lx_scrollDelayTimeout = undefined\n  }\n  if (delay) {\n    let scrollCancelFn: Noop\n    cancelFn = () => {\n      if (element.lx_scrollDelayTimeout == null) {\n        scrollCancelFn?.()\n      } else {\n        window.clearTimeout(element.lx_scrollDelayTimeout)\n        element.lx_scrollDelayTimeout = undefined\n      }\n    }\n    element.lx_scrollDelayTimeout = window.setTimeout(() => {\n      element.lx_scrollDelayTimeout = undefined\n      scrollCancelFn = handleScrollY(element, to, duration, fn)\n    }, delay)\n  } else {\n    cancelFn = handleScrollY(element, to, duration, fn) ?? noop\n  }\n  return cancelFn\n}\nconst handleScrollX = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = () => {}): () => void => {\n  if (!element) {\n    fn()\n    return noop\n  }\n  const clean = () => {\n    element.lx_scrollLockKey = undefined\n    element.lx_scrollNextParams = undefined\n    if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout)\n    element.lx_scrollTimeout = undefined\n  }\n  if (element.lx_scrollLockKey) {\n    element.lx_scrollNextParams = [element, to, duration, fn]\n    element.lx_scrollLockKey = -1\n    return clean\n  }\n  // @ts-expect-error\n  const start = element.scrollLeft || element.scrollX || 0\n  if (to > start) {\n    let maxScrollLeft = element.scrollWidth - element.clientWidth\n    if (to > maxScrollLeft) to = maxScrollLeft\n  } else if (to < start) {\n    if (to < 0) to = 0\n  } else {\n    fn()\n    return noop\n  }\n  const change = to - start\n  const increment = 10\n  if (!change) {\n    fn()\n    return noop\n  }\n\n  let currentTime = 0\n  let val: number\n  let key = Math.random()\n\n  const animateScroll = () => {\n    element.lx_scrollTimeout = undefined\n    if (element.lx_scrollNextParams && currentTime > duration * 0.75) {\n      const [_element, to, duration, fn] = element.lx_scrollNextParams\n      clean()\n      handleScrollY(_element, to, duration, fn)\n      return\n    }\n\n    currentTime += increment\n    val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))\n    if (element.scrollTo) {\n      element.scrollTo(val, 0)\n    } else {\n      element.scrollLeft = val\n    }\n    if (currentTime < duration) {\n      element.lx_scrollTimeout = window.setTimeout(animateScroll, increment)\n    } else {\n      if (element.lx_scrollNextParams) {\n        const [_element, to, duration, fn] = element.lx_scrollNextParams\n        clean()\n        handleScrollY(_element, to, duration, fn)\n      } else {\n        clean()\n        fn()\n      }\n    }\n  }\n  element.lx_scrollLockKey = key\n  animateScroll()\n  return clean\n}\n/**\n  * 设置滚动条位置\n  * @param {*} element 要设置滚动的容器 dom\n  * @param {*} to 滚动的目标位置\n  * @param {*} duration 滚动完成时间 ms\n  * @param {*} fn 滚动完成后的回调\n  * @param {*} delay 延迟执行时间\n  */\nexport const scrollXTo = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {\n  let cancelFn: Noop\n  if (element.lx_scrollDelayTimeout != null) {\n    window.clearTimeout(element.lx_scrollDelayTimeout)\n    element.lx_scrollDelayTimeout = undefined\n  }\n  if (delay) {\n    let scrollCancelFn: Noop\n    cancelFn = () => {\n      if (element.lx_scrollDelayTimeout == null) {\n        scrollCancelFn?.()\n      } else {\n        window.clearTimeout(element.lx_scrollDelayTimeout)\n        element.lx_scrollDelayTimeout = undefined\n      }\n    }\n    element.lx_scrollDelayTimeout = window.setTimeout(() => {\n      element.lx_scrollDelayTimeout = undefined\n      scrollCancelFn = handleScrollX(element, to, duration, fn)\n    }, delay)\n  } else {\n    cancelFn = handleScrollX(element, to, duration, fn)\n  }\n  return cancelFn\n}\n\nconst handleScrollXR = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = () => {}): () => void => {\n  if (!element) {\n    fn()\n    return noop\n  }\n  const clean = () => {\n    element.lx_scrollLockKey = undefined\n    element.lx_scrollNextParams = undefined\n    if (element.lx_scrollTimeout) window.clearTimeout(element.lx_scrollTimeout)\n    element.lx_scrollTimeout = undefined\n  }\n  if (element.lx_scrollLockKey) {\n    element.lx_scrollNextParams = [element, to, duration, fn]\n    element.lx_scrollLockKey = -1\n    return clean\n  }\n  // @ts-expect-error\n  const start = element.scrollLeft || element.scrollX as number || 0\n  if (to < start) {\n    let maxScrollLeft = -element.scrollWidth + element.clientWidth\n    if (to < maxScrollLeft) to = maxScrollLeft\n  } else if (to > start) {\n    if (to > 0) to = 0\n  } else {\n    fn()\n    return noop\n  }\n\n  const change = to - start\n  const increment = 10\n  if (!change) {\n    fn()\n    return noop\n  }\n\n  let currentTime = 0\n  let val: number\n  let key = Math.random()\n\n  const animateScroll = () => {\n    element.lx_scrollTimeout = undefined\n    if (element.lx_scrollNextParams && currentTime > duration * 0.75) {\n      const [_element, to, duration, fn] = element.lx_scrollNextParams\n      clean()\n      handleScrollY(_element, to, duration, fn)\n      return\n    }\n\n    currentTime += increment\n    val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))\n\n    if (element.scrollTo) {\n      element.scrollTo(val, 0)\n    } else {\n      element.scrollLeft = val\n    }\n    if (currentTime < duration) {\n      element.lx_scrollTimeout = window.setTimeout(animateScroll, increment)\n    } else {\n      if (element.lx_scrollNextParams) {\n        const [_element, to, duration, fn] = element.lx_scrollNextParams\n        clean()\n        handleScrollY(_element, to, duration, fn)\n      } else {\n        clean()\n        fn()\n      }\n    }\n  }\n\n  element.lx_scrollLockKey = key\n  animateScroll()\n\n  return clean\n}\n/**\n  * 设置滚动条位置 （writing-mode: vertical-rl 专用）\n  * @param element 要设置滚动的容器 dom\n  * @param to 滚动的目标位置\n  * @param duration 滚动完成时间 ms\n  * @param fn 滚动完成后的回调\n  * @param delay 延迟执行时间\n  */\nexport const scrollXRTo = (element: ScrollElement<HTMLElement>, to: number, duration = 300, fn = () => {}, delay = 0): () => void => {\n  let cancelFn: Noop\n  if (element.lx_scrollDelayTimeout != null) {\n    window.clearTimeout(element.lx_scrollDelayTimeout)\n    element.lx_scrollDelayTimeout = undefined\n  }\n  if (delay) {\n    let scrollCancelFn: Noop\n    cancelFn = () => {\n      if (element.lx_scrollDelayTimeout == null) {\n        scrollCancelFn?.()\n      } else {\n        window.clearTimeout(element.lx_scrollDelayTimeout)\n        element.lx_scrollDelayTimeout = undefined\n      }\n    }\n    element.lx_scrollDelayTimeout = window.setTimeout(() => {\n      element.lx_scrollDelayTimeout = undefined\n      scrollCancelFn = handleScrollXR(element, to, duration, fn)\n    }, delay)\n  } else {\n    cancelFn = handleScrollXR(element, to, duration, fn)\n  }\n  return cancelFn\n}\n\n\n/**\n  * 设置标题\n  */\nlet dom_title = document.getElementsByTagName('title')[0]\nexport const setTitle = (title: string | null) => {\n  title ||= 'LX Music'\n  dom_title.innerText = title\n}\n\n"
  },
  {
    "path": "src/common/utils/request.ts",
    "content": "import qs from 'node:querystring'\nimport {\n  FormData,\n  getGlobalDispatcher,\n  interceptors,\n  request as nodeRrequest,\n  ProxyAgent,\n  setGlobalDispatcher,\n  type Dispatcher,\n} from 'undici'\n\nconst defaultOptions: Options = {\n  timeout: 15000,\n  headers: {\n    'User-Agent':\n      'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n  },\n  maxRedirect: 5,\n} as const\nconst redirectDispatcher = interceptors.redirect({ maxRedirections: defaultOptions.maxRedirect })\nconst dispatchers = [\n  interceptors.retry({\n    maxRetries: 3,\n    minTimeout: 1000,\n    maxTimeout: 10000,\n    timeoutFactor: 2,\n    retryAfter: true,\n  }),\n  // interceptors.responseError(),\n] as const\nlet proxyAgent: ProxyAgent | null = null\nlet globalDispatcher = getGlobalDispatcher()\nconst buildDispatcher = (redirectDispatcher: Dispatcher.DispatcherComposeInterceptor | null, retryNum = 3) => {\n  const otherInterceptors =\n    retryNum == 3\n      ? dispatchers\n      : [\n          interceptors.retry({\n            maxRetries: retryNum,\n            minTimeout: 1000,\n            maxTimeout: 6000,\n            timeoutFactor: 2,\n            retryAfter: true,\n          }),\n        ]\n  if (redirectDispatcher) {\n    return (proxyAgent ?? globalDispatcher).compose(redirectDispatcher, ...otherInterceptors)\n  }\n  return (proxyAgent ?? globalDispatcher).compose(...otherInterceptors)\n}\n\nsetGlobalDispatcher(buildDispatcher(redirectDispatcher))\n\nexport const setProxy = (url?: string) => {\n  proxyAgent = url ? new ProxyAgent(url) : null\n  setGlobalDispatcher(buildDispatcher(redirectDispatcher))\n}\nexport const setProxyByHost = (host?: string, port?: string) => {\n  setProxy(host ? `http://${host}:${port}` : undefined)\n}\nconst CONTENT_TYPE = {\n  json: 'application/json',\n  form: 'application/x-www-form-urlencoded',\n  text: 'text/plain',\n  formdata: 'multipart/form-data',\n  xml: 'application/xml',\n  binary: 'application/octet-stream',\n}\ntype ParamsData = Record<string, string | number | null | undefined | boolean>\nexport interface Options {\n  method?:\n  | 'GET'\n  | 'HEAD'\n  | 'POST'\n  | 'PUT'\n  | 'DELETE'\n  | 'OPTIONS'\n  | 'PATCH'\n  | 'PROPFIND'\n  | 'COPY'\n  | 'MOVE'\n  | 'MKCOL'\n  | 'PROPPATCH'\n  | 'QUOTA'\n  query?: ParamsData\n  headers?: Record<string, string | string[]>\n  timeout?: number\n  maxRedirect?: number\n  signal?: AbortController['signal']\n  json?: Record<string, unknown>\n  form?: ParamsData\n  binary?: Buffer | Uint8Array\n  text?: string\n  formdata?: Record<string, string | Buffer | Uint8Array>\n  xml?: string\n  needBody?: boolean\n  needRaw?: boolean\n  retryNum?: number\n}\nexport interface Response<Res> {\n  headers: {\n    accept?: string | undefined\n    'accept-language'?: string | undefined\n    'accept-patch'?: string | undefined\n    'accept-ranges'?: string | undefined\n    'access-control-allow-credentials'?: string | undefined\n    'access-control-allow-headers'?: string | undefined\n    'access-control-allow-methods'?: string | undefined\n    'access-control-allow-origin'?: string | undefined\n    'access-control-expose-headers'?: string | undefined\n    'access-control-max-age'?: string | undefined\n    'access-control-request-headers'?: string | undefined\n    'access-control-request-method'?: string | undefined\n    age?: string | undefined\n    allow?: string | undefined\n    'alt-svc'?: string | undefined\n    authorization?: string | undefined\n    'cache-control'?: string | undefined\n    connection?: string | undefined\n    'content-disposition'?: string | undefined\n    'content-encoding'?: string | undefined\n    'content-language'?: string | undefined\n    'content-length'?: string | undefined\n    'content-location'?: string | undefined\n    'content-range'?: string | undefined\n    'content-type'?: string | undefined\n    cookie?: string | undefined\n    date?: string | undefined\n    etag?: string | undefined\n    expect?: string | undefined\n    expires?: string | undefined\n    forwarded?: string | undefined\n    from?: string | undefined\n    host?: string | undefined\n    'if-match'?: string | undefined\n    'if-modified-since'?: string | undefined\n    'if-none-match'?: string | undefined\n    'if-unmodified-since'?: string | undefined\n    'last-modified'?: string | undefined\n    location?: string | undefined\n    origin?: string | undefined\n    pragma?: string | undefined\n    'proxy-authenticate'?: string | undefined\n    'proxy-authorization'?: string | undefined\n    'public-key-pins'?: string | undefined\n    range?: string | undefined\n    referer?: string | undefined\n    'retry-after'?: string | undefined\n    'sec-websocket-accept'?: string | undefined\n    'sec-websocket-extensions'?: string | undefined\n    'sec-websocket-key'?: string | undefined\n    'sec-websocket-protocol'?: string | undefined\n    'sec-websocket-version'?: string | undefined\n    'set-cookie'?: string[] | undefined\n    'strict-transport-security'?: string | undefined\n    tk?: string | undefined\n    trailer?: string | undefined\n    'transfer-encoding'?: string | undefined\n    upgrade?: string | undefined\n    'user-agent'?: string | undefined\n    vary?: string | undefined\n    via?: string | undefined\n    warning?: string | undefined\n    'www-authenticate'?: string | undefined\n  }\n  body: Res\n  raw: Uint8Array\n  statusCode?: number\n  statusMessage?: string\n}\n\nconst buildRequestBody = (options: Options) => {\n  let contentType: string | undefined\n  let body: string | Buffer | Uint8Array | FormData | undefined\n  if (options.json != null) {\n    contentType = CONTENT_TYPE.json\n    body = JSON.stringify(options.json)\n  } else if (options.form != null) {\n    contentType = CONTENT_TYPE.form\n    body = qs.stringify(options.form)\n  } else if (options.binary != null) {\n    contentType = CONTENT_TYPE.binary\n    body = options.binary\n  } else if (options.formdata != null) {\n    const formdata = new FormData()\n    // eslint-disable-next-line guard-for-in\n    for (const key in options.formdata) {\n      formdata.append(key, options.formdata[key])\n    }\n    contentType = CONTENT_TYPE.formdata\n    body = formdata\n  } else if (options.xml != null) {\n    contentType = CONTENT_TYPE.xml\n    body = options.xml\n  } else if (options.text != null) {\n    contentType = CONTENT_TYPE.text\n    body = options.text\n  }\n  const headers = options.headers ?? {}\n  if (headers['Content-Type']) {\n    contentType = headers['Content-Type'] as string\n  }\n  const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }\n  if (contentType) finalHeaders['Content-Type'] = contentType\n  return [finalHeaders, body] as const\n}\n\nconst buildRequestDispatcher = (options: Options) => {\n  let dispatcher: Dispatcher.ComposedDispatcher | undefined\n\n  if (options.maxRedirect != null) {\n    if (options.maxRedirect != defaultOptions.maxRedirect) {\n      if (options.maxRedirect) {\n        dispatcher = buildDispatcher(interceptors.redirect({ maxRedirections: options.maxRedirect }), options.retryNum)\n      } else {\n        dispatcher = buildDispatcher(null, options.retryNum)\n      }\n    }\n  }\n  return dispatcher\n}\n\nexport const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {\n  const method = options.method?.toUpperCase() ?? 'GET'\n  const timeout = options.timeout ?? defaultOptions.timeout\n  const [headers, body] = buildRequestBody(options)\n  // console.log(url, {\n  //   method,\n  //   bodyTimeout: timeout,\n  //   headersTimeout: timeout,\n  //   headers,\n  //   query: options.query,\n  //   body,\n  //   signal: options.signal,\n  //   dispatcher: buildRequestDispatcher(options),\n  // })\n  return nodeRrequest(url, {\n    method,\n    bodyTimeout: timeout,\n    headersTimeout: timeout,\n    headers,\n    query: options.query,\n    body,\n    signal: options.signal,\n    dispatcher: buildRequestDispatcher(options),\n  }).then(async(response) => {\n    if (options.needBody) {\n      return {\n        headers: response.headers,\n        statusCode: response.statusCode,\n        body: response.body as unknown as T,\n      } satisfies Omit<Response<T>, 'raw'> as Response<T>\n    }\n    if (options.needRaw) {\n      return {\n        headers: response.headers,\n        statusCode: response.statusCode,\n        raw: await response.body.bytes(),\n      } satisfies Omit<Response<T>, 'body'> as Response<T>\n    }\n    // console.log(response)\n    let body = (await response.body.text()) as T\n    if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {\n      try {\n        body = JSON.parse(body as string) as T\n      } catch {}\n    }\n    return {\n      body,\n      headers: response.headers,\n      statusCode: response.statusCode,\n    } satisfies Omit<Response<T>, 'raw'> as Response<T>\n  })\n}\n"
  },
  {
    "path": "src/common/utils/request_node16.ts",
    "content": "import qs from 'node:querystring'\nimport {\n  FormData,\n  getGlobalDispatcher,\n  request as nodeRrequest,\n  ProxyAgent,\n  setGlobalDispatcher,\n  type Dispatcher,\n} from 'undici'\n\nconst defaultOptions: Options = {\n  timeout: 15000,\n  headers: {\n    'User-Agent':\n      'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n  },\n  maxRedirect: 5,\n} as const\nlet proxyAgent: ProxyAgent | null = null\nlet globalDispatcher = getGlobalDispatcher()\nconst buildDispatcher = () => {\n  return proxyAgent ?? globalDispatcher\n}\n\nsetGlobalDispatcher(buildDispatcher())\n\nexport const setProxy = (url?: string) => {\n  proxyAgent = url ? new ProxyAgent(url) : null\n  setGlobalDispatcher(buildDispatcher())\n}\nexport const setProxyByHost = (host?: string, port?: string) => {\n  console.log(host)\n  setProxy(host ? `http://${host}:${port}` : undefined)\n}\nconst CONTENT_TYPE = {\n  json: 'application/json',\n  form: 'application/x-www-form-urlencoded',\n  text: 'text/plain',\n  formdata: 'multipart/form-data',\n  xml: 'application/xml',\n  binary: 'application/octet-stream',\n}\ntype ParamsData = Record<string, string | number | null | undefined | boolean>\nexport interface Options {\n  method?:\n  | 'GET'\n  | 'HEAD'\n  | 'POST'\n  | 'PUT'\n  | 'DELETE'\n  | 'OPTIONS'\n  | 'PATCH'\n  | 'PROPFIND'\n  | 'COPY'\n  | 'MOVE'\n  | 'MKCOL'\n  | 'PROPPATCH'\n  | 'QUOTA'\n  query?: ParamsData\n  headers?: Record<string, string | string[]>\n  timeout?: number\n  maxRedirect?: number\n  signal?: AbortController['signal']\n  json?: Record<string, unknown>\n  form?: ParamsData\n  binary?: Buffer | Uint8Array\n  text?: string\n  formdata?: Record<string, string | Buffer | Uint8Array>\n  xml?: string\n  needBody?: boolean\n  needRaw?: boolean\n  retryNum?: number\n}\nexport interface Response<Res> {\n  headers: {\n    accept?: string | undefined\n    'accept-language'?: string | undefined\n    'accept-patch'?: string | undefined\n    'accept-ranges'?: string | undefined\n    'access-control-allow-credentials'?: string | undefined\n    'access-control-allow-headers'?: string | undefined\n    'access-control-allow-methods'?: string | undefined\n    'access-control-allow-origin'?: string | undefined\n    'access-control-expose-headers'?: string | undefined\n    'access-control-max-age'?: string | undefined\n    'access-control-request-headers'?: string | undefined\n    'access-control-request-method'?: string | undefined\n    age?: string | undefined\n    allow?: string | undefined\n    'alt-svc'?: string | undefined\n    authorization?: string | undefined\n    'cache-control'?: string | undefined\n    connection?: string | undefined\n    'content-disposition'?: string | undefined\n    'content-encoding'?: string | undefined\n    'content-language'?: string | undefined\n    'content-length'?: string | undefined\n    'content-location'?: string | undefined\n    'content-range'?: string | undefined\n    'content-type'?: string | undefined\n    cookie?: string | undefined\n    date?: string | undefined\n    etag?: string | undefined\n    expect?: string | undefined\n    expires?: string | undefined\n    forwarded?: string | undefined\n    from?: string | undefined\n    host?: string | undefined\n    'if-match'?: string | undefined\n    'if-modified-since'?: string | undefined\n    'if-none-match'?: string | undefined\n    'if-unmodified-since'?: string | undefined\n    'last-modified'?: string | undefined\n    location?: string | undefined\n    origin?: string | undefined\n    pragma?: string | undefined\n    'proxy-authenticate'?: string | undefined\n    'proxy-authorization'?: string | undefined\n    'public-key-pins'?: string | undefined\n    range?: string | undefined\n    referer?: string | undefined\n    'retry-after'?: string | undefined\n    'sec-websocket-accept'?: string | undefined\n    'sec-websocket-extensions'?: string | undefined\n    'sec-websocket-key'?: string | undefined\n    'sec-websocket-protocol'?: string | undefined\n    'sec-websocket-version'?: string | undefined\n    'set-cookie'?: string[] | undefined\n    'strict-transport-security'?: string | undefined\n    tk?: string | undefined\n    trailer?: string | undefined\n    'transfer-encoding'?: string | undefined\n    upgrade?: string | undefined\n    'user-agent'?: string | undefined\n    vary?: string | undefined\n    via?: string | undefined\n    warning?: string | undefined\n    'www-authenticate'?: string | undefined\n  }\n  body: Res\n  raw: Uint8Array\n  statusCode?: number\n  statusMessage?: string\n}\n\nconst buildRequestBody = (options: Options) => {\n  let contentType: string | undefined\n  let body: string | Buffer | Uint8Array | FormData | undefined\n  if (options.json != null) {\n    contentType = CONTENT_TYPE.json\n    body = JSON.stringify(options.json)\n  } else if (options.form != null) {\n    contentType = CONTENT_TYPE.form\n    body = qs.stringify(options.form)\n  } else if (options.binary != null) {\n    contentType = CONTENT_TYPE.binary\n    body = options.binary\n  } else if (options.formdata != null) {\n    const formdata = new FormData()\n    // eslint-disable-next-line guard-for-in\n    for (const key in options.formdata) {\n      formdata.append(key, options.formdata[key])\n    }\n    contentType = CONTENT_TYPE.formdata\n    body = formdata\n  } else if (options.xml != null) {\n    contentType = CONTENT_TYPE.xml\n    body = options.xml\n  } else if (options.text != null) {\n    contentType = CONTENT_TYPE.text\n    body = options.text\n  }\n  const headers = options.headers ?? {}\n  if (headers['Content-Type']) {\n    contentType = headers['Content-Type'] as string\n  }\n  const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }\n  if (contentType) finalHeaders['Content-Type'] = contentType\n  return [finalHeaders, body] as const\n}\n\nconst buildRequestDispatcher = (options: Options) => {\n  return buildDispatcher()\n}\n\nexport const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {\n  const method = (options.method?.toUpperCase() ?? 'GET') as Dispatcher.RequestOptions['method']\n  const timeout = options.timeout ?? defaultOptions.timeout\n  const [headers, body] = buildRequestBody(options)\n  // console.log(url, {\n  //   method,\n  //   bodyTimeout: timeout,\n  //   headersTimeout: timeout,\n  //   headers,\n  //   query: options.query,\n  //   body,\n  //   signal: options.signal,\n  //   dispatcher: buildRequestDispatcher(options),\n  // })\n  return nodeRrequest(url, {\n    method,\n    bodyTimeout: timeout,\n    headersTimeout: timeout,\n    headers,\n    query: options.query,\n    body,\n    signal: options.signal,\n    dispatcher: buildRequestDispatcher(options),\n  }).then(async(response) => {\n    if (options.needBody) {\n      return {\n        headers: response.headers,\n        statusCode: response.statusCode,\n        body: response.body as unknown as T,\n      } satisfies Omit<Response<T>, 'raw'> as Response<T>\n    }\n    if (options.needRaw) {\n      return {\n        headers: response.headers,\n        statusCode: response.statusCode,\n        raw: new Uint8Array(await response.body.arrayBuffer()),\n      } satisfies Omit<Response<T>, 'body'> as unknown as Response<T>\n    }\n    // console.log(response)\n    let body = (await response.body.text()) as T\n    if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {\n      try {\n        body = JSON.parse(body as string) as T\n      } catch {}\n    }\n    return {\n      body,\n      headers: response.headers,\n      statusCode: response.statusCode,\n    } satisfies Omit<Response<T>, 'raw'> as Response<T>\n  })\n}\n"
  },
  {
    "path": "src/common/utils/tools.ts",
    "content": "// 业务工具方法\n\nexport const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {\n  const meta: Record<string, any> = {\n    songId: oldMusicInfo.songmid, // 歌曲ID，local为文件路径\n    albumName: oldMusicInfo.albumName, // 歌曲专辑名称\n    picUrl: oldMusicInfo.img, // 歌曲图片链接\n  }\n  const newInfo = {\n    id: `${oldMusicInfo.source}_${oldMusicInfo.songmid}`,\n    name: oldMusicInfo.name,\n    singer: oldMusicInfo.singer,\n    source: oldMusicInfo.source,\n    interval: oldMusicInfo.interval,\n    meta: meta as LX.Music.MusicInfoOnline['meta'],\n  }\n\n  if (oldMusicInfo.source == 'local') {\n    meta.filePath = oldMusicInfo.filePath ?? oldMusicInfo.songmid ?? ''\n    meta.ext = oldMusicInfo.ext ?? /\\.(\\w+)$/.exec(meta.filePath)?.[1] ?? ''\n  } else {\n    meta.qualitys = oldMusicInfo.types\n    meta._qualitys = oldMusicInfo._types\n    meta.albumId = oldMusicInfo.albumId\n    if (meta._qualitys.flac32bit && !meta._qualitys.flac24bit) {\n      meta._qualitys.flac24bit = meta._qualitys.flac32bit\n      delete meta._qualitys.flac32bit\n\n      meta.qualitys = (meta.qualitys as any[]).map(quality => {\n        if (quality.type == 'flac32bit') quality.type = 'flac24bit'\n        return quality\n      })\n    }\n\n    switch (oldMusicInfo.source) {\n      case 'kg':\n        meta.hash = oldMusicInfo.hash\n        newInfo.id = oldMusicInfo.songmid + '_' + oldMusicInfo.hash\n        break\n      case 'tx':\n        meta.strMediaMid = oldMusicInfo.strMediaMid\n        meta.id = oldMusicInfo.songId\n        meta.albumMid = oldMusicInfo.albumMid\n        break\n      case 'mg':\n        meta.copyrightId = oldMusicInfo.copyrightId\n        meta.lrcUrl = oldMusicInfo.lrcUrl\n        meta.mrcUrl = oldMusicInfo.mrcUrl\n        meta.trcUrl = oldMusicInfo.trcUrl\n        break\n    }\n  }\n\n  return newInfo\n}\n\nexport const toOldMusicInfo = (minfo: LX.Music.MusicInfo) => {\n  const oInfo: Record<string, any> = {\n    name: minfo.name,\n    singer: minfo.singer,\n    source: minfo.source,\n    songmid: minfo.meta.songId,\n    interval: minfo.interval,\n    albumName: minfo.meta.albumName,\n    img: minfo.meta.picUrl ?? '',\n    typeUrl: {},\n  }\n  if (minfo.source == 'local') {\n    oInfo.filePath = minfo.meta.filePath\n    oInfo.ext = minfo.meta.ext\n    oInfo.albumId = ''\n    oInfo.types = []\n    oInfo._types = {}\n  } else {\n    oInfo.albumId = minfo.meta.albumId\n    oInfo.types = minfo.meta.qualitys\n    oInfo._types = minfo.meta._qualitys\n\n    switch (minfo.source) {\n      case 'kg':\n        oInfo.hash = minfo.meta.hash\n        break\n      case 'tx':\n        oInfo.strMediaMid = minfo.meta.strMediaMid\n        oInfo.albumMid = minfo.meta.albumMid\n        oInfo.songId = minfo.meta.id\n        break\n      case 'mg':\n        oInfo.copyrightId = minfo.meta.copyrightId\n        oInfo.lrcUrl = minfo.meta.lrcUrl\n        oInfo.mrcUrl = minfo.meta.mrcUrl\n        oInfo.trcUrl = minfo.meta.trcUrl\n        break\n    }\n  }\n\n  return oInfo\n}\n\n/**\n * 修复2.0.0-dev.8之前的新列表数据音质\n * @param musicInfo\n */\nexport const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {\n  if (musicInfo.source == 'local') return musicInfo\n\n  // @ts-expect-error\n  if (musicInfo.meta._qualitys.flac32bit && !musicInfo.meta._qualitys.flac24bit) {\n    // @ts-expect-error\n    musicInfo.meta._qualitys.flac24bit = musicInfo.meta._qualitys.flac32bit\n    // @ts-expect-error\n    delete musicInfo.meta._qualitys.flac32bit\n\n    musicInfo.meta.qualitys = musicInfo.meta.qualitys.map(quality => {\n      // @ts-expect-error\n      if (quality.type == 'flac32bit') quality.type = 'flac24bit'\n      return quality\n    })\n  }\n\n  return musicInfo\n}\n\nexport const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {\n  const ids = new Set<string>()\n  return list.filter(s => {\n    if (!s.id || ids.has(s.id) || !s.name) return false\n    if (s.singer == null) s.singer = ''\n    ids.add(s.id)\n    return true\n  })\n}\n\n\nconst MAX_NAME_LENGTH = 80\nconst MAX_FILE_NAME_LENGTH = 150\nexport const clipNameLength = (name: string) => {\n  if (name.length <= MAX_NAME_LENGTH || !name.includes('、')) return name\n  const names = name.split('、')\n  let newName = names.shift()!\n  for (const name of names) {\n    if (newName.length + name.length > MAX_NAME_LENGTH) break\n    newName = newName + '、' + name\n  }\n  return newName\n}\nexport const clipFileNameLength = (name: string) => {\n  return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name\n}\n\n"
  },
  {
    "path": "src/common/utils/vueRouter.ts",
    "content": "export { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'\n\n"
  },
  {
    "path": "src/common/utils/vueTools.ts",
    "content": "import {\n  ref,\n  reactive,\n  computed,\n  watch,\n  watchEffect,\n  nextTick,\n  onMounted,\n  onBeforeUnmount,\n  toRaw,\n  useCssModule,\n  toRef,\n  toRefs,\n  shallowRef,\n  unref,\n  markRaw,\n  type ComputedRef,\n  type Ref,\n  type ShallowRef,\n  shallowReactive,\n  withDefaults,\n} from 'vue'\n// import { useStore } from 'vuex'\n\n// export const useState = name => {\n//   const store = useStore()\n//   return store.state[name]\n// }\n// export const useGetter = (...names) => {\n//   const store = useStore()\n//   return store.getters[names.join('/')]\n// }\n// export const useRefGetter = (...names) => {\n//   const store = useStore()\n//   return computed(() => store.getters[names.join('/')])\n// }\n\n// export const useAction = (...names) => {\n//   const store = useStore()\n//   return params => {\n//     return store.dispatch(names.join('/'), params)\n//   }\n// }\n// export const useCommit = (...names) => {\n//   const store = useStore()\n//   return params => {\n//     return store.commit(names.join('/'), params)\n//   }\n// }\n\nexport const markRawList = <T extends any[]>(list: T) => {\n  for (const item of list) {\n    markRaw(item)\n  }\n  return list\n}\n\nexport {\n  nextTick,\n  onBeforeUnmount,\n  ref,\n  toRaw,\n  reactive,\n  watch,\n  watchEffect,\n  computed,\n  useCssModule,\n  toRef,\n  toRefs,\n  shallowRef,\n  unref,\n  onMounted,\n  markRaw,\n  shallowReactive,\n  withDefaults,\n}\n\nexport type {\n  ComputedRef,\n  Ref,\n  ShallowRef,\n}\n"
  },
  {
    "path": "src/lang/.eslintrc.cjs",
    "content": "/* eslint-env node */\nconst { base, typescript } = require('../../.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": "src/lang/Readme.md",
    "content": "新增语言时创建的语言文件夹需要与以下列表对应：\n\n- `ar-sa` - Arabic Saudi Arabia\n- `cs-cz` - Czech Czech Republic\n- `da-dk` - Danish Denmark\n- `de-de` - German Germany\n- `el-gr` - Modern Greek Greece\n- `en-au` - English Australia\n- `en-gb` - English United Kingdom\n- `en-ie` - English Ireland\n- `en-us` - English United States\n- `en-za` - English South Africa\n- `es-es` - Spanish Spain\n- `es-mx` - Spanish Mexico\n- `fi-fi` - Finnish Finland\n- `fr-ca` - French Canada\n- `fr-fr` - French France\n- `he-il` - Hebrew Israel\n- `hi-in` - Hindi India\n- `hu-hu` - Hungarian Hungary\n- `id-id` - Indonesian Indonesia\n- `it-it` - Italian Italy\n- `ja-jp` - Japanese Japan\n- `ko-kr` - Korean Republic of Korea\n- `nl-be` - Dutch Belgium\n- `nl-nl` - Dutch Netherlands\n- `no-no` - Norwegian Norway\n- `pl-pl` - Polish Poland\n- `pt-br` - Portuguese Brazil\n- `pt-pt` - Portuguese Portugal\n- `ro-ro` - Romanian Romania\n- `ru-ru` - Russian Russian Federation\n- `sk-sk` - Slovak Slovakia\n- `sv-se` - Swedish Sweden\n- `th-th` - Thai Thailand\n- `tr-tr` - Turkish Turkey\n- `zh-cn` - Chinese China\n- `zh-hk` - Chinese Hong Kong\n- `zh-tw` - Chinese Taiwan\n"
  },
  {
    "path": "src/lang/en-us.json",
    "content": "{\n  \"action\": \"Actions\",\n  \"agree\": \"Accept\",\n  \"alert_button_text\": \"All right\",\n  \"audio_visualization\": \"Audio Visualization (Experimental)\",\n  \"back\": \"Back\",\n  \"btn_cancel\": \"Cancel\",\n  \"btn_close\": \"Close\",\n  \"btn_confirm\": \"Confirm\",\n  \"btn_save\": \"Save\",\n  \"cancel_button_text\": \"Cancel\",\n  \"cancel_button_text_2\": \"No, no, wrong click\",\n  \"close\": \"Close\",\n  \"comment__hot_load_error\": \"Failed to load top comments. Click to try to reload.\",\n  \"comment__hot_loading\": \"Top comments are loading...\",\n  \"comment__hot_title\": \"Top comments\",\n  \"comment__location\": \"From {location}\",\n  \"comment__new_load_error\": \"Failed to load latest comments. Click to try to reload.\",\n  \"comment__new_loading\": \"Latest comments are loading...\",\n  \"comment__new_title\": \"Latest comments\",\n  \"comment__no_content\": \"No comments yet\",\n  \"comment__refresh\": \"Refresh comments\",\n  \"comment__show\": \"Song comments\",\n  \"comment__title\": \"Comments for \\\"{name}\\\"\",\n  \"comment__unavailable\": \"Unable to get comments for this song.\",\n  \"confirm_button_text\": \"Yes\",\n  \"copy_tip\": \" (Click to copy)\",\n  \"date_format_hour\": \"{num} hours ago\",\n  \"date_format_minute\": \"{num} minutes ago\",\n  \"date_format_second\": \"{num} seconds ago\",\n  \"deep_link__handle_error_tip\": \"Failed to call: {message}\",\n  \"default\": \"Default\",\n  \"default_list\": \"Default\",\n  \"desktop_lyric__back\": \"Back\",\n  \"desktop_lyric__close\": \"Close\",\n  \"desktop_lyric__font_decrease\": \"Decrease font size\",\n  \"desktop_lyric__font_increase\": \"Increase font size\",\n  \"desktop_lyric__lock\": \"Lock\",\n  \"desktop_lyric__lrc_active_zoom_off\": \"Unzoom currently playing lyrics\",\n  \"desktop_lyric__lrc_active_zoom_on\": \"Zoom currently playing lyrics\",\n  \"desktop_lyric__opacity_decrease\": \"Increase transparency\\n(Right-click to fine-tune)\",\n  \"desktop_lyric__opacity_increase\": \"Decrease transparency\\n(Right-click to fine-tune)\",\n  \"desktop_lyric__theme\": \"Theme Color\",\n  \"desktop_lyric__unlock\": \"Unlock\",\n  \"desktop_lyric__win_top_off\": \"Stay always-on-top\",\n  \"desktop_lyric__win_top_on\": \"Un-top\",\n  \"download\": \"Downloads\",\n  \"download___status_completed\": \"Download is complete\",\n  \"download___status_error\": \"Error\",\n  \"download___status_paused\": \"Paused\",\n  \"download___status_running\": \"Downloading\",\n  \"download___status_waiting\": \"Waiting to download\",\n  \"download__all\": \"All Tasks\",\n  \"download__error\": \"Error\",\n  \"download__finished\": \"Completed\",\n  \"download__high_quality\": \"High Quality\",\n  \"download__lossless\": \"Lossless\",\n  \"download__multiple_tip\": \"{len} song(s) selected\",\n  \"download__multiple_tip2\": \"Choose preferred download quality\",\n  \"download__normal\": \"Normal\",\n  \"download__not_available_tip\": \"The audio quality is not available.\",\n  \"download__paused\": \"Paused\",\n  \"download__progress\": \"Progress\",\n  \"download__quality\": \"Quality\",\n  \"download__running\": \"Downloading\",\n  \"download__status\": \"Status\",\n  \"download_status_error_check_path\": \"There is an error in checking the download path. Please check whether the set download directory is normal\",\n  \"download_status_error_check_path_exist\": \"A file with the same name exists. The download has been skipped\",\n  \"download_status_error_refresh_url\": \"The link is dead, refreshing...\",\n  \"download_status_error_response\": \"Failed to download: \",\n  \"download_status_error_url_failed\": \"Failed to get song link\",\n  \"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: \",\n  \"download_status_start\": \"Start download\",\n  \"download_status_url_getting\": \"Getting song link...\",\n  \"download_status_write_queue\": \"Writing data in progress ({num})\",\n  \"duplicate_list_tip\": \"You have collected this list \\\"{name}\\\" before. Do you need to update the songs in it?\",\n  \"export\": \"Export\",\n  \"fullscreen_exit\": \"Exit fullscreen\",\n  \"history_clear\": \"Clear history\",\n  \"history_remove\": \"Right-click to remove this entry\",\n  \"history_search\": \"Search History\",\n  \"import\": \"Import\",\n  \"leaderboard\": \"Charts\",\n  \"list__add_to\": \"Add to ...\",\n  \"list__collect\": \"Collect\",\n  \"list__copy_name\": \"Copy Name\",\n  \"list__dislike\": \"Dislike\",\n  \"list__download\": \"Download\",\n  \"list__export_part_desc\": \"Choose where to save the list file\",\n  \"list__file\": \"Reveal File\",\n  \"list__import_part_button_cancel\": \"Don't\",\n  \"list__import_part_button_confirm\": \"Overwrite\",\n  \"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?\",\n  \"list__import_part_desc\": \"Choose list file\",\n  \"list__load_failed\": \"Ah, the loading failed 😭\",\n  \"list__loading\": \"List loading...⏳\",\n  \"list__move_to\": \"Move to ...\",\n  \"list__movedown\": \"Move down\",\n  \"list__moveup\": \"Move up\",\n  \"list__name_default\": \"Default\",\n  \"list__name_love\": \"Loved\",\n  \"list__new_list_btn\": \"New List\",\n  \"list__new_list_input\": \"New list...\",\n  \"list__pause\": \"Stop\",\n  \"list__play\": \"Play\",\n  \"list__play_later\": \"Play Later\",\n  \"list__remove\": \"Remove\",\n  \"list__remove_tip\": \"Do you really want to remove \\\"{name}\\\"?\",\n  \"list__remove_tip_button\": \"Yes, that's right\",\n  \"list__rename\": \"Rename\",\n  \"list__search\": \"Search\",\n  \"list__sort\": \"Adjust Position\",\n  \"list__source_detail\": \"Song Detail\",\n  \"list__start\": \"Resume\",\n  \"list__sync\": \"Update\",\n  \"list__toggle_source\": \"Change Source\",\n  \"list_add__btn_title\": \"Add the song(s) to \\\"{name}\\\"\",\n  \"list_add__multiple_btn_title\": \"Add these song(s) to \\\"{name}\\\"\",\n  \"list_add__multiple_title_add\": \"Add the selected {num} songs to ...\",\n  \"list_add__multiple_title_move\": \"Move the selected {num} songs to ...\",\n  \"list_add__title_first_add\": \"Add\",\n  \"list_add__title_first_move\": \"Move\",\n  \"list_add__title_last\": \"to ...\",\n  \"list_duplicate_tip\": \"A list with the same name already exists. Do you want to continue creating it?\",\n  \"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\",\n  \"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\",\n  \"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\",\n  \"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\",\n  \"list_import_tip__unknown\": \"Failed to import. Unknown file type. Please try to upgrade the app to the latest version and try again.\",\n  \"list_sort_modal_by_album\": \"Album\",\n  \"list_sort_modal_by_down\": \"Descending\",\n  \"list_sort_modal_by_field\": \"Sort Field\",\n  \"list_sort_modal_by_name\": \"Title\",\n  \"list_sort_modal_by_random\": \"Random\",\n  \"list_sort_modal_by_singer\": \"Artist\",\n  \"list_sort_modal_by_source\": \"Music Service\",\n  \"list_sort_modal_by_time\": \"Length\",\n  \"list_sort_modal_by_type\": \"Sort Category\",\n  \"list_sort_modal_by_up\": \"Ascending\",\n  \"list_sort_modal_tip_confirm\": \"Are you sure you want to do this?\",\n  \"list_update_modal__auto_update\": \"Auto Update\",\n  \"list_update_modal__tips\": \"💡 The list with \\\"Auto Update\\\" checked will be automatically updated each time the app is started.\",\n  \"list_update_modal__title\": \"List Update Management\",\n  \"list_update_modal__update\": \"Sync\",\n  \"lists__add_local_file_desc\": \"Choose song file\",\n  \"lists__dislike_music_singer_tip\": \"Do you really dislike {singer}'s \\\"{name}\\\"?\",\n  \"lists__dislike_music_tip\": \"Do you really dislike \\\"{name}\\\"?\",\n  \"lists__duplicate\": \"Duplicate Songs\",\n  \"lists__export\": \"Export\",\n  \"lists__export_part_desc\": \"Choose where to save the list file\",\n  \"lists__import\": \"Import\",\n  \"lists__import_part_button_cancel\": \"No\",\n  \"lists__import_part_button_confirm\": \"Overwrite\",\n  \"lists__import_part_confirm\": \"The imported list ({importName}) has the same ID as the local list ({localName}). Do you overwrite the local list?\",\n  \"lists__import_part_desc\": \"Select list file\",\n  \"lists__new_list_btn\": \"Create list\",\n  \"lists__new_list_input\": \"New list...\",\n  \"lists__remove\": \"Remove\",\n  \"lists__remove_music_tip\": \"Do you really want to remove the selected {len} songs?\",\n  \"lists__remove_tip\": \"Do you really want to remove \\\"{name}\\\"?\",\n  \"lists__remove_tip_button\": \"Yes, that's right\",\n  \"lists__rename\": \"Rename\",\n  \"lists__select_local_file\": \"Add Local Songs\",\n  \"lists__sort_list\": \"Sort Songs\",\n  \"lists__source_detail\": \"Playlist Page\",\n  \"lists__sync\": \"Update\",\n  \"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?\",\n  \"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}\",\n  \"load_list_file_error_title\": \"Error loading playlist data\",\n  \"loading\": \"Loading...\",\n  \"love_list\": \"Favorites\",\n  \"lyric__load_error\": \"Failed to get lyrics\",\n  \"lyric__select\": \"Allow selection of lyrics text\",\n  \"lyric_menu__align\": \"Lyric alignment\",\n  \"lyric_menu__align_center\": \"Center\",\n  \"lyric_menu__align_left\": \"Left\",\n  \"lyric_menu__lrc_size\": \"Font Size [{size}]\",\n  \"lyric_menu__offset\": \"Offset [{offset}ms]\",\n  \"lyric_menu__offset_add_10\": \"Increase 10ms\",\n  \"lyric_menu__offset_add_100\": \"Increase 100ms\",\n  \"lyric_menu__offset_dec_10\": \"Decrease 10ms\",\n  \"lyric_menu__offset_dec_100\": \"Decrease 100ms\",\n  \"lyric_menu__offset_reset\": \"Reset\",\n  \"lyric_menu__size_add\": \"Increase font size\\n(Right-click to fine-tune)\",\n  \"lyric_menu__size_dec\": \"Decrease font size\\n(Right-click to fine-tune)\",\n  \"lyric_menu__size_reset\": \"Reset\",\n  \"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.\",\n  \"min\": \"Minimize\",\n  \"music_album\": \"Album\",\n  \"music_duplicate\": \"Duplicate song\",\n  \"music_name\": \"Title\",\n  \"music_singer\": \"Artist\",\n  \"music_sort__input_tip\": \"Please enter which position you want to adjust to\",\n  \"music_sort__title\": \"Adjust the position of \\\"{name}\\\" to: \",\n  \"music_sort__title_multiple\": \"Adjust the position of the selected {num} songs to: \",\n  \"music_time\": \"Length\",\n  \"music_toggle_clean\": \"Cancel Change\",\n  \"music_toggle_confirm\": \"Confirm\",\n  \"music_toggle_duplicate_tip\": \"The same song already exists in the list, should I remove it and continue?\",\n  \"my_list\": \"Your Library\",\n  \"no_item\": \"Nothing's here...\",\n  \"not_agree\": \"Decline\",\n  \"ok\": \"OK\",\n  \"pagination__next\": \"Next page\",\n  \"pagination__page\": \"Page {num}\",\n  \"pagination__prev\": \"Previous page\",\n  \"play_timeout\": \"Timed Pause\",\n  \"play_timeout_close\": \"Close\",\n  \"play_timeout_confirm\": \"Confirm\",\n  \"play_timeout_end\": \"Wait for the song to finish before pausing\",\n  \"play_timeout_stop\": \"Cancel timer\",\n  \"play_timeout_tip\": \"Pause after {time}\",\n  \"play_timeout_unit\": \"Minute(s)\",\n  \"play_timeout_update\": \"Update timing\",\n  \"player__add_music_to\": \"Add the current song to ...\",\n  \"player__buffering\": \"Buffering...\",\n  \"player__desktop_lyric_lock\": \"Right-click to lock lyric window\",\n  \"player__desktop_lyric_off\": \"Hide lyric window\",\n  \"player__desktop_lyric_on\": \"Show lyric window\",\n  \"player__desktop_lyric_unlock\": \"Right-click to unlock lyric window\",\n  \"player__end\": \"Stopped\",\n  \"player__error\": \"Error loading music. Switch to the next song after 5 seconds\",\n  \"player__getting_url\": \"Getting music link...\",\n  \"player__getting_url_delay_retry\": \"The server is busy. Try again in {time} seconds...\",\n  \"player__hide_detail_tip\": \"Hide detail page\\n(Right-click in the app window to quickly hide the detail page)\",\n  \"player__loading\": \"Music loading...\",\n  \"player__music_album\": \"Album: \",\n  \"player__music_name\": \"Title: \",\n  \"player__music_singer\": \"Artist: \",\n  \"player__next\": \"Next\",\n  \"player__pause\": \"Pause\",\n  \"player__pic_tip\": \"Playback detail page\\n(Right-click to locate the currently playing song in \\\"Your Library\\\")\",\n  \"player__play\": \"Play\",\n  \"player__play_toggle_mode_list\": \"In Order\",\n  \"player__play_toggle_mode_list_loop\": \"Repeat Playlist\",\n  \"player__play_toggle_mode_off\": \"Disable\",\n  \"player__play_toggle_mode_random\": \"Shuffle\",\n  \"player__play_toggle_mode_single_loop\": \"Repeat\",\n  \"player__playback_preserves_pitch\": \"Pitch compensation\",\n  \"player__playback_rate\": \"Current playback rate: \",\n  \"player__playback_rate_reset_btn\": \"Reset\",\n  \"player__playing\": \"Now playing...\",\n  \"player__prev\": \"Prev\",\n  \"player__refresh_url\": \"Music URL expired, refreshing...\",\n  \"player__sound_effect\": \"Sound options (EXPERIMENTAL)\",\n  \"player__sound_effect_biquad_filter\": \"Equalizer\",\n  \"player__sound_effect_biquad_filter_preset_classical\": \"Classical\",\n  \"player__sound_effect_biquad_filter_preset_dance\": \"Dance\",\n  \"player__sound_effect_biquad_filter_preset_electronic\": \"Electronic\",\n  \"player__sound_effect_biquad_filter_preset_pop\": \"Pop\",\n  \"player__sound_effect_biquad_filter_preset_rock\": \"Rock\",\n  \"player__sound_effect_biquad_filter_preset_slow\": \"Slow\",\n  \"player__sound_effect_biquad_filter_preset_soft\": \"Soft\",\n  \"player__sound_effect_biquad_filter_preset_subwoofer\": \"Subwoofer\",\n  \"player__sound_effect_biquad_filter_preset_vocal\": \"Vocal\",\n  \"player__sound_effect_biquad_filter_reset_btn\": \"Reset\",\n  \"player__sound_effect_biquad_filter_save_btn\": \"Save Preset As\",\n  \"player__sound_effect_biquad_filter_save_input\": \"New presets...\",\n  \"player__sound_effect_convolution\": \"Ambient Reverb Sound Effect\",\n  \"player__sound_effect_convolution_file_bright_hall\": \"Hall\",\n  \"player__sound_effect_convolution_file_cardiod_35_10_spread\": \"Heart-shaped Diffusion\",\n  \"player__sound_effect_convolution_file_cinema_diningroom\": \"Cinema\",\n  \"player__sound_effect_convolution_file_dining_living_true_stereo\": \"Dining Room\",\n  \"player__sound_effect_convolution_file_feedback_spring\": \"Feedback Spring\",\n  \"player__sound_effect_convolution_file_living_bedroom_leveled\": \"Bathroom\",\n  \"player__sound_effect_convolution_file_matrix_1\": \"Matrix (1)\",\n  \"player__sound_effect_convolution_file_matrix_2\": \"Matrix (2)\",\n  \"player__sound_effect_convolution_file_s2_r4_bd\": \"Church\",\n  \"player__sound_effect_convolution_file_s3_r1_bd\": \"Stereo\",\n  \"player__sound_effect_convolution_file_spreader50_65ms\": \"Indoor\",\n  \"player__sound_effect_convolution_file_telephone\": \"Telephone\",\n  \"player__sound_effect_convolution_file_tim_omni_35_10_magnetic\": \"Magnetic Stereo\",\n  \"player__sound_effect_convolution_main_gain\": \"Original Audio Gain\",\n  \"player__sound_effect_convolution_send_gain\": \"Ambient Sound Effect Gain\",\n  \"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.\",\n  \"player__sound_effect_panner\": \"3D Stereo Surround (Need to use headphones)\",\n  \"player__sound_effect_panner_enabled\": \"Enabled\",\n  \"player__sound_effect_panner_sound_r\": \"Sound Distance\",\n  \"player__sound_effect_panner_sound_speed\": \"Surround Speed\",\n  \"player__sound_effect_pitch_shifter\": \"Pitch Adjustment\",\n  \"player__sound_effect_pitch_shifter_preset_semitones\": \"{num} semitones\",\n  \"player__sound_effect_pitch_shifter_reset_btn\": \"Reset\",\n  \"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.\",\n  \"player__stop\": \"Paused\",\n  \"player__volume\": \"Volume: \",\n  \"player__volume_mute_label\": \"Mute\",\n  \"player__volume_muted\": \"Muted\",\n  \"search\": \"Search\",\n  \"search__hot_search\": \"Top Searches\",\n  \"search__type_music\": \"Song\",\n  \"search__type_songlist\": \"Playlist\",\n  \"search__welcome\": \"Search what I want~~ 😉\",\n  \"setting\": \"Settings\",\n  \"setting__about\": \"About LX Music\",\n  \"setting__backup\": \"Backup & Restore\",\n  \"setting__backup_all\": \"All Data (\\\"List\\\" data and \\\"Setting\\\" data)\",\n  \"setting__backup_all_export\": \"Export\",\n  \"setting__backup_all_export_desc\": \"Save the backup to ...\",\n  \"setting__backup_all_import\": \"Import\",\n  \"setting__backup_all_import_desc\": \"Choose a backup file\",\n  \"setting__backup_other\": \"Other Backup Formats (Restoring such backup files is not currently supported)\",\n  \"setting__backup_other_export_dir\": \"Choose where to save the file\",\n  \"setting__backup_other_export_list_csv\": \"Export lists in CSV format\",\n  \"setting__backup_other_export_list_text\": \"Export lists in TXT format\",\n  \"setting__backup_other_export_list_text_confirm\": \"Do you want to merge all the lists into one file?\",\n  \"setting__backup_part\": \"Partial Data (\\\"List\\\" data includes \\\"Default\\\", \\\"Loved\\\", and user-created lists. \\\"Setting\\\" data does not include \\\"Shortcuts\\\" settings)\",\n  \"setting__backup_part_export_list\": \"Export lists\",\n  \"setting__backup_part_export_list_desc\": \"Save the lists to ...\",\n  \"setting__backup_part_export_setting\": \"Export settings\",\n  \"setting__backup_part_export_setting_desc\": \"Save the settings to ...\",\n  \"setting__backup_part_import_list\": \"Import lists\",\n  \"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?\",\n  \"setting__backup_part_import_list_desc\": \"Choose a list backup file\",\n  \"setting__backup_part_import_setting\": \"Import settings\",\n  \"setting__backup_part_import_setting_desc\": \"Choose a setting backup file\",\n  \"setting__basic\": \"General\",\n  \"setting__basic_animation\": \"Randomize pop-up animation\",\n  \"setting__basic_control_btn_position\": \"Control Button Position\",\n  \"setting__basic_control_btn_position_left\": \"Left\",\n  \"setting__basic_control_btn_position_right\": \"Right\",\n  \"setting__basic_font\": \"Font\",\n  \"setting__basic_font_size\": \"Font Size\",\n  \"setting__basic_font_size_14px\": \"Smaller\",\n  \"setting__basic_font_size_15px\": \"Small\",\n  \"setting__basic_font_size_16px\": \"Standard\",\n  \"setting__basic_font_size_17px\": \"Big\",\n  \"setting__basic_font_size_18px\": \"Larger\",\n  \"setting__basic_font_size_19px\": \"Oversize\",\n  \"setting__basic_lang\": \"Language\",\n  \"setting__basic_lang_title\": \"The language displayed in the app\",\n  \"setting__basic_playbar_progress_style\": \"Playbar Progress Bar Style\",\n  \"setting__basic_playbar_progress_style_full\": \"Full-width\",\n  \"setting__basic_playbar_progress_style_middle\": \"Medium\",\n  \"setting__basic_playbar_progress_style_mini\": \"Mini\",\n  \"setting__basic_show_animation\": \"Show animation\",\n  \"setting__basic_source\": \"Music API\",\n  \"setting__basic_source_status_failed\": \"Failed to initialize\",\n  \"setting__basic_source_status_initing\": \"Initializing...\",\n  \"setting__basic_source_status_success\": \"Successfully initialized\",\n  \"setting__basic_source_temp\": \"Temporary API (Some features not available. Workaround if Test API is unavailable)\",\n  \"setting__basic_source_test\": \"Test API (Available for most app features)\",\n  \"setting__basic_source_user_api_btn\": \"Music API Management\",\n  \"setting__basic_sourcename\": \"Music Streaming Service Name\",\n  \"setting__basic_sourcename_alias\": \"Alias\",\n  \"setting__basic_sourcename_real\": \"Original\",\n  \"setting__basic_sourcename_title\": \"Choose the music streaming service name\",\n  \"setting__basic_start_in_fullscreen\": \"Start in fullscreen mode\",\n  \"setting__basic_theme\": \"Theme\",\n  \"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.\",\n  \"setting__basic_to_tray\": \"Minimize the app window to the system tray when closing it\",\n  \"setting__basic_window_size\": \"Window Size\",\n  \"setting__basic_window_size_big\": \"Big\",\n  \"setting__basic_window_size_huge\": \"Huge\",\n  \"setting__basic_window_size_larger\": \"Larger\",\n  \"setting__basic_window_size_medium\": \"Medium\",\n  \"setting__basic_window_size_oversized\": \"Oversize\",\n  \"setting__basic_window_size_small\": \"Small\",\n  \"setting__basic_window_size_smaller\": \"Smaller\",\n  \"setting__basic_window_size_title\": \"Set the window size\",\n  \"setting__click_copy\": \"Click to copy\",\n  \"setting__click_open\": \"Click to open\",\n  \"setting__desktop_lyric\": \"Desktop Lyric\",\n  \"setting__desktop_lyric_align\": \"Lyric Alignment\",\n  \"setting__desktop_lyric_align_center\": \"Center\",\n  \"setting__desktop_lyric_align_left\": \"Left\",\n  \"setting__desktop_lyric_align_right\": \"Right\",\n  \"setting__desktop_lyric_always_on_top\": \"Stay always-on-top\",\n  \"setting__desktop_lyric_always_on_top_loop\": \"Refresh lyric window repeatedly when \\\"Stay always-on-top\\\" is enabled\",\n  \"setting__desktop_lyric_always_on_top_loop_tip\": \"Try to enable this option when the window are still covered by some programs.\",\n  \"setting__desktop_lyric_audio_visualization\": \"Enable audio visualization (EXPERIMENTAL)\",\n  \"setting__desktop_lyric_color\": \"Lyric Font Color\",\n  \"setting__desktop_lyric_color_reset\": \"Reset color\",\n  \"setting__desktop_lyric_delay_scroll\": \"Delay scrolling lyrics\",\n  \"setting__desktop_lyric_direction\": \"Lyric Display Direction\",\n  \"setting__desktop_lyric_direction_horizontal\": \"Horizontal\",\n  \"setting__desktop_lyric_direction_vertical\": \"Vertical\",\n  \"setting__desktop_lyric_ellipsis\": \"Do not wrap lyrics\",\n  \"setting__desktop_lyric_enable\": \"Show lyric window\",\n  \"setting__desktop_lyric_font\": \"Lyric Font\",\n  \"setting__desktop_lyric_font_default\": \"Default\",\n  \"setting__desktop_lyric_font_weight\": \"Bold Font\",\n  \"setting__desktop_lyric_font_weight_extended\": \"Translated & romanized lyrics\",\n  \"setting__desktop_lyric_font_weight_font\": \"Verbatim lyrics\",\n  \"setting__desktop_lyric_font_weight_line\": \"Progressive lyrics\",\n  \"setting__desktop_lyric_fullscreen_hide\": \"Hide lyric window when in fullscreen\",\n  \"setting__desktop_lyric_hover_hide\": \"Increase lyric window transparency when the mouse moves into the lyric window\",\n  \"setting__desktop_lyric_hover_hide_tip\": \"This feature has platform compatibility issues.\",\n  \"setting__desktop_lyric_line_gap\": \"Lyric Spacing ({num})\",\n  \"setting__desktop_lyric_line_gap_add\": \"Increase spacing\",\n  \"setting__desktop_lyric_line_gap_dec\": \"Decrease spacing\",\n  \"setting__desktop_lyric_lock\": \"Lock lyric window\",\n  \"setting__desktop_lyric_lock_screen\": \"Do not allow lyric window to be dragged out of main screen\",\n  \"setting__desktop_lyric_pause_hide\": \"Increase lyric window transparency when pausing music\",\n  \"setting__desktop_lyric_played_color\": \"Played\",\n  \"setting__desktop_lyric_reset\": \"Reset\",\n  \"setting__desktop_lyric_reset_window\": \"Reset Desktop Lyric Options\",\n  \"setting__desktop_lyric_scroll_align\": \"Position of Lyrics Scrolling While Playing\",\n  \"setting__desktop_lyric_scroll_align_center\": \"Center\",\n  \"setting__desktop_lyric_scroll_align_top\": \"Top\",\n  \"setting__desktop_lyric_shadow_color\": \"Shadow\",\n  \"setting__desktop_lyric_show_taskbar\": \"Show lyric window process on the taskbar\",\n  \"setting__desktop_lyric_show_taskbar_tip\": \"This option is used as a workaround when the screen recording program cannot capture the lyric window.\",\n  \"setting__desktop_lyric_unplay_color\": \"Not Played\",\n  \"setting__dislike_list_input_tip\": \"song_name@artist_name\\nsong_name\\n@artist_name\",\n  \"setting__dislike_list_save_btn\": \"Save\",\n  \"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\",\n  \"setting__dislike_list_title\": \"Disliked Song Rule List\",\n  \"setting__download\": \"Download\",\n  \"setting__download_data_embed\": \"Embed the Following into Audio File\",\n  \"setting__download_embed_lxlyric\": \"Embed LX Music lyrics at the same time (if any, verbatim lyrics can be supported when playing with LX Music)\",\n  \"setting__download_embed_lyric\": \"Embed lyrics\",\n  \"setting__download_embed_pic\": \"Embed cover\",\n  \"setting__download_embed_rlyric\": \"Also embed romanized lyrics if available\",\n  \"setting__download_embed_tlyric\": \"Also embed translated lyrics if available\",\n  \"setting__download_enable\": \"Enable Download\",\n  \"setting__download_lxlyric\": \"Also write LX Music lyrics into the lyrics file (if any, verbatim lyrics can be supported when playing with LX Music)\",\n  \"setting__download_lyric\": \"Lyric Download\",\n  \"setting__download_lyric_format\": \"Encoding Format of Downloaded Lyric Files\",\n  \"setting__download_lyric_format_gbk\": \"GBK\",\n  \"setting__download_lyric_format_tip\": \"Try to select GBK format when Chinese garbled characters appear on some devices.\",\n  \"setting__download_lyric_format_utf8\": \"UTF-8\",\n  \"setting__download_lyric_title\": \"Select whether to download the lyrics file\",\n  \"setting__download_max_num\": \"Number of Simultaneous Downloads\",\n  \"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?\",\n  \"setting__download_max_num_tooltip\": \"Set too high may result in IP being blocked, depending on the Music API.\",\n  \"setting__download_name\": \"Music File Naming\",\n  \"setting__download_name1\": \"\\\"Title - Artist\\\"\",\n  \"setting__download_name2\": \"\\\"Artist - Title\\\"\",\n  \"setting__download_name3\": \"Title only\",\n  \"setting__download_name_title\": \"Select the music naming style for downloading\",\n  \"setting__download_path\": \"Download Path\",\n  \"setting__download_path_change_btn\": \"Change\",\n  \"setting__download_path_label\": \"Current: \",\n  \"setting__download_path_open_label\": \"Click to open this path\",\n  \"setting__download_path_title\": \"Choose the path to downloading\",\n  \"setting__download_rlyric\": \"Also write romanized lyrics to the lyric file if available\",\n  \"setting__download_select_save_path\": \"Select the save path\",\n  \"setting__download_skip_exist_file\": \"Skip the download task if a file with the same name exists in the download directory\",\n  \"setting__download_tlyric\": \"Also write translated lyrics to the lyric file if available\",\n  \"setting__download_use_other_source\": \"Automatic Switch Source Download\",\n  \"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.\",\n  \"setting__hot_key\": \"Shortcuts\",\n  \"setting__hot_key_common_focus_search_input\": \"Focus Search Box\",\n  \"setting__hot_key_common_min\": \"Minimize App\",\n  \"setting__hot_key_common_toggle_close\": \"Exit App\",\n  \"setting__hot_key_common_toggle_hide\": \"Show/Hide App\",\n  \"setting__hot_key_common_toggle_min\": \"Minimize/Restore App\",\n  \"setting__hot_key_desktop_lyric_toggle_always_top\": \"Top/Un-top Desktop Lyric\",\n  \"setting__hot_key_desktop_lyric_toggle_lock\": \"Lock/Unlock Desktop Lyric\",\n  \"setting__hot_key_desktop_lyric_toggle_visible\": \"Show/Hide Desktop Lyric\",\n  \"setting__hot_key_global_title\": \"Global Keyboard Shortcuts\",\n  \"setting__hot_key_local_title\": \"In-app Keyboard Shortcuts\",\n  \"setting__hot_key_player_music_dislike\": \"Dislike Song\",\n  \"setting__hot_key_player_music_love\": \"Love Song\",\n  \"setting__hot_key_player_music_unlove\": \"Unlove Song\",\n  \"setting__hot_key_player_next\": \"Next Song\",\n  \"setting__hot_key_player_prev\": \"Previous Song\",\n  \"setting__hot_key_player_seekbackward\": \"Seek Backward 5s\",\n  \"setting__hot_key_player_seekforward\": \"Seek Forward 5s\",\n  \"setting__hot_key_player_toggle_play\": \"Play/Pause Control\",\n  \"setting__hot_key_player_volume_down\": \"Decrease Volume\",\n  \"setting__hot_key_player_volume_mute\": \"Toggle Mute\",\n  \"setting__hot_key_player_volume_up\": \"Increase Volume\",\n  \"setting__hot_key_tip_input\": \"Please enter a new key\",\n  \"setting__hot_key_unset_input\": \"Not set\",\n  \"setting__is_enable\": \"Enable\",\n  \"setting__is_show\": \"Show\",\n  \"setting__list\": \"List\",\n  \"setting__list_action_btn\": \"Show list action buttons\",\n  \"setting__list_add_music_location_type\": \"Position When Adding a Song to the List\",\n  \"setting__list_add_music_location_type_bottom\": \"Bottom\",\n  \"setting__list_add_music_location_type_top\": \"Top\",\n  \"setting__list_click_action\": \"Automatically switch to current list when double-clicking a song in the list (Only valid for \\\"Playlists\\\" and \\\"Charts\\\" page)\",\n  \"setting__list_scroll\": \"Remember position of scroll bar of playlist (Only valid for \\\"Your Library\\\" page)\",\n  \"setting__list_source\": \"Show which music streaming service the song is from (Only valid for \\\"Your Library\\\" page)\",\n  \"setting__network\": \"Network\",\n  \"setting__network_proxy_host\": \"Host\",\n  \"setting__network_proxy_password\": \"Password\",\n  \"setting__network_proxy_port\": \"Port\",\n  \"setting__network_proxy_title\": \"HTTP Proxy (Incorrect settings will prevent the app from being networked)\",\n  \"setting__network_proxy_username\": \"Username\",\n  \"setting__odc\": \"Auto Empty\",\n  \"setting__odc_clear_search_input\": \"Empty search box when you are not searching\",\n  \"setting__odc_clear_search_list\": \"Empty search list when you are not searching\",\n  \"setting__open_api\": \"Open API\",\n  \"setting__open_api_address\": \"Service Address: \",\n  \"setting__open_api_bind_lan\": \"Allow access from LAN\",\n  \"setting__open_api_enable\": \"Enable Open API service\",\n  \"setting__open_api_port\": \"Service Port\",\n  \"setting__open_api_port_tip\": \"Please enter the Open API service port\",\n  \"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: \",\n  \"setting__open_api_tip_link\": \"Visit Documents\",\n  \"setting__other\": \"Extras\",\n  \"setting__other_dislike_list\": \"Dislike Song Rule\",\n  \"setting__other_dislike_list_label\": \"Number of rules: \",\n  \"setting__other_dislike_list_show_btn\": \"Edit Rule\",\n  \"setting__other_listdata\": \"List Data Cleanup\",\n  \"setting__other_listdata_clear_btn\": \"Clear \\\"Your Library\\\" Data\",\n  \"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?\",\n  \"setting__other_lyric_edited_cache\": \"Lyric Management with Adjusted Offset\",\n  \"setting__other_lyric_edited_clear_btn\": \"Clear Time-adjusted Lyrics\",\n  \"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 🤪)\",\n  \"setting__other_lyric_edited_label\": \"Number of lyrics: \",\n  \"setting__other_lyric_raw_clear_btn\": \"Clear Lyric Cache\",\n  \"setting__other_lyric_raw_label\": \"Number of lyrics: \",\n  \"setting__other_music_url_clear_btn\": \"Clear Song URL Cache\",\n  \"setting__other_music_url_label\": \"Number of song URLs: \",\n  \"setting__other_other_cache\": \"Other Cache Management\",\n  \"setting__other_other_source_clear_btn\": \"Clear Song Cache of Changed Source\",\n  \"setting__other_other_source_label\": \"Number of songs information that changed source: \",\n  \"setting__other_resource_cache\": \"Resource Cache Management\",\n  \"setting__other_resource_cache_clear_btn\": \"Clear Resource Cache\",\n  \"setting__other_resource_cache_confirm\": \"I want to clear\",\n  \"setting__other_resource_cache_label\": \"The app has used cache size: \",\n  \"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.\",\n  \"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?\",\n  \"setting__other_transparent_window\": \"Use built-in rounded corners and shadows for the main window\",\n  \"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.\",\n  \"setting__other_tray_theme\": \"Tray Icon Style\",\n  \"setting__other_tray_theme_auto\": \"Follow System\",\n  \"setting__other_tray_theme_black\": \"Black\",\n  \"setting__other_tray_theme_native\": \"White\",\n  \"setting__other_tray_theme_origin\": \"Default\",\n  \"setting__play\": \"Playback\",\n  \"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?\",\n  \"setting__play_auto_clean_played_list\": \"Empty played list when playing the same list as the current playlist\",\n  \"setting__play_auto_clean_played_list_tip\": \"All songs in the list in shuffle mode will participate in the random again.\",\n  \"setting__play_auto_skip_on_error\": \"Automatically switch songs when playback error occurs\",\n  \"setting__play_detail\": \"Playback Detail Page\",\n  \"setting__play_detail_align\": \"Lyric Alignment\",\n  \"setting__play_detail_align_center\": \"Center\",\n  \"setting__play_detail_align_left\": \"Left\",\n  \"setting__play_detail_align_right\": \"Right\",\n  \"setting__play_detail_font_size\": \"Lyrics font size (you can use the keyboard \\\"+\\\" & \\\"-\\\" to adjust the font size on the playback detail page)\",\n  \"setting__play_detail_font_size_current\": \"Current font size: {size}\",\n  \"setting__play_detail_font_size_reset\": \"Reset\",\n  \"setting__play_detail_font_zoom\": \"Zoom currently playing lyrics\",\n  \"setting__play_detail_lyric_delay_scroll\": \"Delay scrolling lyrics\",\n  \"setting__play_detail_lyric_progress\": \"Allow to adjust playback progress by drag-and-drop lyrics\",\n  \"setting__play_lyric_lxlrc\": \"Playback with karaoke-style lyrics if available\",\n  \"setting__play_lyric_lxlrc_tip\": \"This feature requires higher performance and is not recommended to be turned on for low performance devices!\",\n  \"setting__play_lyric_roma\": \"Show romanized lyrics if available\",\n  \"setting__play_lyric_s2t\": \"Convert Chinese lyrics that are playing and downloading to traditional\",\n  \"setting__play_lyric_transition\": \"Show translated lyrics if available\",\n  \"setting__play_max_output_channel_count\": \"Output audio using the maximum number of channels the device can handle\",\n  \"setting__play_mediaDevice\": \"Audio Output\",\n  \"setting__play_mediaDevice_remove_stop_play\": \"Pause playback when current audio output device is changed\",\n  \"setting__play_mediaDevice_title\": \"Select a media device for audio output\",\n  \"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.\",\n  \"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?\",\n  \"setting__play_playQuality\": \"Prioritize Sound Quality for Playback If Available\",\n  \"setting__play_power_save_blocker\": \"Prevent device from hibernating while playing songs\",\n  \"setting__play_save_play_time\": \"Remember playback progress\",\n  \"setting__play_startup_auto_play\": \"Automatically play music on startup\",\n  \"setting__play_statusbar_lyric\": \"Show lyrics on status menus\",\n  \"setting__play_statusbar_lyric_tip\": \"Requires \\\"Minimize the app window to the system tray when closing it\\\" to be enabled in Settings → General\",\n  \"setting__play_task_bar\": \"Show playback progress on the taskbar\",\n  \"setting__play_timeout\": \"Timed Pause\",\n  \"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?\",\n  \"setting__player_swap_lyric_trans_roma\": \"Swap the position of the translated and romanized lyrics\",\n  \"setting__search\": \"Search\",\n  \"setting__search_focus_search_box\": \"Automatically focus search box on startup\",\n  \"setting__search_history\": \"Enable Search History\",\n  \"setting__search_hot\": \"Enable Top Searches\",\n  \"setting__sync\": \"Data Sync\",\n  \"setting__sync_client_address\": \"Current device address: {address}\",\n  \"setting__sync_client_host\": \"Sync service address\",\n  \"setting__sync_client_host_tip\": \"http://<Host>:<Port>\",\n  \"setting__sync_client_mode\": \"Client Mode\",\n  \"setting__sync_client_status\": \"Status: {status}\",\n  \"setting__sync_code_blocked_ip\": \"The IP of the current device has been blocked by the server!\",\n  \"setting__sync_code_fail\": \"Invalid connection code\",\n  \"setting__sync_enable\": \"Enable Sync\",\n  \"setting__sync_mode\": \"Sync Mode\",\n  \"setting__sync_mode_client\": \"Client\",\n  \"setting__sync_mode_server\": \"Server\",\n  \"setting__sync_server_address\": \"Sync service address: {address}\",\n  \"setting__sync_server_auth_code\": \"Connection code: {code}\",\n  \"setting__sync_server_device\": \"Connected devices: {devices}\",\n  \"setting__sync_server_device_list_btn_remove\": \"Remove\",\n  \"setting__sync_server_device_list_noitem\": \"Nothing here ┗( ▔, ▔ )┛\",\n  \"setting__sync_server_device_list_time\": \"Last connection time: {time}\",\n  \"setting__sync_server_device_list_tips\": \"💡 After the device is removed, you need to re-enter the connection code when reconnecting\",\n  \"setting__sync_server_device_list_title\": \"Certified device\",\n  \"setting__sync_server_mode\": \"Server Mode (Please use it on a trusted network as data is transmitted in plaintext)\",\n  \"setting__sync_server_port\": \"Sync Port\",\n  \"setting__sync_server_port_tip\": \"Please enter a port number\",\n  \"setting__sync_server_refresh_code\": \"Refresh connection code\",\n  \"setting__sync_server_show_device_list\": \"List of certified devices\",\n  \"setting__sync_tip\": \"For how to use it, please see the \\\"Sync feature\\\" section of the FAQ\",\n  \"setting__update\": \"Update\",\n  \"setting__update_checking\": \"Checking for updates...\",\n  \"setting__update_commit_date\": \"Commit date: \",\n  \"setting__update_commit_id\": \"Commit hash: \",\n  \"setting__update_current_label\": \"Current version: \",\n  \"setting__update_downloading\": \"Update is found and being downloaded...⏳\",\n  \"setting__update_init\": \"Updating...\",\n  \"setting__update_latest\": \"The app is up-to-date. Enjoy yourself!🥂\",\n  \"setting__update_latest_label\": \"Latest version: \",\n  \"setting__update_new_version\": \"Found a new version. Hurry up and update~🚀🚀\",\n  \"setting__update_open_version_modal_btn\": \"Open Update Window\",\n  \"setting__update_progress\": \"Status: \",\n  \"setting__update_show_change_log\": \"Show changelog on first startup after update\",\n  \"setting__update_try_auto_update\": \"Automatically attempt to download updates when a new version is found\",\n  \"setting__update_unknown\": \"unknown\",\n  \"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\",\n  \"setting_download_save_group_list_name\": \"Save files to a subdirectory named after the corresponding list\",\n  \"setting_sync_status_enabled\": \"connected\",\n  \"song_list\": \"Playlists\",\n  \"songlist__import_input_btn_confirm\": \"Open\",\n  \"songlist__import_input_show_btn\": \"Open Playlist\",\n  \"songlist__import_input_tip\": \"Enter a playlist link/ID\",\n  \"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\",\n  \"songlist__import_input_tip_2\": \"If you encounter a playlist link that cannot be opened. Please send us your feedback\",\n  \"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\",\n  \"songlist__import_input_tip_4\": \"NetEase Cloud Music's \\\"I Like\\\" playlist requires a token to open. For details, see \",\n  \"songlist__import_input_title\": \"Open shared playlist\",\n  \"songlist__open_list\": \"Open \\\"{name}\\\" playlist\",\n  \"songlist__tag_info_hot_tag\": \"Top Tags\",\n  \"source_alias_all\": \"Aggregated\",\n  \"source_alias_bd\": \"BD Music\",\n  \"source_alias_kg\": \"KG Music\",\n  \"source_alias_kw\": \"KW Music\",\n  \"source_alias_mg\": \"MG Music\",\n  \"source_alias_tx\": \"TX Music\",\n  \"source_alias_wy\": \"WY Music\",\n  \"source_alias_xm\": \"XM Music\",\n  \"source_all\": \"Aggregated\",\n  \"source_bd\": \"Baidu\",\n  \"source_kg\": \"Kugou\",\n  \"source_kw\": \"Kuwo\",\n  \"source_mg\": \"Migu\",\n  \"source_tx\": \"Tencent\",\n  \"source_wy\": \"NetEase\",\n  \"source_xm\": \"Xiami\",\n  \"sync__auth_code_input_tip\": \"Please enter the connection code\",\n  \"sync__auth_code_title\": \"Need to enter the connection code\",\n  \"sync__dislike_merge_tip_desc\": \"Merge the content of the two lists and remove the duplicates.\",\n  \"sync__dislike_other_tip_desc\": \"\\\"Cancel Sync\\\" will not sync the \\\"dislike song\\\" list.\",\n  \"sync__dislike_overwrite_tip_desc\": \"The list of overwritten parties will be replaced with the list of overwriting parties.\",\n  \"sync__dislike_title\": \"Choose how to sync with {name}'s \\\"dislike song\\\" list\",\n  \"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.\",\n  \"sync__list_other_tip_desc\": \"\\\"Cancel Sync\\\" will not sync the list.\",\n  \"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.\",\n  \"sync__list_title\": \"Choose how to synchronize the list with \\\"{name}\\\"\",\n  \"sync__merge_btn_local_remote\": \"\\\"Local List\\\" Merge \\\"Remote List\\\"\",\n  \"sync__merge_btn_remote_local\": \"\\\"Remote List\\\" Merge \\\"Local List\\\"\",\n  \"sync__merge_label\": \"Merge\",\n  \"sync__merge_tip\": \"Merge: \",\n  \"sync__other_label\": \"Other\",\n  \"sync__other_tip\": \"Other: \",\n  \"sync__overwrite\": \"Full Overwrite\",\n  \"sync__overwrite_btn_cancel\": \"Cancel Sync\",\n  \"sync__overwrite_btn_local_remote\": \"\\\"Local List\\\" Overwrite \\\"Remote List\\\"\",\n  \"sync__overwrite_btn_none\": \"Only use real-time synchronization\",\n  \"sync__overwrite_btn_remote_local\": \"\\\"Remote List\\\" Overwrite \\\"Local List\\\"\",\n  \"sync__overwrite_label\": \"Overwrite\",\n  \"sync__overwrite_tip\": \"Overwrite: \",\n  \"sync_status_disabled\": \"not connected\",\n  \"tag__high_quality\": \"HQ\",\n  \"tag__lossless\": \"SQ\",\n  \"tag__lossless_24bit\": \"24bit\",\n  \"theme_add\": \"Add theme\",\n  \"theme_auto\": \"Auto\",\n  \"theme_auto_tip\": \"Right-click to open the light and dark theme options window\",\n  \"theme_black\": \"Black\",\n  \"theme_blue\": \"Blue\",\n  \"theme_blue2\": \"Purple Blue\",\n  \"theme_blue_plus\": \"Blue Plus\",\n  \"theme_china_ink\": \"China Ink\",\n  \"theme_edit_modal__app_bg\": \"App Background Color\",\n  \"theme_edit_modal__aside_color\": \"Sidebar Button Color\",\n  \"theme_edit_modal__badge\": \"Label Color\",\n  \"theme_edit_modal__badge_primary\": \"Main Color\",\n  \"theme_edit_modal__badge_secondary\": \"2nd Color\",\n  \"theme_edit_modal__badge_tertiary\": \"3rd Color\",\n  \"theme_edit_modal__bg_image\": \"Background Image\",\n  \"theme_edit_modal__bg_image_add\": \"Add background image\",\n  \"theme_edit_modal__bg_image_change\": \"Change background image\",\n  \"theme_edit_modal__bg_image_remove\": \"Remove background image\",\n  \"theme_edit_modal__close_btn\": \"Close\",\n  \"theme_edit_modal__control_btn\": \"Left Control Button Color\",\n  \"theme_edit_modal__copy\": \"Copy Theme\",\n  \"theme_edit_modal__dark\": \"Dark Theme\",\n  \"theme_edit_modal__dark_font\": \"Dark Font\",\n  \"theme_edit_modal__font\": \"Font Color\",\n  \"theme_edit_modal__hide_btn\": \"Hide Detail Page\",\n  \"theme_edit_modal__main_bg\": \"Content Area Background Color\",\n  \"theme_edit_modal__min_btn\": \"Minimize\",\n  \"theme_edit_modal__pick_cancel\": \"Reset\",\n  \"theme_edit_modal__pick_color\": \"Choose color\",\n  \"theme_edit_modal__pick_last_color\": \"Use previous color\",\n  \"theme_edit_modal__pick_save\": \"Confirm\",\n  \"theme_edit_modal__preview\": \"Preview\",\n  \"theme_edit_modal__primary\": \"Theme Color\",\n  \"theme_edit_modal__remove\": \"Remove\",\n  \"theme_edit_modal__remove_tip\": \"Do you really want to remove this theme?\",\n  \"theme_edit_modal__save_new\": \"Copy\",\n  \"theme_edit_modal__select_bg_file\": \"Choose a background image\",\n  \"theme_edit_modal__title_add\": \"Add Theme\",\n  \"theme_edit_modal__title_edit\": \"Edit Theme\",\n  \"theme_green\": \"Green\",\n  \"theme_grey\": \"Grey\",\n  \"theme_happy_new_year\": \"New Year\",\n  \"theme_max_tip\": \"You can only add up to 10 themes. Remove some and add more 😜\",\n  \"theme_mid_autumn\": \"Mid-Autumn\",\n  \"theme_ming\": \"Ming\",\n  \"theme_more_btn_show\": \"More themes\",\n  \"theme_naruto\": \"Naruto\",\n  \"theme_orange\": \"Orange\",\n  \"theme_pink\": \"Pink\",\n  \"theme_purple\": \"Purple\",\n  \"theme_red\": \"Red\",\n  \"theme_selector_modal__dark_title\": \"Dark Theme\",\n  \"theme_selector_modal__light_title\": \"Light Theme\",\n  \"theme_selector_modal__theme_name\": \"Theme name\",\n  \"theme_selector_modal__title\": \"Follow System Theme Options\",\n  \"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.\",\n  \"toggle_source_failed\": \"Failed to change the source. Please try to manually search for the song on the search page by specifying another service.\",\n  \"toggle_source_try\": \"Try switching to another source...\",\n  \"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.\",\n  \"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.\",\n  \"update__ignore_cancel\": \"I don't want to update 🤨\",\n  \"update__ignore_confirm\": \"Ok, let's update it ❤️\",\n  \"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.\",\n  \"update__ignore_confirm_tip_confirm\": \"OK, I understood\",\n  \"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😘!\",\n  \"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!\",\n  \"user_api__allow_show_update_alert\": \"Allow update popup to show\",\n  \"user_api__btn_export\": \"Export\",\n  \"user_api__btn_import\": \"Import from Local File\",\n  \"user_api__btn_import_online\": \"Import from Network\",\n  \"user_api__btn_remove\": \"Remove\",\n  \"user_api__import_file\": \"Choose Music API script file\",\n  \"user_api__init_failed_alert\": \"Failed to initialize Music API \\\"{name}\\\": \",\n  \"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.\",\n  \"user_api__noitem\": \"There is nothing here...😲\",\n  \"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.\",\n  \"user_api__readme\": \"API writing instructions: \",\n  \"user_api__title\": \"Music API Management\",\n  \"user_api__update_alert\": \"Music API \\\"{name}\\\" found new version: \",\n  \"user_api__update_alert_open_url\": \"Open update address\",\n  \"user_api_import__failed\": \"Failed to import Music API:\\n\\n{message}\",\n  \"user_api_import_online__input_confirm\": \"Import\",\n  \"user_api_import_online__input_loading\": \"Importing...\",\n  \"user_api_import_online__input_tip\": \"Please enter an HTTP link\",\n  \"user_api_import_online__title\": \"Import Music API from network.\"\n}\n"
  },
  {
    "path": "src/lang/i18n.ts",
    "content": "import { type App, ref } from 'vue'\nimport { messages } from './index'\nimport type { Messages, Message } from './index'\n\ntype TranslateValues = Record<string, string | number | boolean>\n\ntype Langs = keyof Messages\n\nexport declare interface I18n {\n  locale: Langs\n  fallbackLocale: Langs\n  availableLocales: Langs[]\n  messages: Messages\n  message: Message\n  setLanguage: (locale: Langs) => void\n  fillMessage: (message: string, val: TranslateValues) => string\n  getMessage: (key: keyof Message, val?: TranslateValues) => string\n  t: (key: keyof Message, val?: TranslateValues) => string\n}\n\nconst locale = ref<Langs>('zh-cn')\n\nlet i18n: I18n\n\n\nconst trackReactivityValues = (): any => {\n  return locale.value\n}\n\nconst i18nPlugin = {\n  install: (app: App) => {\n    // inject a globally available $translate() method\n    app.config.globalProperties.$t = (key: keyof Message, val?: TranslateValues): string => {\n      // retrieve a nested property in `options`\n      // using `key` as the path\n      // return key.split('.').reduce((o, i) => {\n      //   if (o) return o[i]\n      // }, options)\n      trackReactivityValues()\n      return i18n.getMessage(key, val)\n    }\n  },\n}\n\nconst useI18n = () => {\n  return (key: keyof Message, val?: TranslateValues): string => {\n    trackReactivityValues()\n    return i18n.getMessage(key, val)\n  }\n}\n\nconst setLanguage = (lang: Langs) => {\n  i18n.setLanguage(lang)\n}\n\nconst createI18n = (): I18n => {\n  return i18n = {\n    locale: locale.value,\n    fallbackLocale: 'zh-cn',\n    availableLocales: Object.keys(messages) as Langs[],\n    messages,\n    message: messages[locale.value],\n    setLanguage(_locale: Langs) {\n      this.locale = _locale\n      this.message = messages[_locale]\n      locale.value = _locale\n    },\n    fillMessage(message: string, vals: TranslateValues): string {\n      for (const [key, val] of Object.entries(vals)) {\n        message = message.replaceAll(`{${key}}`, String(val))\n      }\n      return message\n    },\n    getMessage(key: keyof Message, val?: TranslateValues): string {\n      let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? key\n      return val ? this.fillMessage(targetMessage, val) : targetMessage\n    },\n    t(key: keyof Message, val?: TranslateValues): string {\n      trackReactivityValues()\n      return this.getMessage(key, val)\n    },\n  }\n}\n\n\nexport {\n  i18nPlugin,\n  setLanguage,\n  useI18n,\n  createI18n,\n}\n"
  },
  {
    "path": "src/lang/index.ts",
    "content": "import zh_cn from './zh-cn.json'\nimport zh_tw from './zh-tw.json'\nimport en_us from './en-us.json'\n\ntype Message = Record<keyof typeof zh_cn, string>\n| Record<keyof typeof zh_tw, string>\n| Record<keyof typeof en_us, string>\n\ntype Messages = Record<(typeof langs)[number]['locale'], Message>\n\nconst langs = [\n  {\n    name: '简体中文',\n    locale: 'zh-cn',\n    // alternate: 'zh-hans',\n    country: 'cn',\n    fallback: true,\n    message: zh_cn,\n  },\n  {\n    name: '繁體中文',\n    locale: 'zh-tw',\n    // alternate: 'zh-hk',\n    country: 'cn',\n    message: zh_tw,\n  },\n  {\n    name: 'English',\n    locale: 'en-us',\n    country: 'us',\n    message: en_us,\n  },\n] as const\n\nconst langList: Array<{\n  name: string\n  locale: keyof Messages\n  alternate?: string\n}> = []\n// @ts-expect-error\nconst messages: Messages = {}\nlangs.forEach(item => {\n  langList.push({\n    name: item.name,\n    locale: item.locale,\n    // alternate: item.alternate,\n  })\n  messages[item.locale] = item.message\n})\n\nexport {\n  langList,\n  messages,\n}\n\nexport type {\n  Messages,\n  Message,\n}\n\nexport * from './i18n'\n"
  },
  {
    "path": "src/lang/languages.json",
    "content": "[\n  {\n    \"name\": \"简体中文\",\n    \"locale\": \"zh-cn\",\n    \"alternate\": \"zh-hans\",\n    \"country\": \"cn\",\n    \"fallback\": true\n  },\n  {\n    \"name\": \"繁體中文\",\n    \"locale\": \"zh-tw\",\n    \"alternate\": \"zh-hk\",\n    \"country\": \"cn\"\n  },\n  {\n    \"name\": \"English\",\n    \"locale\": \"en-us\",\n    \"country\": \"us\"\n  }\n]\n"
  },
  {
    "path": "src/lang/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    // \"typeRoots\": [\n    //   \"./types\"\n    // ],\n  },\n}\n"
  },
  {
    "path": "src/lang/zh-cn.json",
    "content": "{\n  \"action\": \"操作\",\n  \"agree\": \"接受\",\n  \"alert_button_text\": \"好吧\",\n  \"audio_visualization\": \"音频可视化（实验性）\",\n  \"back\": \"返回\",\n  \"btn_cancel\": \"取消\",\n  \"btn_close\": \"关闭\",\n  \"btn_confirm\": \"确定\",\n  \"btn_save\": \"保存\",\n  \"cancel_button_text\": \"我不\",\n  \"cancel_button_text_2\": \"不不不，点错了\",\n  \"close\": \"关闭\",\n  \"comment__hot_load_error\": \"热门评论加载失败，点击尝试重新加载\",\n  \"comment__hot_loading\": \"热门评论加载中...\",\n  \"comment__hot_title\": \"热门评论\",\n  \"comment__location\": \"来自{location}\",\n  \"comment__new_load_error\": \"最新评论加载失败，点击尝试重新加载\",\n  \"comment__new_loading\": \"最新评论加载中\",\n  \"comment__new_title\": \"最新评论\",\n  \"comment__no_content\": \"暂无评论\",\n  \"comment__refresh\": \"刷新评论\",\n  \"comment__show\": \"歌曲评论\",\n  \"comment__title\": \"「{name}」的评论\",\n  \"comment__unavailable\": \"此歌曲不支持获取评论\",\n  \"confirm_button_text\": \"是的\",\n  \"copy_tip\": \"（点击复制）\",\n  \"date_format_hour\": \"{num} 小时前\",\n  \"date_format_minute\": \"{num} 分钟前\",\n  \"date_format_second\": \"{num} 秒前\",\n  \"deep_link__handle_error_tip\": \"调用失败：{message}\",\n  \"default\": \"默认\",\n  \"default_list\": \"试听列表\",\n  \"desktop_lyric__back\": \"返回\",\n  \"desktop_lyric__close\": \"关闭\",\n  \"desktop_lyric__font_decrease\": \"减小字体大小\",\n  \"desktop_lyric__font_increase\": \"增加字体大小\",\n  \"desktop_lyric__lock\": \"锁定歌词\",\n  \"desktop_lyric__lrc_active_zoom_off\": \"取消缩放当前播放的歌词\",\n  \"desktop_lyric__lrc_active_zoom_on\": \"缩放当前播放的歌词\",\n  \"desktop_lyric__opacity_decrease\": \"增加透明度（右击可微调）\",\n  \"desktop_lyric__opacity_increase\": \"减小透明度（右击可微调）\",\n  \"desktop_lyric__theme\": \"主题配色\",\n  \"desktop_lyric__unlock\": \"解锁歌词\",\n  \"desktop_lyric__win_top_off\": \"取消置顶歌词界面\",\n  \"desktop_lyric__win_top_on\": \"置顶歌词界面\",\n  \"download\": \"下载\",\n  \"download___status_completed\": \"下载完成\",\n  \"download___status_error\": \"任务出错\",\n  \"download___status_paused\": \"暂停下载\",\n  \"download___status_running\": \"正在下载\",\n  \"download___status_waiting\": \"等待下载\",\n  \"download__all\": \"所有任务\",\n  \"download__error\": \"出错\",\n  \"download__finished\": \"下载完成\",\n  \"download__high_quality\": \"高清音质\",\n  \"download__lossless\": \"无损音质\",\n  \"download__multiple_tip\": \"已选择 {len} 首歌曲\",\n  \"download__multiple_tip2\": \"请选择要优先下载的音质\",\n  \"download__normal\": \"普通音质\",\n  \"download__not_available_tip\": \"该音质不可用\",\n  \"download__paused\": \"已暂停\",\n  \"download__progress\": \"进度\",\n  \"download__quality\": \"品质\",\n  \"download__running\": \"正在下载\",\n  \"download__status\": \"状态\",\n  \"download_status_error_check_path\": \"检查下载路径出错，请检查设置的下载目录是否正常\",\n  \"download_status_error_check_path_exist\": \"存在同名文件，跳过下载\",\n  \"download_status_error_refresh_url\": \"链接失效，正在刷新链接\",\n  \"download_status_error_response\": \"下载失败：\",\n  \"download_status_error_url_failed\": \"获取音乐链接失败\",\n  \"download_status_error_write\": \"歌曲保存位置被占用或没有写入权限，请尝试更改歌曲保存目录或重启软件或重启电脑，错误详情：\",\n  \"download_status_start\": \"开始下载\",\n  \"download_status_url_getting\": \"音乐链接获取中...\",\n  \"download_status_write_queue\": \"数据写入中（{num}）\",\n  \"duplicate_list_tip\": \"你之前已收藏过列表「{name}」，是否需要更新里面的歌曲？\",\n  \"export\": \"导出\",\n  \"fullscreen_exit\": \"退出全屏\",\n  \"history_clear\": \"清空搜索历史\",\n  \"history_remove\": \"右击移除该历史\",\n  \"history_search\": \"历史搜索\",\n  \"import\": \"导入\",\n  \"leaderboard\": \"排行榜\",\n  \"list__add_to\": \"添加到...\",\n  \"list__collect\": \"收藏\",\n  \"list__copy_name\": \"复制歌曲名\",\n  \"list__dislike\": \"不喜欢\",\n  \"list__download\": \"下载\",\n  \"list__export_part_desc\": \"选择列表文件保存位置\",\n  \"list__file\": \"定位文件\",\n  \"list__import_part_button_cancel\": \"不要啊\",\n  \"list__import_part_button_confirm\": \"覆盖掉\",\n  \"list__import_part_confirm\": \"导入的列表「{importName}」与本地列表「{localName}」的 ID 相同，是否覆盖本地列表？\",\n  \"list__import_part_desc\": \"选择列表文件\",\n  \"list__load_failed\": \"啊，加载失败了😭\",\n  \"list__loading\": \"列表加载中...⏳\",\n  \"list__move_to\": \"移动到...\",\n  \"list__movedown\": \"下移\",\n  \"list__moveup\": \"上移\",\n  \"list__name_default\": \"试听列表\",\n  \"list__name_love\": \"我的收藏\",\n  \"list__new_list_btn\": \"新建列表\",\n  \"list__new_list_input\": \"新列表...\",\n  \"list__pause\": \"暂停任务\",\n  \"list__play\": \"播放\",\n  \"list__play_later\": \"稍后播放\",\n  \"list__remove\": \"移除\",\n  \"list__remove_tip\": \"你真的想要移除「{name}」吗？\",\n  \"list__remove_tip_button\": \"是的，没错\",\n  \"list__rename\": \"重命名\",\n  \"list__search\": \"搜索\",\n  \"list__sort\": \"调整位置\",\n  \"list__source_detail\": \"歌曲详情页\",\n  \"list__start\": \"开始任务\",\n  \"list__sync\": \"更新\",\n  \"list__toggle_source\": \"歌曲换源\",\n  \"list_add__btn_title\": \"把该歌曲添加到「{name}」\",\n  \"list_add__multiple_btn_title\": \"把这些歌曲添加到「{name}」\",\n  \"list_add__multiple_title_add\": \"添加已选的 {num} 首歌曲到...\",\n  \"list_add__multiple_title_move\": \"移动已选的 {num} 首歌曲到...\",\n  \"list_add__title_first_add\": \"添加\",\n  \"list_add__title_first_move\": \"移动\",\n  \"list_add__title_last\": \"到...\",\n  \"list_duplicate_tip\": \"已存在同名列表，是否仍要继续创建？\",\n  \"list_import_tip__alldata\": \"导入失败，这是一个「所有数据」备份文件，你需要去这里导入：\\n\\n设置 → 备份与恢复 → 所有数据 → 导入\",\n  \"list_import_tip__playlist\": \"导入失败，这是一个「列表」备份文件，你需要去这里导入：\\n\\n设置 → 备份与恢复 → 部分数据 → 导入列表\",\n  \"list_import_tip__playlist_part\": \"导入失败，这是一个「单列表」文件，你需要去这里导入：\\n\\n我的列表 → 右击任意一个列表名 → 在弹出的菜单中选择「导入」\",\n  \"list_import_tip__setting\": \"导入失败，这是一个「设置」备份文件，你需要去这里导入：\\n\\n设置 → 备份与恢复 → 部分数据 → 导入设置\",\n  \"list_import_tip__unknown\": \"导入失败，未知的文件类型，请尝试升级到最新版本后再试\",\n  \"list_sort_modal_by_album\": \"专辑名\",\n  \"list_sort_modal_by_down\": \"降序\",\n  \"list_sort_modal_by_field\": \"排序字段\",\n  \"list_sort_modal_by_name\": \"歌曲名\",\n  \"list_sort_modal_by_random\": \"随机乱序\",\n  \"list_sort_modal_by_singer\": \"艺术家\",\n  \"list_sort_modal_by_source\": \"歌曲来源平台\",\n  \"list_sort_modal_by_time\": \"时长\",\n  \"list_sort_modal_by_type\": \"排序类别\",\n  \"list_sort_modal_by_up\": \"升序\",\n  \"list_sort_modal_tip_confirm\": \"你确定要这么做吗？\",\n  \"list_update_modal__auto_update\": \"自动更新\",\n  \"list_update_modal__tips\": \"💡 每次启动软件时将会自动更新已勾选「自动更新」的列表\",\n  \"list_update_modal__title\": \"列表更新管理\",\n  \"list_update_modal__update\": \"立即更新\",\n  \"lists__add_local_file_desc\": \"选择歌曲文件\",\n  \"lists__dislike_music_singer_tip\": \"你真的不喜欢「{singer}」的「{name}」吗？\",\n  \"lists__dislike_music_tip\": \"你真的不喜欢「{name}」吗？\",\n  \"lists__duplicate\": \"重复歌曲\",\n  \"lists__export\": \"导出\",\n  \"lists__export_part_desc\": \"选择列表文件保存位置\",\n  \"lists__import\": \"导入\",\n  \"lists__import_part_button_cancel\": \"不要啊\",\n  \"lists__import_part_button_confirm\": \"覆盖掉\",\n  \"lists__import_part_confirm\": \"导入的列表「{importName}」与本地列表「{localName}」的 ID 相同，是否覆盖本地列表？\",\n  \"lists__import_part_desc\": \"选择列表文件\",\n  \"lists__new_list_btn\": \"新建列表\",\n  \"lists__new_list_input\": \"新列表...\",\n  \"lists__remove\": \"移除\",\n  \"lists__remove_music_tip\": \"你真的要移除所选的 {len} 首歌曲吗？\",\n  \"lists__remove_tip\": \"你真的想要移除「{name}」吗？\",\n  \"lists__remove_tip_button\": \"是的，没错\",\n  \"lists__rename\": \"重命名\",\n  \"lists__select_local_file\": \"添加本地歌曲\",\n  \"lists__sort_list\": \"排序歌曲\",\n  \"lists__source_detail\": \"歌单详情页\",\n  \"lists__sync\": \"更新\",\n  \"lists__sync_confirm_tip\": \"这将会把「{name}」内的歌曲替换成在线列表的歌曲，你确认要更新吗？\",\n  \"load_list_file_error_detail\": \"我们已经帮你把旧的列表文件备份到：{path}\\n\\n它以 JSON 格式存储，你可以尝试手动修复并恢复它。\\n\\n错误详情：{detail}\",\n  \"load_list_file_error_title\": \"播放列表数据加载错误（建议到 GitHub 反馈）\",\n  \"loading\": \"加载中...\",\n  \"love_list\": \"收藏\",\n  \"lyric__load_error\": \"歌词获取失败\",\n  \"lyric__select\": \"歌词文本选择\",\n  \"lyric_menu__align\": \"歌词对齐方式\",\n  \"lyric_menu__align_center\": \"居中\",\n  \"lyric_menu__align_left\": \"居左\",\n  \"lyric_menu__lrc_size\": \"字体大小  [ {size} ]\",\n  \"lyric_menu__offset\": \"歌词偏移 [ {offset}ms ]\",\n  \"lyric_menu__offset_add_10\": \"加快 10 毫秒\",\n  \"lyric_menu__offset_add_100\": \"加快 100 毫秒\",\n  \"lyric_menu__offset_dec_10\": \"减慢 10 毫秒\",\n  \"lyric_menu__offset_dec_100\": \"减慢 100 毫秒\",\n  \"lyric_menu__offset_reset\": \"重置\",\n  \"lyric_menu__size_add\": \"加大字体（右击可微调）\",\n  \"lyric_menu__size_dec\": \"减小字体（右击可微调）\",\n  \"lyric_menu__size_reset\": \"重置\",\n  \"media_device__empty_device_tip\": \"音频输出设备为空，若出现无法播放的情况，请检查声卡驱动是否已安装。或卸载并重新安装软件，安装时选「仅为我安装」，并保持默认安装路径。\",\n  \"min\": \"最小化\",\n  \"music_album\": \"专辑名\",\n  \"music_duplicate\": \"重复歌曲\",\n  \"music_name\": \"歌曲名\",\n  \"music_singer\": \"艺术家\",\n  \"music_sort__input_tip\": \"请输入要调整到第几个位置\",\n  \"music_sort__title\": \"将「{name}」的位置调整到：\",\n  \"music_sort__title_multiple\": \"将已选的 {num} 首歌曲的位置调整到：\",\n  \"music_time\": \"时长\",\n  \"music_toggle_clean\": \"取消换源\",\n  \"music_toggle_confirm\": \"确认\",\n  \"music_toggle_duplicate_tip\": \"列表中已存在相同的歌曲，是否将其移除并继续？\",\n  \"my_list\": \"我的列表\",\n  \"no_item\": \"列表竟然是空的...\",\n  \"not_agree\": \"不接受\",\n  \"ok\": \"我知道了\",\n  \"pagination__next\": \"下一页\",\n  \"pagination__page\": \"第 {num} 页\",\n  \"pagination__prev\": \"上一页\",\n  \"play_timeout\": \"定时暂停\",\n  \"play_timeout_close\": \"关闭\",\n  \"play_timeout_confirm\": \"确认\",\n  \"play_timeout_end\": \"等待歌曲播放完毕再暂停\",\n  \"play_timeout_stop\": \"取消定时\",\n  \"play_timeout_tip\": \"{time} 后暂停播放\",\n  \"play_timeout_unit\": \"分钟\",\n  \"play_timeout_update\": \"更新定时\",\n  \"player__add_music_to\": \"添加当前歌曲到...\",\n  \"player__buffering\": \"缓冲中...\",\n  \"player__desktop_lyric_lock\": \"右击锁定歌词\",\n  \"player__desktop_lyric_off\": \"关闭桌面歌词\",\n  \"player__desktop_lyric_on\": \"开启桌面歌词\",\n  \"player__desktop_lyric_unlock\": \"右击解锁歌词\",\n  \"player__end\": \"播放完毕\",\n  \"player__error\": \"音频加载出错，5 秒后切换下一首\",\n  \"player__getting_url\": \"歌曲链接获取中...\",\n  \"player__getting_url_delay_retry\": \"服务繁忙，{time} 秒后重试...\",\n  \"player__hide_detail_tip\": \"隐藏详情页（界面内右键双击可快速隐藏详情页）\",\n  \"player__loading\": \"音乐加载中...\",\n  \"player__music_album\": \"专辑名：\",\n  \"player__music_name\": \"歌曲名：\",\n  \"player__music_singer\": \"艺术家：\",\n  \"player__next\": \"下一首\",\n  \"player__pause\": \"暂停\",\n  \"player__pic_tip\": \"播放详情页（右击在「我的列表」定位当前播放的歌曲）\",\n  \"player__play\": \"播放\",\n  \"player__play_toggle_mode_list\": \"顺序播放\",\n  \"player__play_toggle_mode_list_loop\": \"列表循环播放\",\n  \"player__play_toggle_mode_off\": \"禁用歌曲切换\",\n  \"player__play_toggle_mode_random\": \"列表随机播放\",\n  \"player__play_toggle_mode_single_loop\": \"单曲循环播放\",\n  \"player__playback_preserves_pitch\": \"音调补偿\",\n  \"player__playback_rate\": \"当前播放速率：\",\n  \"player__playback_rate_reset_btn\": \"重置\",\n  \"player__playing\": \"播放中...\",\n  \"player__prev\": \"上一首\",\n  \"player__refresh_url\": \"URL 过期，正在刷新 URL...\",\n  \"player__sound_effect\": \"音效设置（实验性）\",\n  \"player__sound_effect_biquad_filter\": \"均衡器\",\n  \"player__sound_effect_biquad_filter_preset_classical\": \"古典\",\n  \"player__sound_effect_biquad_filter_preset_dance\": \"舞曲\",\n  \"player__sound_effect_biquad_filter_preset_electronic\": \"电子乐\",\n  \"player__sound_effect_biquad_filter_preset_pop\": \"流行\",\n  \"player__sound_effect_biquad_filter_preset_rock\": \"摇滚\",\n  \"player__sound_effect_biquad_filter_preset_slow\": \"慢歌\",\n  \"player__sound_effect_biquad_filter_preset_soft\": \"柔和\",\n  \"player__sound_effect_biquad_filter_preset_subwoofer\": \"重低音\",\n  \"player__sound_effect_biquad_filter_preset_vocal\": \"人声\",\n  \"player__sound_effect_biquad_filter_reset_btn\": \"重置\",\n  \"player__sound_effect_biquad_filter_save_btn\": \"另存预设\",\n  \"player__sound_effect_biquad_filter_save_input\": \"新预设...\",\n  \"player__sound_effect_convolution\": \"环境混响音效\",\n  \"player__sound_effect_convolution_file_bright_hall\": \"大厅\",\n  \"player__sound_effect_convolution_file_cardiod_35_10_spread\": \"心形扩散\",\n  \"player__sound_effect_convolution_file_cinema_diningroom\": \"电影院\",\n  \"player__sound_effect_convolution_file_dining_living_true_stereo\": \"餐厅\",\n  \"player__sound_effect_convolution_file_feedback_spring\": \"反馈弹簧\",\n  \"player__sound_effect_convolution_file_living_bedroom_leveled\": \"卫生间\",\n  \"player__sound_effect_convolution_file_matrix_1\": \"矩阵混响（1）\",\n  \"player__sound_effect_convolution_file_matrix_2\": \"矩阵混响（2）\",\n  \"player__sound_effect_convolution_file_s2_r4_bd\": \"教堂\",\n  \"player__sound_effect_convolution_file_s3_r1_bd\": \"立体声\",\n  \"player__sound_effect_convolution_file_spreader50_65ms\": \"室内\",\n  \"player__sound_effect_convolution_file_telephone\": \"电话\",\n  \"player__sound_effect_convolution_file_tim_omni_35_10_magnetic\": \"磁性立体声\",\n  \"player__sound_effect_convolution_main_gain\": \"原始音频增益\",\n  \"player__sound_effect_convolution_send_gain\": \"环境音效增益\",\n  \"player__sound_effect_features_tip\": \"提示：「音效设置」与「自定义音频输出设备」冲突，启用音效设置后音频输出设备将会被重置为默认的输出设备，目前此问题暂无法解决。\",\n  \"player__sound_effect_panner\": \"3D 立体环绕（需使用耳机）\",\n  \"player__sound_effect_panner_enabled\": \"启用\",\n  \"player__sound_effect_panner_sound_r\": \"声音距离\",\n  \"player__sound_effect_panner_sound_speed\": \"环绕速度\",\n  \"player__sound_effect_pitch_shifter\": \"音调升降调节\",\n  \"player__sound_effect_pitch_shifter_preset_semitones\": \"{num} 半音\",\n  \"player__sound_effect_pitch_shifter_reset_btn\": \"重置\",\n  \"player__sound_effect_pitch_shifter_tip\": \"由于升降调需要实时处理音频数据，这会导致额外的 CPU 占用。\\n\\n已知问题：\\n如果 CPU 资源不够将导致处理任务堆积而出现声音异常。\\n这时需要暂停播放一段时间等堆积的任务处理完毕再播放。\",\n  \"player__stop\": \"暂停播放\",\n  \"player__volume\": \"当前音量：\",\n  \"player__volume_mute_label\": \"静音\",\n  \"player__volume_muted\": \"已静音\",\n  \"search\": \"搜索\",\n  \"search__hot_search\": \"热门搜索\",\n  \"search__type_music\": \"歌曲\",\n  \"search__type_songlist\": \"歌单\",\n  \"search__welcome\": \"搜我所想~~😉\",\n  \"setting\": \"设置\",\n  \"setting__about\": \"关于 LX Music\",\n  \"setting__backup\": \"备份与恢复\",\n  \"setting__backup_all\": \"所有数据（列表数据与设置数据）\",\n  \"setting__backup_all_export\": \"导出\",\n  \"setting__backup_all_export_desc\": \"选择备份保存位置\",\n  \"setting__backup_all_import\": \"导入\",\n  \"setting__backup_all_import_desc\": \"选择备份文件\",\n  \"setting__backup_other\": \"其他备份格式（目前不支持恢复此类备份文件）\",\n  \"setting__backup_other_export_dir\": \"选择文件保存位置\",\n  \"setting__backup_other_export_list_csv\": \"导出 CSV 格式列表\",\n  \"setting__backup_other_export_list_text\": \"导出 TXT 格式列表\",\n  \"setting__backup_other_export_list_text_confirm\": \"是否将所有列表合并为一个文件？\",\n  \"setting__backup_part\": \"部分数据（列表数据包括「试听列表」、「我的收藏」和用户自行创建的列表；设置数据不包括快捷键设置）\",\n  \"setting__backup_part_export_list\": \"导出列表\",\n  \"setting__backup_part_export_list_desc\": \"选择歌单保存位置\",\n  \"setting__backup_part_export_setting\": \"导出设置\",\n  \"setting__backup_part_export_setting_desc\": \"选择设置保存位置\",\n  \"setting__backup_part_import_list\": \"导入列表\",\n  \"setting__backup_part_import_list_confirm\": \"备份文件中列表与现有列表 ID 相同时，现有列表内歌曲将会被覆盖，是否要继续？\",\n  \"setting__backup_part_import_list_desc\": \"选择列表文件\",\n  \"setting__backup_part_import_setting\": \"导入设置\",\n  \"setting__backup_part_import_setting_desc\": \"选择配置文件\",\n  \"setting__basic\": \"基本设置\",\n  \"setting__basic_animation\": \"弹出层随机动画\",\n  \"setting__basic_control_btn_position\": \"控制按钮位置\",\n  \"setting__basic_control_btn_position_left\": \"左边\",\n  \"setting__basic_control_btn_position_right\": \"右边\",\n  \"setting__basic_font\": \"字体\",\n  \"setting__basic_font_size\": \"字体大小\",\n  \"setting__basic_font_size_14px\": \"更小\",\n  \"setting__basic_font_size_15px\": \"小\",\n  \"setting__basic_font_size_16px\": \"标准\",\n  \"setting__basic_font_size_17px\": \"大\",\n  \"setting__basic_font_size_18px\": \"更大\",\n  \"setting__basic_font_size_19px\": \"非常大\",\n  \"setting__basic_lang\": \"语言\",\n  \"setting__basic_lang_title\": \"软件显示的语言\",\n  \"setting__basic_playbar_progress_style\": \"播放栏进度条样式\",\n  \"setting__basic_playbar_progress_style_full\": \"全宽\",\n  \"setting__basic_playbar_progress_style_middle\": \"中等\",\n  \"setting__basic_playbar_progress_style_mini\": \"迷你\",\n  \"setting__basic_show_animation\": \"显示动画效果\",\n  \"setting__basic_source\": \"自定义源\",\n  \"setting__basic_source_status_failed\": \"初始化失败\",\n  \"setting__basic_source_status_initing\": \"初始化中...\",\n  \"setting__basic_source_status_success\": \"初始化成功\",\n  \"setting__basic_source_temp\": \"临时接口（软件的某些功能不可用，建议测试接口不可用再使用本接口）\",\n  \"setting__basic_source_test\": \"测试接口（几乎软件的所有功能都可用）\",\n  \"setting__basic_source_user_api_btn\": \"自定义源管理\",\n  \"setting__basic_sourcename\": \"歌曲来源名称\",\n  \"setting__basic_sourcename_alias\": \"别名\",\n  \"setting__basic_sourcename_real\": \"原名\",\n  \"setting__basic_sourcename_title\": \"选择歌曲来源名称类型\",\n  \"setting__basic_start_in_fullscreen\": \"以全屏模式启动\",\n  \"setting__basic_theme\": \"主题颜色\",\n  \"setting__basic_theme_auto_tip\": \"此乃动态主题，你可以预先设置一个亮色主题及暗色主题，此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。\\n\\n注：鼠标右击此主题项即可打开亮、暗色主题设置窗口。\",\n  \"setting__basic_to_tray\": \"关闭窗口时不退出软件将其最小化到系统托盘\",\n  \"setting__basic_window_size\": \"窗口尺寸\",\n  \"setting__basic_window_size_big\": \"大\",\n  \"setting__basic_window_size_huge\": \"巨大\",\n  \"setting__basic_window_size_larger\": \"更大\",\n  \"setting__basic_window_size_medium\": \"中\",\n  \"setting__basic_window_size_oversized\": \"超大\",\n  \"setting__basic_window_size_small\": \"小\",\n  \"setting__basic_window_size_smaller\": \"更小\",\n  \"setting__basic_window_size_title\": \"设置软件窗口尺寸\",\n  \"setting__click_copy\": \"点击复制\",\n  \"setting__click_open\": \"点击打开\",\n  \"setting__desktop_lyric\": \"桌面歌词设置\",\n  \"setting__desktop_lyric_align\": \"歌词对齐方式\",\n  \"setting__desktop_lyric_align_center\": \"居中\",\n  \"setting__desktop_lyric_align_left\": \"居左\",\n  \"setting__desktop_lyric_align_right\": \"居右\",\n  \"setting__desktop_lyric_always_on_top\": \"使歌词总是在其他窗口之上\",\n  \"setting__desktop_lyric_always_on_top_loop\": \"自动刷新歌词置顶\",\n  \"setting__desktop_lyric_always_on_top_loop_tip\": \"当歌词置顶后仍被某些程序遮挡时可尝试启用此选项\",\n  \"setting__desktop_lyric_audio_visualization\": \"音频可视化（实验性）\",\n  \"setting__desktop_lyric_color\": \"歌词字体颜色\",\n  \"setting__desktop_lyric_color_reset\": \"重置颜色\",\n  \"setting__desktop_lyric_delay_scroll\": \"延迟歌词滚动\",\n  \"setting__desktop_lyric_direction\": \"歌词显示方向\",\n  \"setting__desktop_lyric_direction_horizontal\": \"水平方向\",\n  \"setting__desktop_lyric_direction_vertical\": \"垂直方向\",\n  \"setting__desktop_lyric_ellipsis\": \"不允许歌词换行\",\n  \"setting__desktop_lyric_enable\": \"显示歌词\",\n  \"setting__desktop_lyric_font\": \"歌词字体\",\n  \"setting__desktop_lyric_font_default\": \"默认\",\n  \"setting__desktop_lyric_font_weight\": \"加粗字体\",\n  \"setting__desktop_lyric_font_weight_extended\": \"翻译、罗马音歌词\",\n  \"setting__desktop_lyric_font_weight_font\": \"逐字歌词\",\n  \"setting__desktop_lyric_font_weight_line\": \"逐行歌词\",\n  \"setting__desktop_lyric_fullscreen_hide\": \"全屏时自动关闭歌词\",\n  \"setting__desktop_lyric_hover_hide\": \"鼠标移入歌词区域时提高歌词透明度\",\n  \"setting__desktop_lyric_hover_hide_tip\": \"此功能存在平台兼容性问题，在某些平台上可能会出现预料外的问题\",\n  \"setting__desktop_lyric_line_gap\": \"歌词间距（{num}）\",\n  \"setting__desktop_lyric_line_gap_add\": \"加大间距\",\n  \"setting__desktop_lyric_line_gap_dec\": \"减小间距\",\n  \"setting__desktop_lyric_lock\": \"锁定歌词\",\n  \"setting__desktop_lyric_lock_screen\": \"不允许歌词窗口拖出主屏幕之外\",\n  \"setting__desktop_lyric_pause_hide\": \"暂停时提高歌词透明度\",\n  \"setting__desktop_lyric_played_color\": \"已播放颜色\",\n  \"setting__desktop_lyric_reset\": \"重置\",\n  \"setting__desktop_lyric_reset_window\": \"重置窗口设置\",\n  \"setting__desktop_lyric_scroll_align\": \"正在播放歌词滚动位置\",\n  \"setting__desktop_lyric_scroll_align_center\": \"中心\",\n  \"setting__desktop_lyric_scroll_align_top\": \"顶部\",\n  \"setting__desktop_lyric_shadow_color\": \"阴影颜色\",\n  \"setting__desktop_lyric_show_taskbar\": \"在任务栏显示歌词进程\",\n  \"setting__desktop_lyric_show_taskbar_tip\": \"此选项作为在录屏软件无法捕获歌词窗口时的变通解决方法\",\n  \"setting__desktop_lyric_unplay_color\": \"未播放颜色\",\n  \"setting__dislike_list_input_tip\": \"歌曲名@艺术家\\n歌曲名\\n@艺术家\",\n  \"setting__dislike_list_save_btn\": \"保存\",\n  \"setting__dislike_list_tips\": \"1. 每条一行，若歌曲或者艺术家名字中存在「@」符号，需要将其替换成「#」\\n2. 指定某艺术家的某首歌：歌曲名@艺术家\\n3. 指定某首歌：歌曲名\\n4. 指定某艺术家：@艺术家\",\n  \"setting__dislike_list_title\": \"「不喜欢的歌曲」规则列表\",\n  \"setting__download\": \"下载设置\",\n  \"setting__download_data_embed\": \"嵌入到音频文件中的内容\",\n  \"setting__download_embed_lxlyric\": \"同时嵌入 LX Music 歌词（如果有，使用 LX Music 播放时可支持逐字歌词）\",\n  \"setting__download_embed_lyric\": \"嵌入歌词\",\n  \"setting__download_embed_pic\": \"嵌入封面\",\n  \"setting__download_embed_rlyric\": \"同时嵌入罗马音歌词（如果有）\",\n  \"setting__download_embed_tlyric\": \"同时嵌入翻译歌词（如果有）\",\n  \"setting__download_enable\": \"启用下载功能\",\n  \"setting__download_lxlyric\": \"同时将 LX Music 歌词写入歌词文件中（如果有，使用 LX Music 播放时可支持逐字歌词）\",\n  \"setting__download_lyric\": \"歌词下载\",\n  \"setting__download_lyric_format\": \"下载的歌词文件编码格式\",\n  \"setting__download_lyric_format_gbk\": \"GBK\",\n  \"setting__download_lyric_format_tip\": \"在某些设备上出现中文乱码时可尝试选择 GBK 格式\",\n  \"setting__download_lyric_format_utf8\": \"UTF-8\",\n  \"setting__download_lyric_title\": \"同时下载歌词文件\",\n  \"setting__download_max_num\": \"同时下载任务数\",\n  \"setting__download_max_num_tip\": \"过大的同时下载数量可能会导致你的 IP 被自定义源封禁，是否确认修改？\",\n  \"setting__download_max_num_tooltip\": \"设置过大可能会导致 IP 被封，这取决于自定义源\",\n  \"setting__download_name\": \"文件命名方式\",\n  \"setting__download_name1\": \"歌曲名 - 艺术家\",\n  \"setting__download_name2\": \"艺术家 - 歌曲名\",\n  \"setting__download_name3\": \"歌曲名\",\n  \"setting__download_name_title\": \"下载歌曲时的命名方式\",\n  \"setting__download_path\": \"下载路径\",\n  \"setting__download_path_change_btn\": \"更改\",\n  \"setting__download_path_label\": \"当前下载路径：\",\n  \"setting__download_path_open_label\": \"点击打开当前路径\",\n  \"setting__download_path_title\": \"下载歌曲保存的路径\",\n  \"setting__download_rlyric\": \"同时将罗马音歌词写入歌词文件中（如果有）\",\n  \"setting__download_select_save_path\": \"选择歌曲保存路径\",\n  \"setting__download_skip_exist_file\": \"下载目录存在同名文件时跳过下载此任务\",\n  \"setting__download_tlyric\": \"同时将翻译歌词写入歌词文件中（如果有）\",\n  \"setting__download_use_other_source\": \"自动换源下载\",\n  \"setting__download_use_other_source_tip\": \"当无法从歌曲的原始来源下载时，尝试切换到其他来源下载。\\n注：此功能不 100% 保证换源后的歌曲版本与原版一致。\",\n  \"setting__hot_key\": \"快捷键设置\",\n  \"setting__hot_key_common_focus_search_input\": \"聚焦搜索框\",\n  \"setting__hot_key_common_min\": \"最小化程序\",\n  \"setting__hot_key_common_toggle_close\": \"退出程序\",\n  \"setting__hot_key_common_toggle_hide\": \"显示/隐藏程序\",\n  \"setting__hot_key_common_toggle_min\": \"最小化/还原程序\",\n  \"setting__hot_key_desktop_lyric_toggle_always_top\": \"桌面歌词置顶切换\",\n  \"setting__hot_key_desktop_lyric_toggle_lock\": \"桌面歌词锁定切换\",\n  \"setting__hot_key_desktop_lyric_toggle_visible\": \"开/关桌面歌词\",\n  \"setting__hot_key_global_title\": \"全局快捷键\",\n  \"setting__hot_key_local_title\": \"软件内快捷键\",\n  \"setting__hot_key_player_music_dislike\": \"不喜欢该歌曲\",\n  \"setting__hot_key_player_music_love\": \"收藏歌曲\",\n  \"setting__hot_key_player_music_unlove\": \"取消收藏\",\n  \"setting__hot_key_player_next\": \"下一首歌曲\",\n  \"setting__hot_key_player_prev\": \"上一首歌曲\",\n  \"setting__hot_key_player_seekbackward\": \"快退 5s\",\n  \"setting__hot_key_player_seekforward\": \"快进 5s\",\n  \"setting__hot_key_player_toggle_play\": \"播放/暂停控制\",\n  \"setting__hot_key_player_volume_down\": \"减少音量\",\n  \"setting__hot_key_player_volume_mute\": \"静音切换\",\n  \"setting__hot_key_player_volume_up\": \"增加音量\",\n  \"setting__hot_key_tip_input\": \"请输入新的按键\",\n  \"setting__hot_key_unset_input\": \"未设置\",\n  \"setting__is_enable\": \"启用\",\n  \"setting__is_show\": \"显示\",\n  \"setting__list\": \"列表设置\",\n  \"setting__list_action_btn\": \"显示列表操作按钮\",\n  \"setting__list_add_music_location_type\": \"添加歌曲到列表时的位置\",\n  \"setting__list_add_music_location_type_bottom\": \"底部\",\n  \"setting__list_add_music_location_type_top\": \"顶部\",\n  \"setting__list_click_action\": \"双击列表里的歌曲时自动切换到当前列表播放（仅对「歌单」和「排行榜」有效）\",\n  \"setting__list_scroll\": \"记住播放列表滚动条位置（仅对「我的列表」有效）\",\n  \"setting__list_source\": \"显示歌曲来源平台（仅对「我的列表」有效）\",\n  \"setting__network\": \"网络设置\",\n  \"setting__network_proxy_host\": \"主机\",\n  \"setting__network_proxy_password\": \"密码\",\n  \"setting__network_proxy_port\": \"端口\",\n  \"setting__network_proxy_title\": \"HTTP 代理设置（乱设置软件将无法联网）\",\n  \"setting__network_proxy_username\": \"用户名\",\n  \"setting__odc\": \"强迫症设置\",\n  \"setting__odc_clear_search_input\": \"离开搜索界面时清空搜索框\",\n  \"setting__odc_clear_search_list\": \"离开搜索界面时清空搜索列表\",\n  \"setting__open_api\": \"开放 API\",\n  \"setting__open_api_address\": \"服务地址：\",\n  \"setting__open_api_bind_lan\": \"允许来自局域网的访问\",\n  \"setting__open_api_enable\": \"启用开放 API 服务\",\n  \"setting__open_api_port\": \"服务端口\",\n  \"setting__open_api_port_tip\": \"请输入开放 API 服务端口\",\n  \"setting__open_api_tip\": \"该功能用于为第三方软件提供调用 LX Music 的能力，目前可用的功能可以看：\",\n  \"setting__open_api_tip_link\": \"接入文档\",\n  \"setting__other\": \"其他\",\n  \"setting__other_dislike_list\": \"「不喜欢的歌曲」规则\",\n  \"setting__other_dislike_list_label\": \"规则数量：\",\n  \"setting__other_dislike_list_show_btn\": \"编辑规则\",\n  \"setting__other_listdata\": \"列表数据清理\",\n  \"setting__other_listdata_clear_btn\": \"清空「我的列表」数据\",\n  \"setting__other_listdata_clear_tip_confirm\": \"这将清理你创建的「所有列表」及收藏的「所有歌曲」，是否真的要继续？\",\n  \"setting__other_lyric_edited_cache\": \"已调整过偏移时间的歌词管理\",\n  \"setting__other_lyric_edited_clear_btn\": \"清理已调整过时间的歌词\",\n  \"setting__other_lyric_edited_clear_tip_confirm\": \"这将清理所有你之前已调整过偏移时间的歌词，是否确认清理？（手抖确认🤪）\",\n  \"setting__other_lyric_edited_label\": \"歌词数量：\",\n  \"setting__other_lyric_raw_clear_btn\": \"清理歌词缓存\",\n  \"setting__other_lyric_raw_label\": \"歌词数量：\",\n  \"setting__other_music_url_clear_btn\": \"清理歌曲 URL 缓存\",\n  \"setting__other_music_url_label\": \"歌曲 URL 数量：\",\n  \"setting__other_other_cache\": \"其他缓存管理\",\n  \"setting__other_other_source_clear_btn\": \"清理换源歌曲缓存\",\n  \"setting__other_other_source_label\": \"换源歌曲信息数量：\",\n  \"setting__other_resource_cache\": \"资源缓存管理\",\n  \"setting__other_resource_cache_clear_btn\": \"清理资源缓存\",\n  \"setting__other_resource_cache_confirm\": \"我要清掉\",\n  \"setting__other_resource_cache_label\": \"软件已使用缓存大小：\",\n  \"setting__other_resource_cache_tip\": \"包括图片、音频等缓存，清理后这些资源将需要重新下载，不建议清理，软件会根据磁盘空间动态管理缓存大小\",\n  \"setting__other_resource_cache_tip_confirm\": \"涉及图片、音频等缓存，清理后这些资源将需要重新下载，不建议清理，软件会根据磁盘空间动态管理缓存大小，是否仍要清理？\",\n  \"setting__other_transparent_window\": \"主窗口使用软件内置的圆角及阴影\",\n  \"setting__other_transparent_window_tip\": \"关闭后将使用系统原生窗口样式。更改后重启软件生效。\",\n  \"setting__other_tray_theme\": \"托盘图标样式\",\n  \"setting__other_tray_theme_auto\": \"跟随系统亮暗模式\",\n  \"setting__other_tray_theme_black\": \"黑色\",\n  \"setting__other_tray_theme_native\": \"白色\",\n  \"setting__other_tray_theme_origin\": \"原色\",\n  \"setting__play\": \"播放设置\",\n  \"setting__play_advanced_audio_features_tip\": \"自定义音频输出设备与该功能冲突，启用该功能后音频输出设备将会被重置为默认，目前此问题暂无法解决。是否仍要开启？\",\n  \"setting__play_auto_clean_played_list\": \"点击与播放列表相同的列表切歌时清空已播放列表\",\n  \"setting__play_auto_clean_played_list_tip\": \"随机模式下列表内所有歌曲会重新参与随机\",\n  \"setting__play_auto_skip_on_error\": \"播放错误时自动切换歌曲\",\n  \"setting__play_detail\": \"播放详情页设置\",\n  \"setting__play_detail_align\": \"歌词对齐方式\",\n  \"setting__play_detail_align_center\": \"居中\",\n  \"setting__play_detail_align_left\": \"居左\",\n  \"setting__play_detail_align_right\": \"居右\",\n  \"setting__play_detail_font_size\": \"歌词字体大小（可以在播放详情页使用键盘的「+」「-」调整字体大小）\",\n  \"setting__play_detail_font_size_current\": \"当前字体大小：{size}\",\n  \"setting__play_detail_font_size_reset\": \"重置\",\n  \"setting__play_detail_font_zoom\": \"缩放当前正在播放的歌词\",\n  \"setting__play_detail_lyric_delay_scroll\": \"延迟歌词滚动\",\n  \"setting__play_detail_lyric_progress\": \"允许通过拖拽歌词调整播放进度\",\n  \"setting__play_lyric_lxlrc\": \"使用卡拉 OK 式歌词播放（如果可用）\",\n  \"setting__play_lyric_lxlrc_tip\": \"此功能比较耗性能，低配置电脑不建议开启！\",\n  \"setting__play_lyric_roma\": \"显示歌词罗马音（如果可用）\",\n  \"setting__play_lyric_s2t\": \"将播放与下载的中文歌词转换为繁体\",\n  \"setting__play_lyric_transition\": \"显示歌词翻译（如果可用）\",\n  \"setting__play_max_output_channel_count\": \"使用设备能处理的最大声道数输出音频\",\n  \"setting__play_mediaDevice\": \"音频输出\",\n  \"setting__play_mediaDevice_remove_stop_play\": \"当前的声音输出设备被改变时暂停播放歌曲\",\n  \"setting__play_mediaDevice_title\": \"选择声音输出的媒体设备\",\n  \"setting__play_media_device_error_tip\": \"此功能与高级音频功能（音频可视化、音效设置、使用设备能处理的最大声道数输出音频功能）冲突。你本次启动软件时已启用这些功能，此选项暂不可用。请关闭这些功能并重启软件后，再修改此选项！\",\n  \"setting__play_media_device_tip\": \"此功能与音频可视化功能冲突，两者无法同时启用。是否关闭「音频可视化」并应用所选音频输出设置？\",\n  \"setting__play_playQuality\": \"优先播放的音质（如果可用）\",\n  \"setting__play_power_save_blocker\": \"播放歌曲时阻止电脑休眠\",\n  \"setting__play_save_play_time\": \"记住播放进度\",\n  \"setting__play_startup_auto_play\": \"启动软件后自动播放音乐\",\n  \"setting__play_statusbar_lyric\": \"在菜单栏的状态菜单显示歌词\",\n  \"setting__play_statusbar_lyric_tip\": \"需要在「设置 → 基本设置」启用「关闭窗口时不退出软件将其最小化到系统托盘」。\",\n  \"setting__play_task_bar\": \"在任务栏上显示当前歌曲播放进度\",\n  \"setting__play_timeout\": \"定时暂停\",\n  \"setting__player_audio_visualization_tip\": \"自定义音频输出设备与音频可视化功能冲突，启用「音频可视化」后音频输出设备将会被重置为默认，目前此问题暂无法解决。是否仍要开启？\",\n  \"setting__player_swap_lyric_trans_roma\": \"调换歌词翻译与歌词罗马音位置\",\n  \"setting__search\": \"搜索设置\",\n  \"setting__search_focus_search_box\": \"启动时自动聚焦搜索框\",\n  \"setting__search_history\": \"显示历史搜索记录\",\n  \"setting__search_hot\": \"显示热门搜索\",\n  \"setting__sync\": \"数据同步\",\n  \"setting__sync_client_address\": \"当前设备地址：{address}\",\n  \"setting__sync_client_host\": \"同步服务地址\",\n  \"setting__sync_client_host_tip\": \"http://<IP 地址>:<端口号>\",\n  \"setting__sync_client_mode\": \"客户端模式\",\n  \"setting__sync_client_status\": \"状态：{status}\",\n  \"setting__sync_code_blocked_ip\": \"当前设备的 IP 已被服务端封禁！\",\n  \"setting__sync_code_fail\": \"连接码无效\",\n  \"setting__sync_enable\": \"启用同步功能\",\n  \"setting__sync_mode\": \"同步模式\",\n  \"setting__sync_mode_client\": \"客户端模式\",\n  \"setting__sync_mode_server\": \"服务端模式\",\n  \"setting__sync_server_address\": \"同步服务地址：{address}\",\n  \"setting__sync_server_auth_code\": \"连接码：{code}\",\n  \"setting__sync_server_device\": \"已连接的设备：{devices}\",\n  \"setting__sync_server_device_list_btn_remove\": \"移除\",\n  \"setting__sync_server_device_list_noitem\": \"这里啥也没有 ┗( ▔, ▔ )┛\",\n  \"setting__sync_server_device_list_time\": \"最后连接时间：{time}\",\n  \"setting__sync_server_device_list_tips\": \"💡 设备被移除后，再连接时需要重新输入连接码\",\n  \"setting__sync_server_device_list_title\": \"已认证设备\",\n  \"setting__sync_server_mode\": \"服务端模式（由于数据是明文传输，请在受信任的网络下使用）\",\n  \"setting__sync_server_port\": \"同步端口设置\",\n  \"setting__sync_server_port_tip\": \"请输入同步服务端口号\",\n  \"setting__sync_server_refresh_code\": \"刷新连接码\",\n  \"setting__sync_server_show_device_list\": \"已认证设备列表\",\n  \"setting__sync_tip\": \"使用方式请看常见问题「同步功能」部分\",\n  \"setting__update\": \"软件更新\",\n  \"setting__update_checking\": \"检查更新中...\",\n  \"setting__update_commit_date\": \"提交日期：\",\n  \"setting__update_commit_id\": \"代码版本：\",\n  \"setting__update_current_label\": \"当前版本：\",\n  \"setting__update_downloading\": \"发现新版本并在努力下载中，请稍后...⏳\",\n  \"setting__update_init\": \"处理更新中...\",\n  \"setting__update_latest\": \"软件已是最新，尽情地体验吧~🥂\",\n  \"setting__update_latest_label\": \"最新版本：\",\n  \"setting__update_new_version\": \"发现新版本，赶快去更新吧~🚀🚀\",\n  \"setting__update_open_version_modal_btn\": \"打开更新窗口\",\n  \"setting__update_progress\": \"状态：\",\n  \"setting__update_show_change_log\": \"更新版本后的首次启动时显示更新日志\",\n  \"setting__update_try_auto_update\": \"发现新版本时尝试自动下载更新\",\n  \"setting__update_unknown\": \"未知\",\n  \"setting__update_unknown_tip\": \"❓ 获取最新版本信息失败，建议去「关于」页面打开项目发布地址查看当前版本是否最新\",\n  \"setting_download_save_group_list_name\": \"将文件保存到以对应列表命名的子目录中\",\n  \"setting_sync_status_enabled\": \"已连接\",\n  \"song_list\": \"歌单\",\n  \"songlist__import_input_btn_confirm\": \"打开\",\n  \"songlist__import_input_show_btn\": \"打开歌单\",\n  \"songlist__import_input_tip\": \"输入歌单链接或歌单 ID\",\n  \"songlist__import_input_tip_1\": \"不支持跨源打开歌单，请确认要打开的歌单与当前选择的歌单来源是否对应\",\n  \"songlist__import_input_tip_2\": \"若遇到无法打开的歌单链接，欢迎反馈\",\n  \"songlist__import_input_tip_3\": \"酷狗源歌单不支持用歌单 ID 或概念版链接打开，但支持用普通版链接或酷狗码打开\",\n  \"songlist__import_input_tip_4\": \"网易源的「我喜欢」歌单需要 Token 才能打开，详情看 \",\n  \"songlist__import_input_title\": \"打开分享的歌单\",\n  \"songlist__open_list\": \"打开「{name}」歌单\",\n  \"songlist__tag_info_hot_tag\": \"热门标签\",\n  \"source_alias_all\": \"聚合大会\",\n  \"source_alias_bd\": \"小杜音乐\",\n  \"source_alias_kg\": \"小枸音乐\",\n  \"source_alias_kw\": \"小蜗音乐\",\n  \"source_alias_mg\": \"小蜜音乐\",\n  \"source_alias_tx\": \"小秋音乐\",\n  \"source_alias_wy\": \"小芸音乐\",\n  \"source_alias_xm\": \"小霞音乐\",\n  \"source_all\": \"聚合搜索\",\n  \"source_bd\": \"百度音乐\",\n  \"source_kg\": \"酷狗音乐\",\n  \"source_kw\": \"酷我音乐\",\n  \"source_mg\": \"咪咕音乐\",\n  \"source_tx\": \"企鹅音乐\",\n  \"source_wy\": \"网易音乐\",\n  \"source_xm\": \"虾米音乐\",\n  \"sync__auth_code_input_tip\": \"请输入连接码\",\n  \"sync__auth_code_title\": \"需要输入连接码\",\n  \"sync__dislike_merge_tip_desc\": \"合并两边列表内容并去重\",\n  \"sync__dislike_other_tip_desc\": \"「取消同步」将不使用「不喜欢的歌曲」列表同步功能\",\n  \"sync__dislike_overwrite_tip_desc\": \"被覆盖者的列表将被替换成覆盖者的列表\",\n  \"sync__dislike_title\": \"选择与「{name}」的「不喜欢的歌曲」列表同步方式\",\n  \"sync__list_merge_tip_desc\": \"将两边的列表合并到一起，相同的歌曲将被去掉（去掉的是被合并者的歌曲），不同的歌曲将被添加。\",\n  \"sync__list_other_tip_desc\": \"「取消同步」将不使用列表同步功能。\",\n  \"sync__list_overwrite_tip_desc\": \"被覆盖者与覆盖者列表 ID 相同的列表将被移除后替换成覆盖者的列表（列表 ID 不同的列表将被合并到一起）。若勾选「完全覆盖」，则被覆盖者的所有列表将被移除，然后替换成覆盖者的列表。\",\n  \"sync__list_title\": \"选择与「{name}」的列表同步方式\",\n  \"sync__merge_btn_local_remote\": \"「本机列表」合并「远程列表」\",\n  \"sync__merge_btn_remote_local\": \"「远程列表」合并「本机列表」\",\n  \"sync__merge_label\": \"合并\",\n  \"sync__merge_tip\": \"合并：\",\n  \"sync__other_label\": \"其他\",\n  \"sync__other_tip\": \"其他：\",\n  \"sync__overwrite\": \"完全覆盖\",\n  \"sync__overwrite_btn_cancel\": \"取消同步\",\n  \"sync__overwrite_btn_local_remote\": \"「本机列表」覆盖「远程列表」\",\n  \"sync__overwrite_btn_none\": \"仅使用实时同步功能\",\n  \"sync__overwrite_btn_remote_local\": \"「远程列表」覆盖「本机列表」\",\n  \"sync__overwrite_label\": \"覆盖\",\n  \"sync__overwrite_tip\": \"覆盖:\",\n  \"sync_status_disabled\": \"未连接\",\n  \"tag__high_quality\": \"HQ\",\n  \"tag__lossless\": \"SQ\",\n  \"tag__lossless_24bit\": \"24bit\",\n  \"theme_add\": \"添加主题\",\n  \"theme_auto\": \"道法自然\",\n  \"theme_auto_tip\": \"鼠标右击可打开亮、暗主题设置窗口\",\n  \"theme_black\": \"黑灯瞎火\",\n  \"theme_blue\": \"蓝田生玉\",\n  \"theme_blue2\": \"清热版蓝\",\n  \"theme_blue_plus\": \"蛋雅深蓝\",\n  \"theme_china_ink\": \"近墨者黑\",\n  \"theme_edit_modal__app_bg\": \"应用背景颜色\",\n  \"theme_edit_modal__aside_color\": \"侧栏按钮颜色\",\n  \"theme_edit_modal__badge\": \"标签颜色\",\n  \"theme_edit_modal__badge_primary\": \"主颜色\",\n  \"theme_edit_modal__badge_secondary\": \"次要颜色\",\n  \"theme_edit_modal__badge_tertiary\": \"第三颜色\",\n  \"theme_edit_modal__bg_image\": \"背景图片\",\n  \"theme_edit_modal__bg_image_add\": \"背景图片：\",\n  \"theme_edit_modal__bg_image_change\": \"更改背景图片\",\n  \"theme_edit_modal__bg_image_remove\": \"移除背景图片\",\n  \"theme_edit_modal__close_btn\": \"关闭\",\n  \"theme_edit_modal__control_btn\": \"左侧控制按钮颜色\",\n  \"theme_edit_modal__copy\": \"复制主题\",\n  \"theme_edit_modal__dark\": \"暗色主题\",\n  \"theme_edit_modal__dark_font\": \"深色字体\",\n  \"theme_edit_modal__font\": \"字体颜色\",\n  \"theme_edit_modal__hide_btn\": \"隐藏播放详情页\",\n  \"theme_edit_modal__main_bg\": \"内容区域背景颜色\",\n  \"theme_edit_modal__min_btn\": \"最小化\",\n  \"theme_edit_modal__pick_cancel\": \"重置\",\n  \"theme_edit_modal__pick_color\": \"选择颜色\",\n  \"theme_edit_modal__pick_last_color\": \"使用之前的颜色\",\n  \"theme_edit_modal__pick_save\": \"确认\",\n  \"theme_edit_modal__preview\": \"预览主题\",\n  \"theme_edit_modal__primary\": \"主题色\",\n  \"theme_edit_modal__remove\": \"移除\",\n  \"theme_edit_modal__remove_tip\": \"是否真的要移除这个主题？\",\n  \"theme_edit_modal__save_new\": \"另存\",\n  \"theme_edit_modal__select_bg_file\": \"选择背景图片\",\n  \"theme_edit_modal__title_add\": \"添加主题\",\n  \"theme_edit_modal__title_edit\": \"编辑主题\",\n  \"theme_green\": \"绿意盎然\",\n  \"theme_grey\": \"灰常美丽\",\n  \"theme_happy_new_year\": \"新年快乐\",\n  \"theme_max_tip\": \"最多只能添加 10 个主题哦，删掉一些再添加吧😜\",\n  \"theme_mid_autumn\": \"月里嫦娥\",\n  \"theme_ming\": \"青出于黑\",\n  \"theme_more_btn_show\": \"更多主题\",\n  \"theme_naruto\": \"木叶之村\",\n  \"theme_orange\": \"橙黄橘绿\",\n  \"theme_pink\": \"粉装玉琢\",\n  \"theme_purple\": \"重斤球紫\",\n  \"theme_red\": \"热情似火\",\n  \"theme_selector_modal__dark_title\": \"暗色主题\",\n  \"theme_selector_modal__light_title\": \"亮色主题\",\n  \"theme_selector_modal__theme_name\": \"主题名称\",\n  \"theme_selector_modal__title\": \"跟随系统主题设置\",\n  \"theme_selector_modal__title_tip\": \"注：你可以预先设置一个亮色主题及暗色主题，此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。\",\n  \"toggle_source_failed\": \"换源失败，请尝试手动在搜索页指定其他来源搜索该歌曲播放\",\n  \"toggle_source_try\": \"尝试切换到其他来源...\",\n  \"update__downgrade_tip\": \"我们发现你降级了版本（{ver}），若使用新版本时遇到问题，请先尝试阅读常见问题解决，若你遇到的问题在常见问题中未记录或无法解决，可以通过文档中提到的反馈渠道给我们反馈😘！\\n\\n注意：从新版本降级旧版时建议先备份歌单，若出现异常则可通过清理数据解决，数据目录路径文档有记录。\",\n  \"update__error_top\": \"自动下载新版本失败，你可以尝试重新下载更新或者手动去下载更新。\\n\\n新版地址在更新弹窗下面有写，下载新版直接覆盖安装即可，若安装失败则看常见问题解决。\\n\\n注意：目前只有 Windows 安装版可以自动更新（Linux 的 AppImage、deb 版似乎也可以，未测试），其他版本请手动下载更新！\",\n  \"update__ignore_cancel\": \"我就不想更新🤨\",\n  \"update__ignore_confirm\": \"好，去更新看看❤️\",\n  \"update__ignore_confirm_tip\": \"目前只有 Windows 安装版可以自动更新（Linux 的 AppImage、deb 版似乎也可以，未测试），其他版本请手动下载更新，\\n新版地址在更新弹窗下面有写，下载新版直接覆盖安装即可，若安装失败看常见问题解决。\",\n  \"update__ignore_confirm_tip_confirm\": \"OK，已了解\",\n  \"update__ignore_tip\": \"你现在使用的版本距离最新版本已经落后了 {num} 个版本🤪，为了更好的使用体验，建议更新到最新版本哦~！\\n\\n注：若使用新版本时遇到问题，请先尝试阅读常见问题解决，若你遇到的问题在常见问题中未记录或无法解决，可以通过文档中提到的反馈渠道给我们反馈😘！\",\n  \"update__timeout_top\": \"下载时间过长提示\\n\\n你当前所在网络访问 GitHub 较慢，新版本已经下了一个钟了还没完成😳，你仍可选择继续等，但墙裂建议手动更新版本！\",\n  \"user_api__allow_show_update_alert\": \"允许显示更新弹窗\",\n  \"user_api__btn_export\": \"导出\",\n  \"user_api__btn_import\": \"本地导入\",\n  \"user_api__btn_import_online\": \"在线导入\",\n  \"user_api__btn_remove\": \"移除\",\n  \"user_api__import_file\": \"选择自定义源 API 脚本文件\",\n  \"user_api__init_failed_alert\": \"自定义源「{name}」初始化失败：\",\n  \"user_api__max_tip\": \"最多只能同时存在 20 个源哦🤪\\n想要继续导入的话，请先移除一些旧的源腾出位置吧\",\n  \"user_api__noitem\": \"这里竟然是空的😲\",\n  \"user_api__note\": \"提示：虽然我们已经尽可能地隔离了脚本的运行环境，但导入包含恶意行为的脚本仍可能会影响你的系统，请谨慎导入。\",\n  \"user_api__readme\": \"源编写说明：\",\n  \"user_api__title\": \"自定义源管理\",\n  \"user_api__update_alert\": \"自定义源「{name}」发现新版本：\",\n  \"user_api__update_alert_open_url\": \"打开更新地址\",\n  \"user_api_import__failed\": \"自定义源导入失败：\\n{message}\",\n  \"user_api_import_online__input_confirm\": \"导入\",\n  \"user_api_import_online__input_loading\": \"导入中...\",\n  \"user_api_import_online__input_tip\": \"请输入 HTTP 链接\",\n  \"user_api_import_online__title\": \"在线导入自定义源\"\n}\n"
  },
  {
    "path": "src/lang/zh-tw.json",
    "content": "{\n  \"action\": \"操作\",\n  \"agree\": \"接受\",\n  \"alert_button_text\": \"好吧\",\n  \"audio_visualization\": \"音訊視覺化（實驗性）\",\n  \"back\": \"返回\",\n  \"btn_cancel\": \"取消\",\n  \"btn_close\": \"關閉\",\n  \"btn_confirm\": \"確定\",\n  \"btn_save\": \"儲存\",\n  \"cancel_button_text\": \"我不\",\n  \"cancel_button_text_2\": \"不不不，點錯了\",\n  \"close\": \"關閉\",\n  \"comment__hot_load_error\": \"熱門評論載入失敗，點擊嘗試重新載入\",\n  \"comment__hot_loading\": \"熱門評論載入中...\",\n  \"comment__hot_title\": \"熱門評論\",\n  \"comment__location\": \"來自{location}\",\n  \"comment__new_load_error\": \"最新評論載入失敗，點擊嘗試重新載入\",\n  \"comment__new_loading\": \"最新評論載入中\",\n  \"comment__new_title\": \"最新評論\",\n  \"comment__no_content\": \"暫無評論\",\n  \"comment__refresh\": \"重新整理評論\",\n  \"comment__show\": \"歌曲評論\",\n  \"comment__title\": \"「{name}」的評論\",\n  \"comment__unavailable\": \"此歌曲不支援獲取評論\",\n  \"confirm_button_text\": \"是的\",\n  \"copy_tip\": \"（點擊複製）\",\n  \"date_format_hour\": \"{num} 小時前\",\n  \"date_format_minute\": \"{num} 分鐘前\",\n  \"date_format_second\": \"{num} 秒前\",\n  \"deep_link__handle_error_tip\": \"呼叫失敗：{message}\",\n  \"default\": \"預設\",\n  \"default_list\": \"試聽清單\",\n  \"desktop_lyric__back\": \"返回\",\n  \"desktop_lyric__close\": \"關閉\",\n  \"desktop_lyric__font_decrease\": \"減小字體大小\",\n  \"desktop_lyric__font_increase\": \"增加字體大小\",\n  \"desktop_lyric__lock\": \"鎖定歌詞視窗\",\n  \"desktop_lyric__lrc_active_zoom_off\": \"取消縮放目前播放的歌詞\",\n  \"desktop_lyric__lrc_active_zoom_on\": \"縮放目前播放的歌詞\",\n  \"desktop_lyric__opacity_decrease\": \"增加透明度（右擊可微調）\",\n  \"desktop_lyric__opacity_increase\": \"減小透明度（右擊可微調）\",\n  \"desktop_lyric__theme\": \"主題配色\",\n  \"desktop_lyric__unlock\": \"解鎖歌詞視窗\",\n  \"desktop_lyric__win_top_off\": \"取消置頂歌詞視窗\",\n  \"desktop_lyric__win_top_on\": \"置頂歌詞視窗\",\n  \"download\": \"下載\",\n  \"download___status_completed\": \"下載完成\",\n  \"download___status_error\": \"任務出錯\",\n  \"download___status_paused\": \"暫停下載\",\n  \"download___status_running\": \"正在下載\",\n  \"download___status_waiting\": \"等待下載\",\n  \"download__all\": \"所有任務\",\n  \"download__error\": \"出錯\",\n  \"download__finished\": \"下載完成\",\n  \"download__high_quality\": \"高音質\",\n  \"download__lossless\": \"無損音質\",\n  \"download__multiple_tip\": \"已選取 {len} 首歌曲\",\n  \"download__multiple_tip2\": \"請選取要優先下載的音質\",\n  \"download__normal\": \"一般音質\",\n  \"download__not_available_tip\": \"該音質不可用\",\n  \"download__paused\": \"已暫停\",\n  \"download__progress\": \"進度\",\n  \"download__quality\": \"品質\",\n  \"download__running\": \"正在下載\",\n  \"download__status\": \"狀態\",\n  \"download_status_error_check_path\": \"檢查下載路徑出錯，請檢查設定的下載目錄是否正常\",\n  \"download_status_error_check_path_exist\": \"存在同名檔案，跳過下載\",\n  \"download_status_error_refresh_url\": \"連結失效，正在重新整理連結\",\n  \"download_status_error_response\": \"下載失敗：\",\n  \"download_status_error_url_failed\": \"獲取音樂連結失敗\",\n  \"download_status_error_write\": \"歌曲儲存位置被占用或沒有寫入權限，請嘗試變更歌曲儲存目錄或重啟軟體或重啟電腦，錯誤詳情：\",\n  \"download_status_start\": \"開始下載\",\n  \"download_status_url_getting\": \"音樂連結獲取中...\",\n  \"download_status_write_queue\": \"資料寫入中（{num}）\",\n  \"duplicate_list_tip\": \"你之前已收藏過清單「{name}」，是否需要更新裡面的歌曲？\",\n  \"export\": \"匯出\",\n  \"fullscreen_exit\": \"退出全螢幕\",\n  \"history_clear\": \"清空搜尋歷史\",\n  \"history_remove\": \"右擊移除該歷史\",\n  \"history_search\": \"歷史搜尋\",\n  \"import\": \"匯入\",\n  \"leaderboard\": \"排行榜\",\n  \"list__add_to\": \"加入到...\",\n  \"list__collect\": \"收藏\",\n  \"list__copy_name\": \"複製歌曲名\",\n  \"list__dislike\": \"不喜歡\",\n  \"list__download\": \"下載\",\n  \"list__export_part_desc\": \"選取清單檔案儲存位置\",\n  \"list__file\": \"定位檔案\",\n  \"list__import_part_button_cancel\": \"不要啊\",\n  \"list__import_part_button_confirm\": \"覆寫掉\",\n  \"list__import_part_confirm\": \"匯入的清單「{importName}」與本機清單「{localName}」的 ID 相同，是否覆寫本機清單？\",\n  \"list__import_part_desc\": \"選取清單檔案\",\n  \"list__load_failed\": \"啊，載入失敗了😭\",\n  \"list__loading\": \"清單載入中...⏳\",\n  \"list__move_to\": \"移動到...\",\n  \"list__movedown\": \"下移\",\n  \"list__moveup\": \"上移\",\n  \"list__name_default\": \"試聽清單\",\n  \"list__name_love\": \"我的最愛\",\n  \"list__new_list_btn\": \"建立清單\",\n  \"list__new_list_input\": \"新清單...\",\n  \"list__pause\": \"暫停任務\",\n  \"list__play\": \"播放\",\n  \"list__play_later\": \"稍後播放\",\n  \"list__remove\": \"移除\",\n  \"list__remove_tip\": \"你真的想要移除「{name}」嗎？\",\n  \"list__remove_tip_button\": \"是的，沒錯\",\n  \"list__rename\": \"重新命名\",\n  \"list__search\": \"搜尋\",\n  \"list__sort\": \"調整位置\",\n  \"list__source_detail\": \"歌曲詳情頁\",\n  \"list__start\": \"開始任務\",\n  \"list__sync\": \"更新\",\n  \"list__toggle_source\": \"變更來源\",\n  \"list_add__btn_title\": \"把該歌曲加入到「{name}」\",\n  \"list_add__multiple_btn_title\": \"把這些歌曲加入到「{name}」\",\n  \"list_add__multiple_title_add\": \"將已選的 {num} 首歌曲加入到...\",\n  \"list_add__multiple_title_move\": \"將已選的 {num} 首歌曲移動到...\",\n  \"list_add__title_first_add\": \"添加\",\n  \"list_add__title_first_move\": \"移動\",\n  \"list_add__title_last\": \"到...\",\n  \"list_duplicate_tip\": \"已存在同名清單，是否仍要繼續建立？\",\n  \"list_import_tip__alldata\": \"匯入失敗，這是一個「所有資料」備份檔案，你需要在這裡匯入：\\n\\n設定 → 備份與復原 → 所有資料 → 匯入\",\n  \"list_import_tip__playlist\": \"匯入失敗，這是一個「清單」備份檔案，你需要在這裡匯入：\\n\\n設定 → 備份與復原 → 部分資料 → 匯入清單\",\n  \"list_import_tip__playlist_part\": \"匯入失敗，這是一個「僅清單」檔案，你需要在這裡匯入：\\n\\n我的清單 → 右擊任意一個清單名 → 在彈出的選單中選取「匯入」\",\n  \"list_import_tip__setting\": \"匯入失敗，這是一個「設定」備份檔案，你需要在這裡匯入：\\n\\n設定 → 備份與復原 → 部分資料 → 匯入設定\",\n  \"list_import_tip__unknown\": \"匯入失敗，未知的檔案類型，請嘗試升級到最新版本後再試\",\n  \"list_sort_modal_by_album\": \"專輯\",\n  \"list_sort_modal_by_down\": \"降序\",\n  \"list_sort_modal_by_field\": \"排序欄位\",\n  \"list_sort_modal_by_name\": \"標題\",\n  \"list_sort_modal_by_random\": \"隨機亂序\",\n  \"list_sort_modal_by_singer\": \"演出者\",\n  \"list_sort_modal_by_source\": \"音樂串流平台\",\n  \"list_sort_modal_by_time\": \"長度\",\n  \"list_sort_modal_by_type\": \"排序類別\",\n  \"list_sort_modal_by_up\": \"升序\",\n  \"list_sort_modal_tip_confirm\": \"你確定要這麼做嗎？\",\n  \"list_update_modal__auto_update\": \"自動更新\",\n  \"list_update_modal__tips\": \"💡 每次啟動軟體時將會自動更新已勾選「自動更新」的清單\",\n  \"list_update_modal__title\": \"清單更新管理\",\n  \"list_update_modal__update\": \"立即更新\",\n  \"lists__add_local_file_desc\": \"選取歌曲檔案\",\n  \"lists__dislike_music_singer_tip\": \"你真的不喜歡「{singer}」的「{name}」嗎？\",\n  \"lists__dislike_music_tip\": \"你真的不喜歡「{name}」嗎？\",\n  \"lists__duplicate\": \"重複歌曲\",\n  \"lists__export\": \"匯出\",\n  \"lists__export_part_desc\": \"選取清單檔案儲存位置\",\n  \"lists__import\": \"匯入\",\n  \"lists__import_part_button_cancel\": \"不要啊\",\n  \"lists__import_part_button_confirm\": \"覆寫掉\",\n  \"lists__import_part_confirm\": \"匯入的清單「{importName}」與本機清單「{localName}」的 ID 相同，是否覆寫本機清單？\",\n  \"lists__import_part_desc\": \"選取清單檔案\",\n  \"lists__new_list_btn\": \"建立清單\",\n  \"lists__new_list_input\": \"新清單...\",\n  \"lists__remove\": \"移除\",\n  \"lists__remove_music_tip\": \"你真的要移除所選的 {len} 首歌曲嗎？\",\n  \"lists__remove_tip\": \"你真的想要移除「{name}」嗎？\",\n  \"lists__remove_tip_button\": \"是的，沒錯\",\n  \"lists__rename\": \"重新命名\",\n  \"lists__select_local_file\": \"添加本機歌曲\",\n  \"lists__sort_list\": \"排序歌曲\",\n  \"lists__source_detail\": \"歌單詳情頁\",\n  \"lists__sync\": \"更新\",\n  \"lists__sync_confirm_tip\": \"這將會把「{name}」內的歌曲取代成線上清單的歌曲，你確認要更新嗎？\",\n  \"load_list_file_error_detail\": \"我們已經幫你把舊的清單檔案備份到：{path}\\n\\n它以 JSON 格式儲存，你可以嘗試手動修復並復原它。\\n\\n錯誤詳情：{detail}\",\n  \"load_list_file_error_title\": \"播放清單資料載入錯誤（建議到 GitHub 回報）\",\n  \"loading\": \"載入中...\",\n  \"love_list\": \"收藏\",\n  \"lyric__load_error\": \"歌詞獲取失敗\",\n  \"lyric__select\": \"歌詞文字選取\",\n  \"lyric_menu__align\": \"歌詞對齊方式\",\n  \"lyric_menu__align_center\": \"置中\",\n  \"lyric_menu__align_left\": \"置左\",\n  \"lyric_menu__lrc_size\": \"字體大小  [ {size} ]\",\n  \"lyric_menu__offset\": \"歌詞偏移 [ {offset}ms ]\",\n  \"lyric_menu__offset_add_10\": \"加快 10 毫秒\",\n  \"lyric_menu__offset_add_100\": \"加快 100 毫秒\",\n  \"lyric_menu__offset_dec_10\": \"減慢 10 毫秒\",\n  \"lyric_menu__offset_dec_100\": \"減慢 100 毫秒\",\n  \"lyric_menu__offset_reset\": \"重設\",\n  \"lyric_menu__size_add\": \"加大字體（右擊可微調）\",\n  \"lyric_menu__size_dec\": \"減小字體（右擊可微調）\",\n  \"lyric_menu__size_reset\": \"重設\",\n  \"media_device__empty_device_tip\": \"音頻輸出設備為空，若出現無法播放的情況，請檢查聲卡驅動是否已安裝。\\n或卸載並重新安裝軟件，安裝時選「僅為我安裝」，並保持默認安裝路徑。\",\n  \"min\": \"最小化\",\n  \"music_album\": \"專輯\",\n  \"music_duplicate\": \"重複歌曲\",\n  \"music_name\": \"標題\",\n  \"music_singer\": \"演出者\",\n  \"music_sort__input_tip\": \"請輸入要調整到第幾個位置\",\n  \"music_sort__title\": \"將「{name}」的位置調整到：\",\n  \"music_sort__title_multiple\": \"將已選取的 {num} 首歌曲的位置調整到：\",\n  \"music_time\": \"長度\",\n  \"music_toggle_clean\": \"取消變更\",\n  \"music_toggle_confirm\": \"確認\",\n  \"music_toggle_duplicate_tip\": \"列表中已存在相同的歌曲，是否將其移除並繼續？\",\n  \"my_list\": \"我的清單\",\n  \"no_item\": \"清單竟然是空的...\",\n  \"not_agree\": \"不接受\",\n  \"ok\": \"我知道了\",\n  \"pagination__next\": \"下一頁\",\n  \"pagination__page\": \"第 {num} 頁\",\n  \"pagination__prev\": \"上一頁\",\n  \"play_timeout\": \"定時暫停\",\n  \"play_timeout_close\": \"關閉\",\n  \"play_timeout_confirm\": \"確認\",\n  \"play_timeout_end\": \"等待歌曲播放完畢再暫停\",\n  \"play_timeout_stop\": \"取消定時\",\n  \"play_timeout_tip\": \"{time} 後暫停播放\",\n  \"play_timeout_unit\": \"分鐘\",\n  \"play_timeout_update\": \"更新定時\",\n  \"player__add_music_to\": \"將目前歌曲加入到...\",\n  \"player__buffering\": \"緩衝中...\",\n  \"player__desktop_lyric_lock\": \"右擊鎖定歌詞\",\n  \"player__desktop_lyric_off\": \"關閉歌詞視窗\",\n  \"player__desktop_lyric_on\": \"開啟歌詞視窗\",\n  \"player__desktop_lyric_unlock\": \"右擊解鎖歌詞\",\n  \"player__end\": \"播放完畢\",\n  \"player__error\": \"音訊載入出錯，5 秒後切換下一首\",\n  \"player__getting_url\": \"歌曲連結獲取中...\",\n  \"player__getting_url_delay_retry\": \"服務繁忙，{time} 秒後重試...\",\n  \"player__hide_detail_tip\": \"隱藏詳情頁（介面內右鍵雙擊可快速隱藏詳情頁）\",\n  \"player__loading\": \"音樂載入中...\",\n  \"player__music_album\": \"專輯：\",\n  \"player__music_name\": \"標題：\",\n  \"player__music_singer\": \"演出者：\",\n  \"player__next\": \"下一首\",\n  \"player__pause\": \"暫停\",\n  \"player__pic_tip\": \"播放詳情頁（右擊在「我的清單」定位目前播放的歌曲）\",\n  \"player__play\": \"播放\",\n  \"player__play_toggle_mode_list\": \"順序播放\",\n  \"player__play_toggle_mode_list_loop\": \"重複播放清單\",\n  \"player__play_toggle_mode_off\": \"停用歌曲切換\",\n  \"player__play_toggle_mode_random\": \"隨機播放\",\n  \"player__play_toggle_mode_single_loop\": \"重複播放\",\n  \"player__playback_preserves_pitch\": \"音調補償\",\n  \"player__playback_rate\": \"目前播放速率：\",\n  \"player__playback_rate_reset_btn\": \"重設\",\n  \"player__playing\": \"播放中...\",\n  \"player__prev\": \"上一首\",\n  \"player__refresh_url\": \"URL 過期，正在重新整理 URL...\",\n  \"player__sound_effect\": \"音效設定（實驗性）\",\n  \"player__sound_effect_biquad_filter\": \"均衡器\",\n  \"player__sound_effect_biquad_filter_preset_classical\": \"古典\",\n  \"player__sound_effect_biquad_filter_preset_dance\": \"舞曲\",\n  \"player__sound_effect_biquad_filter_preset_electronic\": \"電子樂\",\n  \"player__sound_effect_biquad_filter_preset_pop\": \"流行\",\n  \"player__sound_effect_biquad_filter_preset_rock\": \"搖滾\",\n  \"player__sound_effect_biquad_filter_preset_slow\": \"慢歌\",\n  \"player__sound_effect_biquad_filter_preset_soft\": \"柔和\",\n  \"player__sound_effect_biquad_filter_preset_subwoofer\": \"重低音\",\n  \"player__sound_effect_biquad_filter_preset_vocal\": \"人聲\",\n  \"player__sound_effect_biquad_filter_reset_btn\": \"重設\",\n  \"player__sound_effect_biquad_filter_save_btn\": \"另存\",\n  \"player__sound_effect_biquad_filter_save_input\": \"新預設...\",\n  \"player__sound_effect_convolution\": \"環境混響音效\",\n  \"player__sound_effect_convolution_file_bright_hall\": \"大廳\",\n  \"player__sound_effect_convolution_file_cardiod_35_10_spread\": \"心形擴散\",\n  \"player__sound_effect_convolution_file_cinema_diningroom\": \"電影院\",\n  \"player__sound_effect_convolution_file_dining_living_true_stereo\": \"餐廳\",\n  \"player__sound_effect_convolution_file_feedback_spring\": \"回饋彈簧\",\n  \"player__sound_effect_convolution_file_living_bedroom_leveled\": \"洗手間\",\n  \"player__sound_effect_convolution_file_matrix_1\": \"矩陣混響（1）\",\n  \"player__sound_effect_convolution_file_matrix_2\": \"矩陣混響（2）\",\n  \"player__sound_effect_convolution_file_s2_r4_bd\": \"教堂\",\n  \"player__sound_effect_convolution_file_s3_r1_bd\": \"立體聲\",\n  \"player__sound_effect_convolution_file_spreader50_65ms\": \"室內\",\n  \"player__sound_effect_convolution_file_telephone\": \"電話\",\n  \"player__sound_effect_convolution_file_tim_omni_35_10_magnetic\": \"磁性立體聲\",\n  \"player__sound_effect_convolution_main_gain\": \"原始音訊增益\",\n  \"player__sound_effect_convolution_send_gain\": \"環境音效增益\",\n  \"player__sound_effect_features_tip\": \"提示：「音效設定」與「自訂音訊輸出裝置」衝突，啟用音效設定後音訊輸出裝置將會被重設為預設的輸出裝置，目前此問題暫無法解決。\",\n  \"player__sound_effect_panner\": \"3D 立體環繞（需使用耳機）\",\n  \"player__sound_effect_panner_enabled\": \"啟用\",\n  \"player__sound_effect_panner_sound_r\": \"聲音距離\",\n  \"player__sound_effect_panner_sound_speed\": \"環繞速度\",\n  \"player__sound_effect_pitch_shifter\": \"音調升降調節\",\n  \"player__sound_effect_pitch_shifter_preset_semitones\": \"{num} 半音\",\n  \"player__sound_effect_pitch_shifter_reset_btn\": \"重設\",\n  \"player__sound_effect_pitch_shifter_tip\": \"由於升降調需要即時處理音訊資料，這會導致額外的 CPU 占用。\\n\\n已知問題：\\n如果 CPU 資源不夠將導致處理任務堆積而出現聲音異常。\\n這時需要暫停播放一段時間等堆積的任務處理完畢再播放。\",\n  \"player__stop\": \"暫停播放\",\n  \"player__volume\": \"目前音量：\",\n  \"player__volume_mute_label\": \"靜音\",\n  \"player__volume_muted\": \"已靜音\",\n  \"search\": \"搜尋\",\n  \"search__hot_search\": \"熱門搜尋\",\n  \"search__type_music\": \"歌曲\",\n  \"search__type_songlist\": \"歌單\",\n  \"search__welcome\": \"搜我所想~~😉\",\n  \"setting\": \"設定\",\n  \"setting__about\": \"關於 LX Music\",\n  \"setting__backup\": \"備份與復原\",\n  \"setting__backup_all\": \"所有資料（清單資料與設定資料）\",\n  \"setting__backup_all_export\": \"匯出\",\n  \"setting__backup_all_export_desc\": \"選取備份儲存位置\",\n  \"setting__backup_all_import\": \"匯入\",\n  \"setting__backup_all_import_desc\": \"選取備份檔案\",\n  \"setting__backup_other\": \"其他備份格式（目前不支援復原此類備份檔案）\",\n  \"setting__backup_other_export_dir\": \"選取檔案儲存位置\",\n  \"setting__backup_other_export_list_csv\": \"匯出 CSV 格式清單\",\n  \"setting__backup_other_export_list_text\": \"匯出 TXT 格式清單\",\n  \"setting__backup_other_export_list_text_confirm\": \"是否將所有清單合併為一個檔案？\",\n  \"setting__backup_part\": \"部分資料（清單資料包括「試聽清單」、「我的最愛」和使用者自行建立的清單；設定資料不包括快速鍵設定）\",\n  \"setting__backup_part_export_list\": \"匯出清單\",\n  \"setting__backup_part_export_list_desc\": \"選取歌單儲存位置\",\n  \"setting__backup_part_export_setting\": \"匯出設定\",\n  \"setting__backup_part_export_setting_desc\": \"選取設定儲存位置\",\n  \"setting__backup_part_import_list\": \"匯入清單\",\n  \"setting__backup_part_import_list_confirm\": \"備份檔案中清單與現有清單 ID 相同時，現有清單內歌曲將會被覆寫，是否要繼續？\",\n  \"setting__backup_part_import_list_desc\": \"選取清單檔案\",\n  \"setting__backup_part_import_setting\": \"匯入設定\",\n  \"setting__backup_part_import_setting_desc\": \"選取設定檔\",\n  \"setting__basic\": \"基本設定\",\n  \"setting__basic_animation\": \"彈出層隨機動畫\",\n  \"setting__basic_control_btn_position\": \"控制按鈕位置\",\n  \"setting__basic_control_btn_position_left\": \"左邊\",\n  \"setting__basic_control_btn_position_right\": \"右邊\",\n  \"setting__basic_font\": \"字體\",\n  \"setting__basic_font_size\": \"字體大小\",\n  \"setting__basic_font_size_14px\": \"更小\",\n  \"setting__basic_font_size_15px\": \"小\",\n  \"setting__basic_font_size_16px\": \"標準\",\n  \"setting__basic_font_size_17px\": \"大\",\n  \"setting__basic_font_size_18px\": \"更大\",\n  \"setting__basic_font_size_19px\": \"非常大\",\n  \"setting__basic_lang\": \"語言\",\n  \"setting__basic_lang_title\": \"軟體顯示的語言\",\n  \"setting__basic_playbar_progress_style\": \"播放欄進度條樣式\",\n  \"setting__basic_playbar_progress_style_full\": \"全寬\",\n  \"setting__basic_playbar_progress_style_middle\": \"中等\",\n  \"setting__basic_playbar_progress_style_mini\": \"迷你\",\n  \"setting__basic_show_animation\": \"顯示動畫效果\",\n  \"setting__basic_source\": \"自訂來源 API\",\n  \"setting__basic_source_status_failed\": \"初始化失敗\",\n  \"setting__basic_source_status_initing\": \"初始化中...\",\n  \"setting__basic_source_status_success\": \"初始化成功\",\n  \"setting__basic_source_temp\": \"臨時 API（軟體的某些功能不可用，建議測試 API 不可用時再使用這個 API）\",\n  \"setting__basic_source_test\": \"測試 API（幾乎軟體的所有功能都可用）\",\n  \"setting__basic_source_user_api_btn\": \"自訂來源 API 管理\",\n  \"setting__basic_sourcename\": \"音樂串流平台名稱\",\n  \"setting__basic_sourcename_alias\": \"別名\",\n  \"setting__basic_sourcename_real\": \"原名\",\n  \"setting__basic_sourcename_title\": \"選取音樂串流平台名稱類型\",\n  \"setting__basic_start_in_fullscreen\": \"以全螢幕模式啟動\",\n  \"setting__basic_theme\": \"主題顏色\",\n  \"setting__basic_theme_auto_tip\": \"此乃動態主題，你可以預先設定一個亮色主題及暗色主題，此後將根據系統的亮、暗主題色自動切換為你預先設定的相應主題。\\n\\n註：滑鼠右擊此主題項即可開啟亮、暗色主題設定視窗。\",\n  \"setting__basic_to_tray\": \"關閉視窗時不退出軟體將其最小化到系統匣\",\n  \"setting__basic_window_size\": \"視窗尺寸\",\n  \"setting__basic_window_size_big\": \"大\",\n  \"setting__basic_window_size_huge\": \"巨大\",\n  \"setting__basic_window_size_larger\": \"更大\",\n  \"setting__basic_window_size_medium\": \"中\",\n  \"setting__basic_window_size_oversized\": \"超大\",\n  \"setting__basic_window_size_small\": \"小\",\n  \"setting__basic_window_size_smaller\": \"更大\",\n  \"setting__basic_window_size_title\": \"設定軟體視窗尺寸\",\n  \"setting__click_copy\": \"點擊複製\",\n  \"setting__click_open\": \"點擊開啟\",\n  \"setting__desktop_lyric\": \"桌面歌詞設定\",\n  \"setting__desktop_lyric_align\": \"歌詞對齊方式\",\n  \"setting__desktop_lyric_align_center\": \"置中\",\n  \"setting__desktop_lyric_align_left\": \"置左\",\n  \"setting__desktop_lyric_align_right\": \"置右\",\n  \"setting__desktop_lyric_always_on_top\": \"使歌詞視窗總是在其他視窗之上\",\n  \"setting__desktop_lyric_always_on_top_loop\": \"自動重新整理歌詞視窗置頂\",\n  \"setting__desktop_lyric_always_on_top_loop_tip\": \"當歌詞視窗置頂後仍被某些程式遮擋時可嘗試啟用此選項\",\n  \"setting__desktop_lyric_audio_visualization\": \"音訊視覺化（實驗性）\",\n  \"setting__desktop_lyric_color\": \"歌詞字體顏色\",\n  \"setting__desktop_lyric_color_reset\": \"重設顏色\",\n  \"setting__desktop_lyric_delay_scroll\": \"延遲歌詞滾動\",\n  \"setting__desktop_lyric_direction\": \"歌詞顯示方向\",\n  \"setting__desktop_lyric_direction_horizontal\": \"水平方向\",\n  \"setting__desktop_lyric_direction_vertical\": \"垂直方向\",\n  \"setting__desktop_lyric_ellipsis\": \"不允許歌詞換行\",\n  \"setting__desktop_lyric_enable\": \"顯示歌詞視窗\",\n  \"setting__desktop_lyric_font\": \"歌詞字體\",\n  \"setting__desktop_lyric_font_default\": \"預設\",\n  \"setting__desktop_lyric_font_weight\": \"加粗字體\",\n  \"setting__desktop_lyric_font_weight_extended\": \"翻譯、羅馬音歌詞\",\n  \"setting__desktop_lyric_font_weight_font\": \"逐字歌詞\",\n  \"setting__desktop_lyric_font_weight_line\": \"逐行歌詞\",\n  \"setting__desktop_lyric_fullscreen_hide\": \"全螢幕時自動關閉歌詞視窗\",\n  \"setting__desktop_lyric_hover_hide\": \"滑鼠移入歌詞視窗時增加歌詞透明度\",\n  \"setting__desktop_lyric_hover_hide_tip\": \"此功能存在平台相容性問題，在某些平台上可能會出現預料外的問題\",\n  \"setting__desktop_lyric_line_gap\": \"歌詞間距（{num}）\",\n  \"setting__desktop_lyric_line_gap_add\": \"加大間距\",\n  \"setting__desktop_lyric_line_gap_dec\": \"減小間距\",\n  \"setting__desktop_lyric_lock\": \"鎖定歌詞視窗\",\n  \"setting__desktop_lyric_lock_screen\": \"不允許歌詞視窗拖出主螢幕之外\",\n  \"setting__desktop_lyric_pause_hide\": \"暫停時增加歌詞視窗透明度\",\n  \"setting__desktop_lyric_played_color\": \"已播放顏色\",\n  \"setting__desktop_lyric_reset\": \"重設\",\n  \"setting__desktop_lyric_reset_window\": \"重設視窗設定\",\n  \"setting__desktop_lyric_scroll_align\": \"正在播放歌詞滾動位置\",\n  \"setting__desktop_lyric_scroll_align_center\": \"中心\",\n  \"setting__desktop_lyric_scroll_align_top\": \"頂部\",\n  \"setting__desktop_lyric_shadow_color\": \"陰影顏色\",\n  \"setting__desktop_lyric_show_taskbar\": \"在工具列顯示歌詞視窗處理程式\",\n  \"setting__desktop_lyric_show_taskbar_tip\": \"此選項作為在螢幕錄影軟體無法擷取歌詞視窗時的變通解決方法\",\n  \"setting__desktop_lyric_unplay_color\": \"未播放顏色\",\n  \"setting__dislike_list_input_tip\": \"標題@演出者\\n標題\\n@演出者\",\n  \"setting__dislike_list_save_btn\": \"儲存\",\n  \"setting__dislike_list_tips\": \"1. 每條一行，若歌曲或者演出者名字中存在「@」符號，需要將其取代成「#」\\n2. 指定某演出者的某首歌：標題@演出者\\n3. 指定某首歌：標題\\n4. 指定某演出者：@演出者\",\n  \"setting__dislike_list_title\": \"「不喜歡的歌曲」規則清單\",\n  \"setting__download\": \"下載設定\",\n  \"setting__download_data_embed\": \"嵌入到音訊檔案中的內容\",\n  \"setting__download_embed_lxlyric\": \"同時嵌入 LX Music 歌詞（如果有，使用 LX Music 播放時可支持逐字歌詞）\",\n  \"setting__download_embed_lyric\": \"嵌入歌詞\",\n  \"setting__download_embed_pic\": \"嵌入封面\",\n  \"setting__download_embed_rlyric\": \"同時嵌入羅馬音歌詞（如果有）\",\n  \"setting__download_embed_tlyric\": \"同時嵌入翻譯歌詞（如果有）\",\n  \"setting__download_enable\": \"啟用下載功能\",\n  \"setting__download_lxlyric\": \"同時將 LX Music 歌詞寫入歌詞文件中（如果有，使用 LX Music 播放時可支持逐字歌詞）\",\n  \"setting__download_lyric\": \"歌詞下載\",\n  \"setting__download_lyric_format\": \"下載的歌詞檔案編碼格式\",\n  \"setting__download_lyric_format_gbk\": \"GBK\",\n  \"setting__download_lyric_format_tip\": \"在某些裝置上出現中文亂碼時可嘗試選取 GBK 格式\",\n  \"setting__download_lyric_format_utf8\": \"UTF-8\",\n  \"setting__download_lyric_title\": \"同時下載歌詞檔案\",\n  \"setting__download_max_num\": \"同時下載任務數\",\n  \"setting__download_max_num_tip\": \"過大的同時下載數量可能會導致你的 IP 被自訂來源 API 封禁，是否確認修改？\",\n  \"setting__download_max_num_tooltip\": \"設定過大可能會導致 IP 被封，這取決於自訂來源 API\",\n  \"setting__download_name\": \"檔案命名方式\",\n  \"setting__download_name1\": \"「標題 - 演出者」\",\n  \"setting__download_name2\": \"「演出者 - 標題」\",\n  \"setting__download_name3\": \"僅標題\",\n  \"setting__download_name_title\": \"下載歌曲時的命名方式\",\n  \"setting__download_path\": \"下載路徑\",\n  \"setting__download_path_change_btn\": \"變更\",\n  \"setting__download_path_label\": \"目前下載路徑：\",\n  \"setting__download_path_open_label\": \"點擊開啟目前路徑\",\n  \"setting__download_path_title\": \"下載歌曲儲存的路徑\",\n  \"setting__download_rlyric\": \"同時將羅馬音歌詞寫入歌詞檔案中（如果有）\",\n  \"setting__download_select_save_path\": \"選取歌曲儲存路徑\",\n  \"setting__download_skip_exist_file\": \"下載目錄存在同名檔案時跳過下載此任務\",\n  \"setting__download_tlyric\": \"同時將翻譯歌詞寫入歌詞檔案中（如果有）\",\n  \"setting__download_use_other_source\": \"自動變更來源下載\",\n  \"setting__download_use_other_source_tip\": \"當無法從歌曲的原始來源下載時，嘗試變更到其他來源下載。\\n註：此功能不 100% 保證變更來源後的歌曲與原版一致。\",\n  \"setting__hot_key\": \"快速鍵設定\",\n  \"setting__hot_key_common_focus_search_input\": \"聚焦搜尋框\",\n  \"setting__hot_key_common_min\": \"最小化軟體\",\n  \"setting__hot_key_common_toggle_close\": \"退出軟體\",\n  \"setting__hot_key_common_toggle_hide\": \"顯示/隱藏軟體\",\n  \"setting__hot_key_common_toggle_min\": \"最小化/還原軟體\",\n  \"setting__hot_key_desktop_lyric_toggle_always_top\": \"歌詞視窗置頂切換\",\n  \"setting__hot_key_desktop_lyric_toggle_lock\": \"歌詞視窗鎖定切換\",\n  \"setting__hot_key_desktop_lyric_toggle_visible\": \"開啟/關閉歌詞視窗\",\n  \"setting__hot_key_global_title\": \"全域快速鍵\",\n  \"setting__hot_key_local_title\": \"軟體內快速鍵\",\n  \"setting__hot_key_player_music_dislike\": \"不喜歡該歌曲\",\n  \"setting__hot_key_player_music_love\": \"收藏歌曲\",\n  \"setting__hot_key_player_music_unlove\": \"取消收藏\",\n  \"setting__hot_key_player_next\": \"下一首歌曲\",\n  \"setting__hot_key_player_prev\": \"上一首歌曲\",\n  \"setting__hot_key_player_seekbackward\": \"快退 5s\",\n  \"setting__hot_key_player_seekforward\": \"快進 5s\",\n  \"setting__hot_key_player_toggle_play\": \"播放/暫停控制\",\n  \"setting__hot_key_player_volume_down\": \"減少音量\",\n  \"setting__hot_key_player_volume_mute\": \"靜音切換\",\n  \"setting__hot_key_player_volume_up\": \"增加音量\",\n  \"setting__hot_key_tip_input\": \"請輸入新的按鍵\",\n  \"setting__hot_key_unset_input\": \"未設定\",\n  \"setting__is_enable\": \"啟用\",\n  \"setting__is_show\": \"顯示\",\n  \"setting__list\": \"清單設定\",\n  \"setting__list_action_btn\": \"顯示清單操作按鈕\",\n  \"setting__list_add_music_location_type\": \"加入歌曲到清單時的位置\",\n  \"setting__list_add_music_location_type_bottom\": \"底部\",\n  \"setting__list_add_music_location_type_top\": \"頂部\",\n  \"setting__list_click_action\": \"雙擊清單裡的歌曲時自動切換到目前清單播放（僅對「歌單」、「排行榜」有效）\",\n  \"setting__list_scroll\": \"記住播放清單滾動條位置（僅對「我的清單」有效）\",\n  \"setting__list_source\": \"顯示歌曲來自哪個音樂串流平台（僅對「我的清單」有效）\",\n  \"setting__network\": \"網路設定\",\n  \"setting__network_proxy_host\": \"主機\",\n  \"setting__network_proxy_password\": \"密碼\",\n  \"setting__network_proxy_port\": \"埠\",\n  \"setting__network_proxy_title\": \"HTTP 代理設定（亂設定軟體將無法聯網）\",\n  \"setting__network_proxy_username\": \"使用者名稱\",\n  \"setting__odc\": \"強迫症設定\",\n  \"setting__odc_clear_search_input\": \"離開搜尋頁面時清空搜尋框\",\n  \"setting__odc_clear_search_list\": \"離開搜尋頁面時清空搜尋清單\",\n  \"setting__open_api\": \"開放 API\",\n  \"setting__open_api_address\": \"服務位址：\",\n  \"setting__open_api_bind_lan\": \"允許來自區域網路的訪問\",\n  \"setting__open_api_enable\": \"啟用開放 API 服務\",\n  \"setting__open_api_port\": \"服務埠\",\n  \"setting__open_api_port_tip\": \"請輸入開放 API 服務埠\",\n  \"setting__open_api_tip\": \"該功能用於為第三方軟體提供串接 LX Music 的能力，目前可用的功能可以看：\",\n  \"setting__open_api_tip_link\": \"串接教學文件\",\n  \"setting__other\": \"其他\",\n  \"setting__other_dislike_list\": \"「不喜歡的歌曲」規則\",\n  \"setting__other_dislike_list_label\": \"規則數量：\",\n  \"setting__other_dislike_list_show_btn\": \"編輯規則\",\n  \"setting__other_listdata\": \"清單資料清理\",\n  \"setting__other_listdata_clear_btn\": \"清空「我的清單」資料\",\n  \"setting__other_listdata_clear_tip_confirm\": \"這將清理你建立的「所有清單」及收藏的「所有歌曲」，是否真的要繼續？\",\n  \"setting__other_lyric_edited_cache\": \"已調整過偏移時間的歌詞管理\",\n  \"setting__other_lyric_edited_clear_btn\": \"清理已調整過時間的歌詞\",\n  \"setting__other_lyric_edited_clear_tip_confirm\": \"這將清理所有你之前已調整過偏移時間的歌詞，是否確認清理？（手抖確認🤪）\",\n  \"setting__other_lyric_edited_label\": \"歌詞數量：\",\n  \"setting__other_lyric_raw_clear_btn\": \"清理歌詞快取\",\n  \"setting__other_lyric_raw_label\": \"歌詞數量：\",\n  \"setting__other_music_url_clear_btn\": \"清理歌曲 URL 快取\",\n  \"setting__other_music_url_label\": \"歌曲 URL 數量：\",\n  \"setting__other_other_cache\": \"其他快取管理\",\n  \"setting__other_other_source_clear_btn\": \"清理變更來源歌曲快取\",\n  \"setting__other_other_source_label\": \"變更來源歌曲資訊數量：\",\n  \"setting__other_resource_cache\": \"資源快取管理\",\n  \"setting__other_resource_cache_clear_btn\": \"清理資源快取\",\n  \"setting__other_resource_cache_confirm\": \"我要清掉\",\n  \"setting__other_resource_cache_label\": \"軟體已使用快取大小：\",\n  \"setting__other_resource_cache_tip\": \"包括圖片、音訊等快取，清理後這些資源將需要重新下載，不建議清理，軟體會根據磁碟空間動態管理快取大小\",\n  \"setting__other_resource_cache_tip_confirm\": \"涉及圖片、音訊等快取，清理後這些資源將需要重新下載，不建議清理，軟體會根據磁碟空間動態管理快取大小，是否仍要清理？\",\n  \"setting__other_transparent_window\": \"主視窗使用軟體內建的圓角及陰影\",\n  \"setting__other_transparent_window_tip\": \"關閉後將使用系統原生視窗樣式。更改後重啟軟體生效。\",\n  \"setting__other_tray_theme\": \"系統匣圖示樣式\",\n  \"setting__other_tray_theme_auto\": \"跟隨系統亮暗模式\",\n  \"setting__other_tray_theme_black\": \"黑色\",\n  \"setting__other_tray_theme_native\": \"白色\",\n  \"setting__other_tray_theme_origin\": \"原色\",\n  \"setting__play\": \"播放設定\",\n  \"setting__play_advanced_audio_features_tip\": \"自訂音訊輸出裝置與該功能衝突，啟用該功能後音訊輸出裝置將會被重設為預設，目前此問題暫無法解決。是否仍要開啟？\",\n  \"setting__play_auto_clean_played_list\": \"點擊與播放清單相同的清單切歌時清空已播放清單\",\n  \"setting__play_auto_clean_played_list_tip\": \"隨機模式下清單內所有歌曲會重新參與隨機\",\n  \"setting__play_auto_skip_on_error\": \"播放錯誤時自動切換歌曲\",\n  \"setting__play_detail\": \"播放詳情頁設定\",\n  \"setting__play_detail_align\": \"歌詞對齊方式\",\n  \"setting__play_detail_align_center\": \"置中\",\n  \"setting__play_detail_align_left\": \"置左\",\n  \"setting__play_detail_align_right\": \"置右\",\n  \"setting__play_detail_font_size\": \"歌詞字體大小（可以在播放詳情頁使用鍵盤的「+」「-」調整字體大小）\",\n  \"setting__play_detail_font_size_current\": \"目前字體大小：{size}\",\n  \"setting__play_detail_font_size_reset\": \"重設\",\n  \"setting__play_detail_font_zoom\": \"縮放目前正在播放的歌詞\",\n  \"setting__play_detail_lyric_delay_scroll\": \"延遲歌詞滾動\",\n  \"setting__play_detail_lyric_progress\": \"允許透過拖曳歌詞調整播放進度\",\n  \"setting__play_lyric_lxlrc\": \"使用卡拉 OK 式歌詞播放（如果可用）\",\n  \"setting__play_lyric_lxlrc_tip\": \"此功能比較耗性能，低配置電腦不建議開啟！\",\n  \"setting__play_lyric_roma\": \"顯示歌詞羅馬音（如果可用）\",\n  \"setting__play_lyric_s2t\": \"將播放與下載的中文歌詞轉換為繁體\",\n  \"setting__play_lyric_transition\": \"顯示歌詞翻譯（如果可用）\",\n  \"setting__play_max_output_channel_count\": \"使用裝置能處理的最大聲道數輸出音訊\",\n  \"setting__play_mediaDevice\": \"音訊輸出\",\n  \"setting__play_mediaDevice_remove_stop_play\": \"目前的音訊輸出裝置被改變時暫停播放歌曲\",\n  \"setting__play_mediaDevice_title\": \"選取音訊輸出的媒體裝置\",\n  \"setting__play_media_device_error_tip\": \"此功能與進階音訊功能（音訊可視化、音效設定、使用裝置能處理的最大聲道數輸出音訊功能）衝突。你本次啟動軟體時已啟用這些功能，此選項暫不可用。請關閉這些功能並重啟軟體後，再修改此選項！\",\n  \"setting__play_media_device_tip\": \"此功能與音訊可視化功能衝突，兩者無法同時啟用。是否關閉「音訊可視化」並應用所選音訊輸出設定？\",\n  \"setting__play_playQuality\": \"優先播放的音質（如果可用）\",\n  \"setting__play_power_save_blocker\": \"播放歌曲時阻止電腦休眠\",\n  \"setting__play_save_play_time\": \"記住播放進度\",\n  \"setting__play_startup_auto_play\": \"啟動軟體後自動播放音樂\",\n  \"setting__play_statusbar_lyric\": \"在選單列的「狀態」選單顯示歌詞\",\n  \"setting__play_statusbar_lyric_tip\": \"需要在「設定 → 基本設定」啟用「關閉視窗時不退出軟體將其最小化到系統匣」。\",\n  \"setting__play_task_bar\": \"在工具列上顯示目前歌曲播放進度\",\n  \"setting__play_timeout\": \"定時暫停\",\n  \"setting__player_audio_visualization_tip\": \"自訂音訊輸出裝置與音訊可視化功能衝突，啟用「音訊可視化」後音訊輸出裝置將會被重設為預設，目前此問題暫無法解決。是否仍要開啟？\",\n  \"setting__player_swap_lyric_trans_roma\": \"調換歌詞翻譯與歌詞羅馬音位置\",\n  \"setting__search\": \"搜尋設定\",\n  \"setting__search_focus_search_box\": \"啟動時自動聚焦搜尋框\",\n  \"setting__search_history\": \"顯示歷史搜尋記錄\",\n  \"setting__search_hot\": \"顯示熱門搜尋\",\n  \"setting__sync\": \"資料同步\",\n  \"setting__sync_client_address\": \"目前裝置位址：{address}\",\n  \"setting__sync_client_host\": \"同步服務位址\",\n  \"setting__sync_client_host_tip\": \"http://<IP 位址>:<埠號>\",\n  \"setting__sync_client_mode\": \"用戶端模式\",\n  \"setting__sync_client_status\": \"狀態：{status}\",\n  \"setting__sync_code_blocked_ip\": \"目前裝置的 IP 已被服務端封禁！\",\n  \"setting__sync_code_fail\": \"連線碼無效\",\n  \"setting__sync_enable\": \"啟用同步功能\",\n  \"setting__sync_mode\": \"同步模式\",\n  \"setting__sync_mode_client\": \"用戶端模式\",\n  \"setting__sync_mode_server\": \"服務端模式\",\n  \"setting__sync_server_address\": \"同步服務位址：{address}\",\n  \"setting__sync_server_auth_code\": \"連線碼：{code}\",\n  \"setting__sync_server_device\": \"已連線的裝置：{devices}\",\n  \"setting__sync_server_device_list_btn_remove\": \"移除\",\n  \"setting__sync_server_device_list_noitem\": \"這裡什麼也沒有 ┗( ▔, ▔ )┛\",\n  \"setting__sync_server_device_list_time\": \"最後連線時間：{time}\",\n  \"setting__sync_server_device_list_tips\": \"💡 裝置被移除後，再連線時需要重新輸入連線碼\",\n  \"setting__sync_server_device_list_title\": \"已認證裝置\",\n  \"setting__sync_server_mode\": \"服務端模式（由於資料是明文傳輸，請在受信任的網路下使用）\",\n  \"setting__sync_server_port\": \"同步埠設定\",\n  \"setting__sync_server_port_tip\": \"請輸入同步服務埠號\",\n  \"setting__sync_server_refresh_code\": \"重新獲取連線碼\",\n  \"setting__sync_server_show_device_list\": \"已認證裝置清單\",\n  \"setting__sync_tip\": \"使用方式請看常見問題「同步功能」部分\",\n  \"setting__update\": \"軟體更新\",\n  \"setting__update_checking\": \"檢查更新中...\",\n  \"setting__update_commit_date\": \"提交日期：\",\n  \"setting__update_commit_id\": \"程式碼版本：\",\n  \"setting__update_current_label\": \"目前版本：\",\n  \"setting__update_downloading\": \"發現新版本並在努力下載中，請稍後...⏳\",\n  \"setting__update_init\": \"處理更新中...\",\n  \"setting__update_latest\": \"軟體已是最新，盡情地體驗吧~🥂\",\n  \"setting__update_latest_label\": \"最新版本：\",\n  \"setting__update_new_version\": \"發現新版本，趕快去更新吧~🚀🚀\",\n  \"setting__update_open_version_modal_btn\": \"開啟更新視窗\",\n  \"setting__update_progress\": \"狀態：\",\n  \"setting__update_show_change_log\": \"更新版本後的首次啟動時顯示更新日誌\",\n  \"setting__update_try_auto_update\": \"發現新版本時嘗試自動下載更新\",\n  \"setting__update_unknown\": \"未知\",\n  \"setting__update_unknown_tip\": \"❓ 獲取最新版本資訊失敗，建議在「關於」頁面開啟項目發布位址查看目前版本是否最新\",\n  \"setting_download_save_group_list_name\": \"將檔案儲存到以對應清單命名的子目錄中\",\n  \"setting_sync_status_enabled\": \"已連線\",\n  \"song_list\": \"歌單\",\n  \"songlist__import_input_btn_confirm\": \"開啟\",\n  \"songlist__import_input_show_btn\": \"開啟歌單\",\n  \"songlist__import_input_tip\": \"輸入歌單連結或歌單 ID\",\n  \"songlist__import_input_tip_1\": \"不支援跨平台開啟歌單，請確認要開啟的歌單與目前歌單的來源平台是否對應\",\n  \"songlist__import_input_tip_2\": \"若遇到無法開啟的歌單連結，歡迎回報\",\n  \"songlist__import_input_tip_3\": \"酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟，但支援用普通版連結與酷狗碼開啟\",\n  \"songlist__import_input_tip_4\": \"網易雲音樂的「我喜歡」歌單需要 Token 才能開啟，詳情看 \",\n  \"songlist__import_input_title\": \"開啟分享的歌單\",\n  \"songlist__open_list\": \"開啟「{name}」歌單\",\n  \"songlist__tag_info_hot_tag\": \"熱門標籤\",\n  \"source_alias_all\": \"聚合大會\",\n  \"source_alias_bd\": \"小杜音樂\",\n  \"source_alias_kg\": \"小枸音樂\",\n  \"source_alias_kw\": \"小蝸音樂\",\n  \"source_alias_mg\": \"小蜜音樂\",\n  \"source_alias_tx\": \"小秋音樂\",\n  \"source_alias_wy\": \"小芸音樂\",\n  \"source_alias_xm\": \"小霞音樂\",\n  \"source_all\": \"聚合搜尋\",\n  \"source_bd\": \"百度音樂\",\n  \"source_kg\": \"酷狗音樂\",\n  \"source_kw\": \"酷我音樂\",\n  \"source_mg\": \"咪咕音樂\",\n  \"source_tx\": \"企鵝音樂\",\n  \"source_wy\": \"網易音樂\",\n  \"source_xm\": \"蝦米音樂\",\n  \"sync__auth_code_input_tip\": \"請輸入連線碼\",\n  \"sync__auth_code_title\": \"需要輸入連線碼\",\n  \"sync__dislike_merge_tip_desc\": \"合併兩邊清單內容並去重\",\n  \"sync__dislike_other_tip_desc\": \"「取消同步」將不使用「不喜歡的歌曲」清單同步功能\",\n  \"sync__dislike_overwrite_tip_desc\": \"被覆寫者的清單將被取代成覆寫者的清單\",\n  \"sync__dislike_title\": \"選取與「{name}」的「不喜歡的歌曲」清單同步方式\",\n  \"sync__list_merge_tip_desc\": \"將兩邊的清單合併到一起，相同的歌曲將被去掉（去掉的是被合併者的歌曲），不同的歌曲將被添加。\",\n  \"sync__list_other_tip_desc\": \"「取消同步」將不使用清單同步功能。\",\n  \"sync__list_overwrite_tip_desc\": \"被覆寫者與覆寫者清單 ID 相同的清單將被移除後取代成覆寫者的清單（清單 ID 不同的清單將被合併到一起）。若勾選「完全覆寫」，則被覆寫者的所有清單將被移除，然後取代成覆寫者的清單。\",\n  \"sync__list_title\": \"選取與「{name}」的清單同步方式\",\n  \"sync__merge_btn_local_remote\": \"「本機清單」合併「遠端清單」\",\n  \"sync__merge_btn_remote_local\": \"「遠端清單」合併「本機清單」\",\n  \"sync__merge_label\": \"合併\",\n  \"sync__merge_tip\": \"合併：\",\n  \"sync__other_label\": \"其他\",\n  \"sync__other_tip\": \"其他：\",\n  \"sync__overwrite\": \"完全覆寫\",\n  \"sync__overwrite_btn_cancel\": \"取消同步\",\n  \"sync__overwrite_btn_local_remote\": \"「本機清單」覆寫「遠端清單」\",\n  \"sync__overwrite_btn_none\": \"僅使用即時同步功能\",\n  \"sync__overwrite_btn_remote_local\": \"「遠端清單」覆寫「本機清單」\",\n  \"sync__overwrite_label\": \"覆寫\",\n  \"sync__overwrite_tip\": \"覆寫:\",\n  \"sync_status_disabled\": \"未連線\",\n  \"tag__high_quality\": \"HQ\",\n  \"tag__lossless\": \"SQ\",\n  \"tag__lossless_24bit\": \"24bit\",\n  \"theme_add\": \"加入主題\",\n  \"theme_auto\": \"道法自然\",\n  \"theme_auto_tip\": \"滑鼠右擊可開啟亮、暗主題設定視窗\",\n  \"theme_black\": \"黑燈瞎火\",\n  \"theme_blue\": \"藍田生玉\",\n  \"theme_blue2\": \"清熱版藍\",\n  \"theme_blue_plus\": \"蛋雅深藍\",\n  \"theme_china_ink\": \"近墨者黑\",\n  \"theme_edit_modal__app_bg\": \"應用背景顏色\",\n  \"theme_edit_modal__aside_color\": \"側欄按鈕顏色\",\n  \"theme_edit_modal__badge\": \"標籤顏色\",\n  \"theme_edit_modal__badge_primary\": \"主顏色\",\n  \"theme_edit_modal__badge_secondary\": \"次要顏色\",\n  \"theme_edit_modal__badge_tertiary\": \"第三顏色\",\n  \"theme_edit_modal__bg_image\": \"背景圖片\",\n  \"theme_edit_modal__bg_image_add\": \"背景圖片：\",\n  \"theme_edit_modal__bg_image_change\": \"變更背景圖片\",\n  \"theme_edit_modal__bg_image_remove\": \"移除背景圖片\",\n  \"theme_edit_modal__close_btn\": \"關閉\",\n  \"theme_edit_modal__control_btn\": \"左側控制按鈕顏色\",\n  \"theme_edit_modal__copy\": \"複製主題\",\n  \"theme_edit_modal__dark\": \"暗色主題\",\n  \"theme_edit_modal__dark_font\": \"深色字體\",\n  \"theme_edit_modal__font\": \"字體顏色\",\n  \"theme_edit_modal__hide_btn\": \"隱藏播放詳情頁\",\n  \"theme_edit_modal__main_bg\": \"內容區域背景顏色\",\n  \"theme_edit_modal__min_btn\": \"最小化\",\n  \"theme_edit_modal__pick_cancel\": \"重設\",\n  \"theme_edit_modal__pick_color\": \"選取顏色\",\n  \"theme_edit_modal__pick_last_color\": \"使用之前的顏色\",\n  \"theme_edit_modal__pick_save\": \"確認\",\n  \"theme_edit_modal__preview\": \"預覽主題\",\n  \"theme_edit_modal__primary\": \"主題色\",\n  \"theme_edit_modal__remove\": \"移除\",\n  \"theme_edit_modal__remove_tip\": \"是否真的要移除這個主題？\",\n  \"theme_edit_modal__save_new\": \"另存\",\n  \"theme_edit_modal__select_bg_file\": \"選取背景圖片\",\n  \"theme_edit_modal__title_add\": \"新增主題\",\n  \"theme_edit_modal__title_edit\": \"編輯主題\",\n  \"theme_green\": \"綠意盎然\",\n  \"theme_grey\": \"灰常美麗\",\n  \"theme_happy_new_year\": \"新年快樂\",\n  \"theme_max_tip\": \"最多只能加入 10 個主題哦，刪掉一些再加吧😜\",\n  \"theme_mid_autumn\": \"月裡嫦娥\",\n  \"theme_ming\": \"青出於黑\",\n  \"theme_more_btn_show\": \"更多主題\",\n  \"theme_naruto\": \"木葉之村\",\n  \"theme_orange\": \"橙黃橘綠\",\n  \"theme_pink\": \"粉裝玉琢\",\n  \"theme_purple\": \"重斤球紫\",\n  \"theme_red\": \"熱情似火\",\n  \"theme_selector_modal__dark_title\": \"暗色主題\",\n  \"theme_selector_modal__light_title\": \"亮色主題\",\n  \"theme_selector_modal__theme_name\": \"主題名稱\",\n  \"theme_selector_modal__title\": \"跟隨系統主題設定\",\n  \"theme_selector_modal__title_tip\": \"註：你可以預先設定一個亮色主題及暗色主題，此後將根據系統的亮、暗主題色自動切換為你預先設定的相應主題。\",\n  \"toggle_source_failed\": \"變更來源失敗，請嘗試手動在搜尋頁指定其他音樂串流平台搜尋該歌曲播放\",\n  \"toggle_source_try\": \"嘗試變更到其他來源...\",\n  \"update__downgrade_tip\": \"我們發現你降級了版本（{ver}），若使用新版本時遇到問題，請先嘗試閱讀常見問題解決，若你遇到的問題在常見問題中未記錄或無法解決，可以透過教學文件中提到的回報管道向我們回報😘！\\n\\n注意：從新版本降級舊版時建議先備份歌單，若出現異常則可透過清理資料解決，資料目錄路徑在教學文件有記錄。\",\n  \"update__error_top\": \"自動下載新版本失敗，你可以嘗試重新下載更新或者手動去下載更新。\\n\\n新版位址在更新彈出視窗下面有寫，下載新版直接覆寫安裝即可，若安裝失敗則看常見問題解決。\\n\\n注意：目前只有 Windows 安裝版可以自動更新（Linux 的 AppImage、deb 版似乎也可以，未測試），其他版本請手動下載更新！\",\n  \"update__ignore_cancel\": \"我就不想更新🤨\",\n  \"update__ignore_confirm\": \"好，去更新看看❤️\",\n  \"update__ignore_confirm_tip\": \"目前只有 Windows 安裝版可以自動更新（Linux 的 AppImage、deb 版似乎也可以，未測試），其他版本請手動下載更新，\\n新版位址在更新彈出視窗下面有寫，下載新版直接覆寫安裝即可，若安裝失敗看常見問題解決。\",\n  \"update__ignore_confirm_tip_confirm\": \"OK，已了解\",\n  \"update__ignore_tip\": \"你現在使用的版本距離最新版本已經落後了 {num} 個版本🤪，為了更好的使用體驗，建議更新到最新版本哦~！\\n\\n註：若使用新版本時遇到問題，請先嘗試閱讀常見問題解決，若你遇到的問題在常見問題中未記錄或無法解決，可以透過教學文件中提到的回報管道向我們回報😘！\",\n  \"update__timeout_top\": \"下載時間過長提示\\n\\n你目前所在網路訪問 GitHub 較慢，新版本已經下了一個鐘了還沒完成😳，你仍可以繼續等，但牆裂建議手動更新版本！\",\n  \"user_api__allow_show_update_alert\": \"允許顯示更新彈出視窗\",\n  \"user_api__btn_export\": \"匯出\",\n  \"user_api__btn_import\": \"從本機匯入\",\n  \"user_api__btn_import_online\": \"從線上匯入\",\n  \"user_api__btn_remove\": \"移除\",\n  \"user_api__import_file\": \"選取自訂來源 API 腳本檔案\",\n  \"user_api__init_failed_alert\": \"自訂來源 API「{name}」初始化失敗：\",\n  \"user_api__max_tip\": \"最多只能同時存在 20 個 API 哦🤪\\n想要繼續匯入的話，請先移除一些舊的 API 騰出位置吧\",\n  \"user_api__noitem\": \"這裡竟然是空的😲\",\n  \"user_api__note\": \"提示：雖然我們已經盡可能地隔離了 API 腳本的執行環境，但匯入包含惡意行為的 API 腳本仍可能會影響你的系統，請謹慎匯入。\",\n  \"user_api__readme\": \"API 編寫說明：\",\n  \"user_api__title\": \"自訂來源 API 管理\",\n  \"user_api__update_alert\": \"自訂來源 API「{name}」發現新版本：\",\n  \"user_api__update_alert_open_url\": \"開啟更新位址\",\n  \"user_api_import__failed\": \"自訂來源 API 匯入失敗：\\n{message}\",\n  \"user_api_import_online__input_confirm\": \"匯入\",\n  \"user_api_import_online__input_loading\": \"匯入中...\",\n  \"user_api_import_online__input_tip\": \"請輸入 HTTP 連結\",\n  \"user_api_import_online__title\": \"從線上匯入自訂來源 API\"\n}\n"
  },
  {
    "path": "src/main/.eslintrc.cjs",
    "content": "/* eslint-env node */\nconst { base, typescript } = require('../../.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n  ignorePatterns: [\n    'vendors',\n  ],\n}\n"
  },
  {
    "path": "src/main/app.ts",
    "content": "import path from 'node:path'\nimport { existsSync, mkdirSync, renameSync } from 'fs'\nimport { app, shell, screen, nativeTheme, dialog } from 'electron'\nimport { URL_SCHEME_RXP } from '@common/constants'\nimport { getProxy, getTheme, initHotKey, initSetting, parseEnvParams } from './utils'\nimport { navigationUrlWhiteList } from '@common/config'\nimport defaultSetting from '@common/defaultSetting'\nimport { isExistWindow as isExistMainWindow, showWindow as showMainWindow } from './modules/winMain'\nimport { createAppEvent, createDislikeEvent, createListEvent } from '@main/event'\nimport { isMac, log } from '@common/utils'\nimport createWorkers from './worker'\nimport { migrateDBData } from './utils/migrate'\nimport { openDirInExplorer } from '@common/utils/electron'\nimport { setProxyByHost } from '@common/utils/request'\n\nexport const initGlobalData = () => {\n  const envParams = parseEnvParams()\n  // envParams.cmdParams.dt = !!envParams.cmdParams.dt\n\n  global.envParams = {\n    cmdParams: envParams.cmdParams,\n    deeplink: envParams.deeplink,\n  }\n  global.lx = {\n    inited: false,\n    isSkipTrayQuit: false,\n    // mainWindowClosed: true,\n    event_app: createAppEvent(),\n    event_list: createListEvent(),\n    event_dislike: createDislikeEvent(),\n    appSetting: defaultSetting,\n    worker: createWorkers(),\n    hotKey: {\n      enable: true,\n      config: {\n        local: {\n          enable: false,\n          keys: {},\n        },\n        global: {\n          enable: false,\n          keys: {},\n        },\n      },\n      state: new Map(),\n    },\n    theme: {\n      shouldUseDarkColors: nativeTheme.shouldUseDarkColors,\n      theme: {\n        id: '',\n        name: '',\n        isDark: false,\n        colors: {},\n      },\n    },\n    player_status: {\n      status: 'stoped',\n      name: '',\n      singer: '',\n      albumName: '',\n      picUrl: '',\n      progress: 0,\n      duration: 0,\n      playbackRate: 1,\n      lyricLineText: '',\n      lyricLineAllText: '',\n      lyric: '',\n      tlyric: '',\n      rlyric: '',\n      lxlyric: '',\n      collect: false,\n      volume: 0,\n      mute: false,\n    },\n  }\n\n  global.staticPath =\n    process.env.NODE_ENV !== 'production'\n      ? webpackStaticPath\n      : path.join(__dirname, 'static')\n}\n\nexport const initSingleInstanceHandle = () => {\n  // 单例应用程序\n  if (!app.requestSingleInstanceLock()) {\n    app.quit()\n    process.exit(0)\n  }\n\n  app.on('second-instance', (event, argv, cwd) => {\n    if (isExistMainWindow()) {\n      const envParams = parseEnvParams(argv)\n      if (envParams.deeplink) {\n        global.envParams.deeplink = envParams.deeplink\n        global.lx.event_app.deeplink(global.envParams.deeplink)\n        return\n      }\n      if (envParams.cmdParams.hidden !== true) {\n        showMainWindow()\n      }\n    } else {\n      app.quit()\n    }\n  })\n}\n\nexport const applyElectronEnvParams = () => {\n  // Is disable hardware acceleration\n  if (global.envParams.cmdParams.dha) app.disableHardwareAcceleration()\n  if (global.envParams.cmdParams.dhmkh) app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling')\n\n  // fix linux transparent fail. https://github.com/electron/electron/issues/25153#issuecomment-843688494\n  if (process.platform == 'linux') app.commandLine.appendSwitch('use-gl', 'desktop')\n\n  // https://github.com/electron/electron/issues/22691\n  app.commandLine.appendSwitch('wm-window-animations-disabled')\n\n  app.commandLine.appendSwitch('--disable-gpu-sandbox')\n\n  // proxy\n  if (global.envParams.cmdParams['proxy-server']) {\n    app.commandLine.appendSwitch('proxy-server', global.envParams.cmdParams['proxy-server'])\n    app.commandLine.appendSwitch('proxy-bypass-list', global.envParams.cmdParams['proxy-bypass-list'] ?? '<local>')\n  }\n}\n\nexport const setUserDataPath = () => {\n  // windows平台下如果应用目录下存在 portable 文件夹则将数据存在此文件下\n  if (process.platform == 'win32') {\n    const portablePath = path.join(path.dirname(app.getPath('exe')), '/portable')\n    if (existsSync(portablePath)) {\n      app.setPath('appData', portablePath)\n      const appDataPath = path.join(portablePath, '/userData')\n      if (!existsSync(appDataPath)) mkdirSync(appDataPath)\n      app.setPath('userData', appDataPath)\n    }\n  }\n\n  const userDataPath = app.getPath('userData')\n  global.lxOldDataPath = userDataPath\n  global.lxDataPath = path.join(userDataPath, 'LxDatas')\n  if (!existsSync(global.lxDataPath)) mkdirSync(global.lxDataPath)\n}\n\nexport const registerDeeplink = (startApp: () => void) => {\n  if (process.env.NODE_ENV !== 'production' && process.platform === 'win32') {\n    // Set the path of electron.exe and your app.\n    // These two additional parameters are only available on windows.\n    // console.log(process.execPath, process.argv)\n    app.setAsDefaultProtocolClient('lxmusic', process.execPath, process.argv.slice(1))\n  } else {\n    app.setAsDefaultProtocolClient('lxmusic')\n  }\n\n  // deep link\n  app.on('open-url', (event, url) => {\n    if (!URL_SCHEME_RXP.test(url)) return\n    event.preventDefault()\n    global.envParams.deeplink = url\n    if (isExistMainWindow()) {\n      if (global.envParams.deeplink) global.lx.event_app.deeplink(global.envParams.deeplink)\n      else showMainWindow()\n    } else {\n      startApp()\n    }\n  })\n}\n\nexport const listenerAppEvent = (startApp: () => void) => {\n  app.on('web-contents-created', (event, contents) => {\n    contents.on('will-navigate', (event, navigationUrl) => {\n      if (process.env.NODE_ENV !== 'production') {\n        console.log('navigation to url:', navigationUrl.length > 130 ? navigationUrl.substring(0, 130) + '...' : navigationUrl)\n        return\n      }\n      if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) {\n        event.preventDefault()\n        return\n      }\n      console.log('navigation to url:', navigationUrl)\n    })\n    contents.setWindowOpenHandler(({ url }) => {\n      if (!/^devtools/.test(url) && /^https?:\\/\\//.test(url)) {\n        void shell.openExternal(url)\n      }\n      console.log(url)\n      return { action: 'deny' }\n    })\n    contents.on('will-attach-webview', (event, webPreferences, params) => {\n      // Strip away preload scripts if unused or verify their location is legitimate\n      delete webPreferences.preload\n      // delete webPreferences.preloadURL\n\n      // Disable Node.js integration\n      webPreferences.nodeIntegration = false\n\n      // Verify URL being loaded\n      if (!navigationUrlWhiteList.some(url => url.test(params.src))) {\n        event.preventDefault()\n      }\n    })\n\n    // disable create dictionary\n    // https://github.com/lyswhut/lx-music-desktop/issues/773\n    contents.session.setSpellCheckerDictionaryDownloadURL('http://0.0.0.0')\n  })\n\n  app.on('activate', () => {\n    if (isExistMainWindow()) {\n      showMainWindow()\n    } else {\n      startApp()\n    }\n  })\n\n  app.on('before-quit', () => {\n    global.lx.isSkipTrayQuit = true\n  })\n  app.on('window-all-closed', () => {\n    if (isMac) return\n\n    app.quit()\n  })\n\n  const initScreenParams = () => {\n    global.envParams.workAreaSize = screen.getPrimaryDisplay().workAreaSize\n  }\n  app.on('ready', () => {\n    screen.on('display-metrics-changed', initScreenParams)\n    initScreenParams()\n  })\n\n  nativeTheme.addListener('updated', () => {\n    const shouldUseDarkColors = nativeTheme.shouldUseDarkColors\n    if (shouldUseDarkColors == global.lx.theme.shouldUseDarkColors) return\n    global.lx.theme.shouldUseDarkColors = shouldUseDarkColors\n    global.lx?.event_app.system_theme_change(shouldUseDarkColors)\n  })\n\n  const setProxy = () => {\n    const proxy = getProxy()\n    if (proxy) {\n      setProxyByHost(proxy.host, proxy.port ? String(proxy.port) : undefined)\n    } else setProxyByHost()\n  }\n  global.lx.event_app.on('updated_config', (keys, setting) => {\n    if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) {\n      setProxy()\n    }\n\n    if (keys.includes('player.volume')) {\n      global.lx.event_app.player_status({ volume: Math.trunc(setting['player.volume']! * 100) })\n    }\n    if (keys.includes('player.isMute')) {\n      global.lx.event_app.player_status({ mute: setting['player.isMute'] })\n    }\n  })\n  global.lx.event_app.on('app_inited', () => {\n    setProxy()\n  })\n}\n\nconst initTheme = () => {\n  global.lx.theme = getTheme()\n  const themeConfigKeys = ['theme.id', 'theme.lightId', 'theme.darkId']\n  global.lx.event_app.on('updated_config', (keys) => {\n    let requireUpdate = false\n    for (const key of keys) {\n      if (themeConfigKeys.includes(key)) {\n        requireUpdate = true\n        break\n      }\n    }\n    if (requireUpdate) {\n      global.lx.theme = getTheme()\n      global.lx.event_app.theme_change()\n    }\n  })\n  global.lx.event_app.on('system_theme_change', () => {\n    if (global.lx.appSetting['theme.id'] == 'auto') {\n      global.lx.theme = getTheme()\n      global.lx.event_app.theme_change()\n    }\n  })\n}\n\nconst backupDB = (backupPath: string) => {\n  const dbPath = path.join(global.lxDataPath, 'lx.data.db')\n  try {\n    renameSync(dbPath, backupPath)\n  } catch {}\n  try {\n    renameSync(`${dbPath}-wal`, `${backupPath}-wal`)\n  } catch {}\n  try {\n    renameSync(`${dbPath}-shm`, `${backupPath}-shm`)\n  } catch {}\n  openDirInExplorer(backupPath)\n}\n\nlet isInitialized = false\nexport const initAppSetting = async() => {\n  if (!global.lx.inited) {\n    const config = await initHotKey()\n    global.lx.hotKey.config.local = config.local\n    global.lx.hotKey.config.global = config.global\n    global.lx.inited = true\n  }\n\n  if (!isInitialized) {\n    let dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)\n    if (dbFileExists === null) {\n      const backupPath = path.join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`)\n      dialog.showMessageBoxSync({\n        type: 'warning',\n        message: 'Database verify failed',\n        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.`,\n      })\n      backupDB(backupPath)\n      dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)\n    }\n    global.lx.appSetting = (await initSetting()).setting\n    if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) })\n    initTheme()\n    if (envParams.cmdParams.dt == null) envParams.cmdParams.dt = !global.lx.appSetting['common.transparentWindow']\n  }\n  // global.lx.theme = getTheme()\n\n  isInitialized ||= true\n}\n\nexport const quitApp = () => {\n  global.lx.isSkipTrayQuit = true\n  app.quit()\n}\n"
  },
  {
    "path": "src/main/event/AppEvent.ts",
    "content": "import { EventEmitter } from 'events'\n\nimport { saveAppHotKeyConfig, updateSetting } from '@main/utils'\nimport type { BrowserWindow } from 'electron'\n\nexport class Event extends EventEmitter {\n  // closeAll() {\n  //   this.emit(COMMON_EVENT_NAME.closeAll)\n  // }\n  // initSetting() {\n  //   this.emit(COMMON_EVENT_NAME.initConfig)\n  //   // this.configStatus(null)\n  // }\n\n  /**\n   * 初始化APP\n   */\n  app_inited() {\n    this.emit('app_inited')\n  }\n\n  /**\n   * 已更新的配置\n   * @param keys 已更新配置的key\n   * @param setting 已更新配置\n   */\n  updated_config(keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) {\n    this.emit('updated_config', keys, setting)\n  }\n\n  /**\n   * 更新配置\n   * @param setting 新设置\n   */\n  update_config(setting: Partial<LX.AppSetting>) {\n    const { setting: newSetting, updatedSettingKeys, updatedSetting } = updateSetting(setting)\n    global.lx.appSetting = newSetting\n    if (!updatedSettingKeys.length) return\n    this.emit('update_config', newSetting)\n    // console.log(updatedSetting)\n    this.updated_config(updatedSettingKeys, updatedSetting)\n  }\n\n  system_theme_change(isDark: boolean) {\n    this.emit('system_theme_change', isDark)\n  }\n\n  theme_change() {\n    this.emit('theme_change')\n  }\n\n  deeplink(link: string) {\n    this.emit('deeplink', link)\n  }\n\n  player_status(status: Partial<LX.Player.Status>) {\n    for (const [key, value] of Object.entries(status)) {\n      // @ts-expect-error\n      global.lx.player_status[key] = value\n    }\n    this.emit('player_status', status)\n  }\n\n  hot_key_down(keyInfo: LX.HotKeyDownInfo) {\n    this.emit('hot_key_down', keyInfo)\n  }\n\n  hot_key_config_update(config: LX.HotKeyConfigAll) {\n    saveAppHotKeyConfig(config)\n    this.emit('hot_key_config_update', config)\n  }\n\n  main_window_created(win: BrowserWindow) {\n    this.emit('main_window_created', win)\n  }\n\n  main_window_ready_to_show() {\n    this.emit('main_window_ready_to_show')\n  }\n\n  main_window_inited() {\n    this.emit('main_window_inited')\n  }\n\n  main_window_show() {\n    this.emit('main_window_show')\n  }\n\n  main_window_hide() {\n    this.emit('main_window_hide')\n  }\n\n  main_window_focus() {\n    this.emit('main_window_focus')\n  }\n\n  main_window_blur() {\n    this.emit('main_window_blur')\n  }\n\n  main_window_close() {\n    this.emit('main_window_close')\n  }\n\n  main_window_fullscreen(isFullscreen: boolean) {\n    this.emit('main_window_fullscreen', isFullscreen)\n  }\n\n  desktop_lyric_window_created(win: BrowserWindow) {\n    this.emit('desktop_lyric_window_created', win)\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof EventEmitter>\ndeclare class EventType extends Event {\n  on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n}\n\nexport type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>\n"
  },
  {
    "path": "src/main/event/DislikeEvent.ts",
    "content": "import { EventEmitter } from 'events'\n\n\nexport class Event extends EventEmitter {\n  dislike_changed() {\n    this.emit('dislike_changed')\n  }\n\n  /**\n   * 覆盖整个列表数据\n   * @param dislikeData 列表数据\n   * @param isRemote 是否属于远程操作\n   */\n  async dislike_data_overwrite(dislikeData: LX.Dislike.DislikeRules, isRemote: boolean = false) {\n    await global.lx.worker.dbService.dislikeInfoOverwrite(dislikeData)\n    this.emit('dislike_data_overwrite', dislikeData, isRemote)\n    this.dislike_changed()\n  }\n\n  /**\n   * 批量添加歌曲到列表\n   * @param dislikeId 列表id\n   * @param musicInfos 添加的歌曲信息\n   * @param addMusicLocationType 添加在到列表的位置\n   * @param isRemote 是否属于远程操作\n   */\n  async dislike_music_add(musicInfo: LX.Dislike.DislikeMusicInfo[], isRemote: boolean = false) {\n    // const changedIds =\n    await global.lx.worker.dbService.dislikeInfoAdd(musicInfo)\n    // await checkUpdateDislike(changedIds)\n    this.emit('dislike_music_add', musicInfo, isRemote)\n    this.dislike_changed()\n  }\n\n  /**\n   * 清空列表内的歌曲\n   * @param ids 列表Id\n   * @param isRemote 是否属于远程操作\n   */\n  async dislike_music_clear(isRemote: boolean = false) {\n    // const changedIds =\n    await global.lx.worker.dbService.dislikeInfoOverwrite('')\n    // await checkUpdateDislike(changedIds)\n    this.emit('dislike_music_clear', isRemote)\n    this.dislike_changed()\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof EventEmitter>\ndeclare class EventType extends Event {\n  on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n}\nexport type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>\n"
  },
  {
    "path": "src/main/event/ListEvent.ts",
    "content": "import { EventEmitter } from 'events'\n// import {\n//   // getAllUserList as getAllUserListByDB,\n//   createUserLists,\n//   removeUserLists,\n//   updateUserLists,\n//   updateUserListsPosition,\n//   musicsAdd,\n//   musicsMove,\n//   musicsRemove,\n//   musicsUpdate,\n//   musicsClear,\n//   musicsPositionUpdate,\n//   musicOverwrite,\n// } from '@main/workers/dbService/modules/list'\n\n// 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题，确保所有ID都是字符串类型\nconst fixListIdType = (lists: LX.List.UserListInfo[] | LX.List.UserListInfoFull[]) => {\n  for (const list of lists) {\n    if (typeof list.sourceListId == 'number') {\n      list.sourceListId = String(list.sourceListId)\n      if (typeof list.id == 'number') {\n        list.id = String(list.id)\n      }\n    }\n  }\n}\n\nexport class Event extends EventEmitter {\n  list_changed() {\n    this.emit('list_changed')\n  }\n\n  /**\n   * 覆盖整个列表数据\n   * @param listData 列表数据\n   * @param isRemote 是否属于远程操作\n   */\n  async list_data_overwrite(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) {\n    fixListIdType(listData.userList)\n    await global.lx.worker.dbService.listDataOverwrite(listData)\n    this.emit('list_data_overwrite', listData, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量创建列表\n   * @param position 列表位置\n   * @param lists 列表信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_create(position: number, lists: LX.List.UserListInfo[], isRemote: boolean = false) {\n    fixListIdType(lists)\n    await global.lx.worker.dbService.createUserLists(position, lists)\n    this.emit('list_create', position, lists, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量删除列表及列表内歌曲\n   * @param ids 列表ids\n   * @param isRemote 是否属于远程操作\n   */\n  async list_remove(ids: string[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.removeUserLists(ids)\n    this.emit('list_remove', ids, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量更新列表信息\n   * @param lists 列表信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_update(lists: LX.List.UserListInfo[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.updateUserLists(lists)\n    this.emit('list_update', lists, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量更新列表位置\n   * @param position 列表位置\n   * @param ids 列表ids\n   * @param isRemote 是否属于远程操作\n   */\n  async list_update_position(position: number, ids: string[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.updateUserListsPosition(position, ids)\n    this.emit('list_update_position', position, ids, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 覆盖列表内歌曲\n   * @param listId 列表id\n   * @param musicInfos 音乐信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_overwrite(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicOverwrite(listId, musicInfos)\n    this.emit('list_music_overwrite', listId, musicInfos, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量添加歌曲到列表\n   * @param listId 列表id\n   * @param musicInfos 添加的歌曲信息\n   * @param addMusicLocationType 添加在到列表的位置\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_add(listId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsAdd(listId, musicInfos, addMusicLocationType)\n    this.emit('list_music_add', listId, musicInfos, addMusicLocationType, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量移动歌曲\n   * @param fromId 源列表id\n   * @param toId 目标列表id\n   * @param musicInfos 移动的歌曲信息\n   * @param addMusicLocationType 添加在到列表的位置\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_move(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsMove(fromId, toId, musicInfos, addMusicLocationType)\n    this.emit('list_music_move', fromId, toId, musicInfos, addMusicLocationType, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量移除歌曲\n   * @param listId\n   * @param listId 列表Id\n   * @param ids 要删除歌曲的id\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_remove(listId: string, ids: string[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsRemove(listId, ids)\n    this.emit('list_music_remove', listId, ids, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量更新歌曲信息\n   * @param musicInfos 歌曲&列表信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_update(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsUpdate(musicInfos)\n    this.emit('list_music_update', musicInfos, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 清空列表内的歌曲\n   * @param ids 列表Id\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_clear(ids: string[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsClear(ids)\n    this.emit('list_music_clear', ids, isRemote)\n    this.list_changed()\n  }\n\n  /**\n   * 批量更新歌曲位置\n   * @param listId 列表ID\n   * @param position 新位置\n   * @param ids 歌曲id\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_update_position(listId: string, position: number, ids: string[], isRemote: boolean = false) {\n    await global.lx.worker.dbService.musicsPositionUpdate(listId, position, ids)\n    this.emit('list_music_update_position', listId, position, ids, isRemote)\n    this.list_changed()\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof EventEmitter>\ndeclare class EventType extends Event {\n  on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  once<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n  off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): this\n}\nexport type Type = Omit<EventType, keyof Omit<EventEmitter, 'on' | 'off' | 'once'>>\n"
  },
  {
    "path": "src/main/event/index.ts",
    "content": "import { Event as App, type Type as AppType } from './AppEvent'\nimport { Event as List, type Type as ListType } from './ListEvent'\nimport { Event as Dislike, type Type as DislikeType } from './DislikeEvent'\n\nexport type {\n  AppType,\n  ListType,\n  DislikeType,\n}\n\nexport const createAppEvent = (): AppType => {\n  return new App()\n}\n\nexport const createListEvent = (): ListType => {\n  return new List()\n}\n\nexport const createDislikeEvent = (): DislikeType => {\n  return new Dislike()\n}\n\n"
  },
  {
    "path": "src/main/index-dev.ts",
    "content": "/**\n * This file is used specifically and only for development. It installs\n * `electron-debug` & `vue-devtools`. There shouldn't be any need to\n *  modify this file, but it can be used to extend your development\n *  environment.\n */\n\nimport { app } from 'electron'\nimport electronDebug from 'electron-debug'\nimport installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'\nimport { openDevTools } from './utils'\n// Install `electron-debug` with `devtron`\nelectronDebug({\n  showDevTools: false,\n  devToolsMode: 'undocked',\n})\n\n// Install `vue-devtools`\napp.on('ready', () => {\n  global.lx.event_app.on('main_window_created', (win) => {\n    openDevTools(win.webContents)\n    installExtension(VUEJS_DEVTOOLS, { session: win.webContents.session })\n      .then((name: string) => {\n        console.log(`[main window] Added Extension:  ${name}`)\n      })\n      .catch((err: Error) => {\n        console.log('[main window] An error occurred: ', err)\n      })\n  })\n  global.lx.event_app.on('desktop_lyric_window_created', (win) => {\n    openDevTools(win.webContents)\n    installExtension(VUEJS_DEVTOOLS, { session: win.webContents.session })\n      .then((name: string) => {\n        console.log(`[lyric window] Added Extension:  ${name}`)\n      })\n      .catch((err: Error) => {\n        console.log('[lyric window] An error occurred: ', err)\n      })\n  })\n})\n\n// Require `main` process to boot app\nrequire('./index')\n\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "import { app } from 'electron'\nimport './utils/logInit'\nimport '@common/error'\nimport {\n  initGlobalData,\n  initSingleInstanceHandle,\n  applyElectronEnvParams,\n  setUserDataPath,\n  registerDeeplink,\n  listenerAppEvent,\n} from './app'\nimport { isLinux } from '@common/utils'\nimport { initAppSetting } from '@main/app'\nimport registerModules from '@main/modules'\n\n// 初始化应用\nconst init = () => {\n  console.log('init')\n  void initAppSetting().then(() => {\n    registerModules()\n    global.lx.event_app.app_inited()\n  })\n}\n\ninitGlobalData()\ninitSingleInstanceHandle()\napplyElectronEnvParams()\nsetUserDataPath()\nregisterDeeplink(init)\nlistenerAppEvent(init)\n\n\n// https://github.com/electron/electron/issues/16809\nvoid app.whenReady().then(() => {\n  isLinux ? setTimeout(init, 300) : init()\n})\n"
  },
  {
    "path": "src/main/modules/appMenu.ts",
    "content": "import { app, Menu } from 'electron'\nimport { isMac } from '@common/utils'\n\n\nexport default () => {\n  if (isMac) {\n    const template: Electron.MenuItemConstructorOptions[] = [\n      {\n        label: app.getName(),\n        submenu: [\n          { label: '关于洛雪音乐', role: 'about' },\n          { type: 'separator' },\n          { label: '隐藏', role: 'hide' },\n          { type: 'separator' },\n          {\n            label: '退出',\n            accelerator: 'Command+Q',\n            click() {\n              global.lx.isSkipTrayQuit = true\n              app.quit()\n            },\n          },\n        ],\n      },\n      {\n        label: '窗口',\n        role: 'window',\n        submenu: [\n          { label: '最小化', role: 'minimize' },\n          { label: '关闭', role: 'close', accelerator: 'Command+W' },\n        ],\n      },\n      {\n        label: '编辑',\n        submenu: [\n          { label: '撤销', accelerator: 'Command+Z', role: 'undo' },\n          { label: '恢复', accelerator: 'Shift+Command+Z', role: 'redo' },\n          { type: 'separator' },\n          { label: '剪切', accelerator: 'Command+X', role: 'cut' },\n          { label: '复制', accelerator: 'Command+C', role: 'copy' },\n          { label: '粘贴', accelerator: 'Command+V', role: 'paste' },\n          { label: '选择全部', accelerator: 'Command+A', role: 'selectAll' },\n        ],\n      },\n    ]\n\n    Menu.setApplicationMenu(Menu.buildFromTemplate(template))\n  } else {\n    Menu.setApplicationMenu(null)\n  }\n}\n"
  },
  {
    "path": "src/main/modules/commonRenderers/common/index.ts",
    "content": "export { registerRendererEvents } from './winRendererEvent'\n\nexport { default } from './rendererEvent'\n"
  },
  {
    "path": "src/main/modules/commonRenderers/common/rendererEvent.ts",
    "content": "import { mainHandle, mainOn } from '@common/mainIpc'\nimport { CMMON_EVENT_NAME } from '@common/ipcNames'\nimport { getFonts } from '@main/utils/fontManage'\n\n// 公共操作事件（公共，只注册一次）\nexport default () => {\n  mainHandle<LX.AppSetting>(CMMON_EVENT_NAME.get_app_setting, async() => {\n    return global.lx.appSetting\n  })\n  mainHandle<Partial<LX.AppSetting>>(CMMON_EVENT_NAME.set_app_setting, async({ params: config }) => {\n    global.lx.event_app.update_config(config)\n  })\n\n  mainHandle<LX.EnvParams>(CMMON_EVENT_NAME.get_env_params, async() => {\n    return global.envParams\n  })\n\n  mainOn(CMMON_EVENT_NAME.clear_env_params_deeplink, () => {\n    global.envParams.deeplink = null\n  })\n\n  mainHandle<string[]>(CMMON_EVENT_NAME.get_system_fonts, async() => {\n    return getFonts()\n  })\n}\n\n"
  },
  {
    "path": "src/main/modules/commonRenderers/common/winRendererEvent.ts",
    "content": "import { CMMON_EVENT_NAME } from '@common/ipcNames'\n\n// 发送列表操作事件到渲染进程的注册方法\n// 哪个渲染进程需要接收则引入此方法注册\nexport const registerRendererEvents = (sendEvent: <T = any>(name: string, params?: T | undefined) => void) => {\n  const sendDeeplink = (link: string) => {\n    sendEvent(CMMON_EVENT_NAME.deeplink, link)\n  }\n  const sendSystemThemeChange = () => {\n    sendEvent(CMMON_EVENT_NAME.theme_change, global.lx.theme)\n  }\n\n  global.lx.event_app.on('deeplink', sendDeeplink)\n  global.lx.event_app.on('theme_change', sendSystemThemeChange)\n\n  return () => {\n    global.lx.event_app.off('deeplink', sendDeeplink)\n    global.lx.event_app.off('theme_change', sendSystemThemeChange)\n  }\n}\n\n"
  },
  {
    "path": "src/main/modules/commonRenderers/dislike/index.ts",
    "content": "export { registerRendererEvents } from './winRendererEvent'\n\nexport { default } from './rendererEvent'\n"
  },
  {
    "path": "src/main/modules/commonRenderers/dislike/rendererEvent.ts",
    "content": "import { mainHandle } from '@common/mainIpc'\nimport { DISLIKE_EVENT_NAME } from '@common/ipcNames'\n\n// 列表操作事件（公共，只注册一次）\nexport default () => {\n  mainHandle<LX.Dislike.DislikeInfo>(DISLIKE_EVENT_NAME.get_dislike_music_infos, async() => {\n    return global.lx.worker.dbService.getDislikeListInfo()\n  })\n  mainHandle<LX.Dislike.DislikeMusicInfo[]>(DISLIKE_EVENT_NAME.add_dislike_music_infos, async({ params: listData }) => {\n    await global.lx.event_dislike.dislike_music_add(listData, false)\n  })\n  mainHandle<LX.Dislike.DislikeRules>(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, async({ params: rules }) => {\n    await global.lx.event_dislike.dislike_data_overwrite(rules, false)\n  })\n  mainHandle(DISLIKE_EVENT_NAME.clear_dislike_music_infos, async() => {\n    await global.lx.event_dislike.dislike_music_clear(false)\n  })\n}\n"
  },
  {
    "path": "src/main/modules/commonRenderers/dislike/winRendererEvent.ts",
    "content": "import { DISLIKE_EVENT_NAME } from '@common/ipcNames'\n\n// 发送列表操作事件到渲染进程的注册方法\n// 哪个渲染进程需要接收则引入此方法注册\nexport const registerRendererEvents = (sendEvent: <T = any>(name: string, params?: T | undefined) => void) => {\n  const dislike_music_add = async(listData: LX.Dislike.DislikeMusicInfo[]) => {\n    sendEvent<LX.Dislike.DislikeMusicInfo[]>(DISLIKE_EVENT_NAME.add_dislike_music_infos, listData)\n  }\n  const dislike_data_overwrite = async(rules: LX.Dislike.DislikeRules) => {\n    sendEvent<LX.Dislike.DislikeRules>(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, rules)\n  }\n  const dislike_music_clear = async() => {\n    sendEvent(DISLIKE_EVENT_NAME.clear_dislike_music_infos)\n  }\n  global.lx.event_dislike.on('dislike_music_add', dislike_music_add)\n  global.lx.event_dislike.on('dislike_data_overwrite', dislike_data_overwrite)\n  global.lx.event_dislike.on('dislike_music_clear', dislike_music_clear)\n\n  return () => {\n    global.lx.event_dislike.off('dislike_music_add', dislike_music_add)\n    global.lx.event_dislike.off('dislike_data_overwrite', dislike_data_overwrite)\n    global.lx.event_dislike.off('dislike_music_clear', dislike_music_clear)\n  }\n}\n"
  },
  {
    "path": "src/main/modules/commonRenderers/index.ts",
    "content": "import common from './common'\nimport list from './list'\nimport dislike from './dislike'\n\nexport default () => {\n  common()\n  list()\n  dislike()\n}\n"
  },
  {
    "path": "src/main/modules/commonRenderers/list/index.ts",
    "content": "export { registerRendererEvents } from './winRendererEvent'\n\nexport { default } from './rendererEvent'\n"
  },
  {
    "path": "src/main/modules/commonRenderers/list/rendererEvent.ts",
    "content": "import { mainHandle } from '@common/mainIpc'\nimport { PLAYER_EVENT_NAME } from '@common/ipcNames'\n\n// 列表操作事件（公共，只注册一次）\nexport default () => {\n  mainHandle<LX.List.UserListInfo[]>(PLAYER_EVENT_NAME.list_get, async() => {\n    return global.lx.worker.dbService.getAllUserList()\n  })\n  mainHandle<LX.List.ListActionDataOverwrite>(PLAYER_EVENT_NAME.list_data_overwire, async({ params: listData }) => {\n    await global.lx.event_list.list_data_overwrite(listData, false)\n  })\n  mainHandle<LX.List.ListActionAdd>(PLAYER_EVENT_NAME.list_add, async({ params: { position, listInfos } }) => {\n    await global.lx.event_list.list_create(position, listInfos, false)\n  })\n  mainHandle<LX.List.ListActionRemove>(PLAYER_EVENT_NAME.list_remove, async({ params: ids }) => {\n    await global.lx.event_list.list_remove(ids, false)\n  })\n  mainHandle<LX.List.ListActionUpdate>(PLAYER_EVENT_NAME.list_update, async({ params: listInfos }) => {\n    await global.lx.event_list.list_update(listInfos, false)\n  })\n  mainHandle<LX.List.ListActionUpdatePosition>(PLAYER_EVENT_NAME.list_update_position, async({ params: { position, ids } }) => {\n    await global.lx.event_list.list_update_position(position, ids, false)\n  })\n  mainHandle<string, LX.Music.MusicInfo[]>(PLAYER_EVENT_NAME.list_music_get, async({ params: listId }) => {\n    return global.lx.worker.dbService.getListMusics(listId)\n  })\n  mainHandle<LX.List.ListActionMusicAdd>(PLAYER_EVENT_NAME.list_music_add, async({ params: { id, musicInfos, addMusicLocationType } }) => {\n    await global.lx.event_list.list_music_add(id, musicInfos, addMusicLocationType, false)\n  })\n  mainHandle<LX.List.ListActionMusicMove>(PLAYER_EVENT_NAME.list_music_move, async({ params: { fromId, toId, musicInfos, addMusicLocationType } }) => {\n    await global.lx.event_list.list_music_move(fromId, toId, musicInfos, addMusicLocationType, false)\n  })\n  mainHandle<LX.List.ListActionMusicRemove>(PLAYER_EVENT_NAME.list_music_remove, async({ params: { listId, ids } }) => {\n    await global.lx.event_list.list_music_remove(listId, ids, false)\n  })\n  mainHandle<LX.List.ListActionMusicUpdate>(PLAYER_EVENT_NAME.list_music_update, async({ params: musicInfos }) => {\n    await global.lx.event_list.list_music_update(musicInfos, false)\n  })\n  mainHandle<LX.List.ListActionMusicUpdatePosition>(PLAYER_EVENT_NAME.list_music_update_position, async({ params: { listId, position, ids } }) => {\n    await global.lx.event_list.list_music_update_position(listId, position, ids, false)\n  })\n  mainHandle<LX.List.ListActionMusicOverwrite>(PLAYER_EVENT_NAME.list_music_overwrite, async({ params: { listId, musicInfos } }) => {\n    await global.lx.event_list.list_music_overwrite(listId, musicInfos, false)\n  })\n  mainHandle<LX.List.ListActionMusicClear>(PLAYER_EVENT_NAME.list_music_clear, async({ params: listId }) => {\n    await global.lx.event_list.list_music_clear(listId, false)\n  })\n  mainHandle<LX.List.ListActionCheckMusicExistList, boolean>(PLAYER_EVENT_NAME.list_music_check_exist, async({ params: { listId, musicInfoId } }) => {\n    return global.lx.worker.dbService.checkListExistMusic(listId, musicInfoId)\n  })\n  mainHandle<string, string[]>(PLAYER_EVENT_NAME.list_music_get_list_ids, async({ params: musicInfoId }) => {\n    return global.lx.worker.dbService.getMusicExistListIds(musicInfoId)\n  })\n}\n"
  },
  {
    "path": "src/main/modules/commonRenderers/list/winRendererEvent.ts",
    "content": "import { PLAYER_EVENT_NAME } from '@common/ipcNames'\n\n// 发送列表操作事件到渲染进程的注册方法\n// 哪个渲染进程需要接收则引入此方法注册\nexport const registerRendererEvents = (sendEvent: <T = any>(name: string, params?: T | undefined) => void) => {\n  const list_data_overwrite = async(listData: LX.List.ListActionDataOverwrite) => {\n    sendEvent<LX.List.ListActionDataOverwrite>(PLAYER_EVENT_NAME.list_data_overwire, listData)\n  }\n  const list_create = async(position: number, listInfos: LX.List.UserListInfo[]) => {\n    sendEvent<LX.List.ListActionAdd>(PLAYER_EVENT_NAME.list_add, { position, listInfos })\n  }\n  const list_remove = async(ids: string[]) => {\n    sendEvent<LX.List.ListActionRemove>(PLAYER_EVENT_NAME.list_remove, ids)\n  }\n  const list_update = async(lists: LX.List.UserListInfo[]) => {\n    sendEvent<LX.List.ListActionUpdate>(PLAYER_EVENT_NAME.list_update, lists)\n  }\n  const list_update_position = async(position: number, ids: string[]) => {\n    sendEvent<LX.List.ListActionUpdatePosition>(PLAYER_EVENT_NAME.list_update_position, { position, ids })\n  }\n  const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n    sendEvent<LX.List.ListActionMusicAdd>(PLAYER_EVENT_NAME.list_music_add, { id, musicInfos, addMusicLocationType })\n  }\n  const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n    sendEvent<LX.List.ListActionMusicMove>(PLAYER_EVENT_NAME.list_music_move, { fromId, toId, musicInfos, addMusicLocationType })\n  }\n  const list_music_remove = async(listId: string, ids: string[]) => {\n    sendEvent<LX.List.ListActionMusicRemove>(PLAYER_EVENT_NAME.list_music_remove, { listId, ids })\n  }\n  const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate) => {\n    sendEvent<LX.List.ListActionMusicUpdate>(PLAYER_EVENT_NAME.list_music_update, musicInfos)\n  }\n  const list_music_update_position = async(listId: string, position: number, ids: string[]) => {\n    sendEvent<LX.List.ListActionMusicUpdatePosition>(PLAYER_EVENT_NAME.list_music_update_position, { listId, position, ids })\n  }\n  const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[]) => {\n    sendEvent<LX.List.ListActionMusicOverwrite>(PLAYER_EVENT_NAME.list_music_overwrite, { listId, musicInfos })\n  }\n  const list_music_clear = async(ids: string[]) => {\n    sendEvent<LX.List.ListActionMusicClear>(PLAYER_EVENT_NAME.list_data_overwire, ids)\n  }\n  global.lx.event_list.on('list_data_overwrite', list_data_overwrite)\n  global.lx.event_list.on('list_create', list_create)\n  global.lx.event_list.on('list_remove', list_remove)\n  global.lx.event_list.on('list_update', list_update)\n  global.lx.event_list.on('list_update_position', list_update_position)\n  global.lx.event_list.on('list_music_add', list_music_add)\n  global.lx.event_list.on('list_music_move', list_music_move)\n  global.lx.event_list.on('list_music_remove', list_music_remove)\n  global.lx.event_list.on('list_music_update', list_music_update)\n  global.lx.event_list.on('list_music_update_position', list_music_update_position)\n  global.lx.event_list.on('list_music_overwrite', list_music_overwrite)\n  global.lx.event_list.on('list_music_clear', list_music_clear)\n\n  return () => {\n    global.lx.event_list.off('list_data_overwrite', list_data_overwrite)\n    global.lx.event_list.off('list_create', list_create)\n    global.lx.event_list.off('list_remove', list_remove)\n    global.lx.event_list.off('list_update', list_update)\n    global.lx.event_list.off('list_update_position', list_update_position)\n    global.lx.event_list.off('list_music_add', list_music_add)\n    global.lx.event_list.off('list_music_move', list_music_move)\n    global.lx.event_list.off('list_music_remove', list_music_remove)\n    global.lx.event_list.off('list_music_update', list_music_update)\n    global.lx.event_list.off('list_music_update_position', list_music_update_position)\n    global.lx.event_list.off('list_music_overwrite', list_music_overwrite)\n    global.lx.event_list.off('list_music_clear', list_music_clear)\n  }\n}\n"
  },
  {
    "path": "src/main/modules/hotKey/index.ts",
    "content": "import { app } from 'electron'\nimport { unRegisterHotkeyAll } from './utils'\n\nimport init from './rendererEvent'\n\nexport default () => {\n  app.on('will-quit', unRegisterHotkeyAll)\n  init()\n}\n"
  },
  {
    "path": "src/main/modules/hotKey/rendererEvent.ts",
    "content": "import { HOTKEY_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainHandle } from '@common/mainIpc'\nimport { init, registerHotkey, unRegisterHotkey, unRegisterHotkeyAll } from './utils'\n\n\nexport default () => {\n  mainHandle<LX.HotKeyActions, boolean>(HOTKEY_RENDERER_EVENT_NAME.set_config, async({ params }) => {\n    switch (params.action) {\n      case 'config':\n        // global.lx.event_app.saveConfig(data, source)\n        global.lx.event_app.hot_key_config_update(params.data)\n        return true\n      case 'enable':\n        global.lx.hotKey.enable = params.data\n        params.data ? init(true) : unRegisterHotkeyAll()\n        return true\n      case 'register':\n        return registerHotkey(params.data)\n      case 'unregister':\n        unRegisterHotkey(params.data)\n        return true\n    }\n  })\n\n  mainHandle<LX.HotKeyState>(HOTKEY_RENDERER_EVENT_NAME.status, async() => global.lx.hotKey.state)\n\n  mainHandle<boolean>(HOTKEY_RENDERER_EVENT_NAME.enable, async({ params: flag }) => {\n    flag ? init() : unRegisterHotkeyAll()\n  })\n\n  init()\n}\n"
  },
  {
    "path": "src/main/modules/hotKey/utils.ts",
    "content": "import { globalShortcut } from 'electron'\nimport { log } from '@common/utils'\n\nexport const handleKeyDown = (key: string) => {\n  if (!global.lx.hotKey.enable) return\n  global.lx.event_app.hot_key_down({ type: 'global', key })\n}\n\nconst transformedKeyRxp = /(^|\\+)[a-z]/g\n\nexport const transformedKey = (key: string): string => {\n  if (key.includes('arrow')) key = key.replace(/arrow/g, '')\n  return key.replace('mod', 'CommandOrControl').replace(transformedKeyRxp, l => l.toUpperCase())\n}\n\nexport const registerHotkey = ({ key, info }: LX.RegisterKeyInfo): boolean => {\n  let targetKey = global.lx.hotKey.state.get(key)\n  if (targetKey?.status) return true\n  const transKey = transformedKey(key)\n  // console.log('Register key:', transKey)\n  if (targetKey) {\n    targetKey.info = info\n  } else {\n    targetKey = {\n      status: false,\n      info,\n    }\n    global.lx.hotKey.state.set(key, targetKey)\n  }\n  const status = targetKey.status = globalShortcut.isRegistered(transKey)\n    ? false\n    : globalShortcut.register(transKey, () => {\n      handleKeyDown(key)\n    })\n  return status\n}\n\nexport const unRegisterHotkey = (key: string) => {\n  let transKey = transformedKey(key)\n  // console.log('Unregister key:', transKey)\n  globalShortcut.unregister(transKey)\n  global.lx.hotKey.state.delete(key)\n}\n\nexport const unRegisterHotkeyAll = () => {\n  global.lx.hotKey.state.clear()\n  globalShortcut.unregisterAll()\n}\n\n\nconst handleRegisterHotkey = (data: LX.RegisterKeyInfo) => {\n  let ret = registerHotkey(data)\n  if (!ret) log.info('Register hot key failed:', data.key)\n}\n\n\nexport const init = (isForce = false) => {\n  unRegisterHotkeyAll()\n  if (!isForce && !global.lx.hotKey.config.global.enable) return\n  // global.lx.hotKey.state = {}\n  // console.log(global.lx.hotKey.config.global.keys)\n  for (const key of Object.keys(global.lx.hotKey.config.global.keys)) {\n    try {\n      handleRegisterHotkey({ key, info: global.lx.hotKey.config.global.keys[key] })\n    } catch (err) {\n      log.info(err)\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/modules/index.ts",
    "content": "import registerUserApi from './userApi'\nimport registerWinMain from './winMain'\nimport registerHotKey from './hotKey'\nimport registerTray from './tray'\nimport registerAppMenu from './appMenu'\nimport registerWinLyric from './winLyric'\nimport registerCommonRenderers from './commonRenderers'\n\nlet isRegistered = false\nexport default () => {\n  if (isRegistered) return\n  registerUserApi()\n  registerCommonRenderers()\n  registerWinMain()\n  registerHotKey()\n  registerTray()\n  registerAppMenu()\n  registerWinLyric()\n  isRegistered = true\n}\n"
  },
  {
    "path": "src/main/modules/openApi/index.ts",
    "content": "import http from 'node:http'\nimport querystring from 'node:querystring'\nimport type { Socket } from 'node:net'\nimport { getAddress } from '@common/utils/nodejs'\nimport { sendTaskbarButtonClick } from '@main/modules/winMain'\n\nconst sendResponse = (res: http.ServerResponse, code = 200, msg: string | Record<any, unknown> = 'OK', contentType = 'text/plain; charset=utf-8') => {\n  res.writeHead(code, {\n    'Content-Type': contentType,\n    'Access-Control-Allow-Origin': '*',\n  })\n  if (typeof msg === 'object') {\n    res.end(JSON.stringify(msg))\n  } else {\n    res.end(msg)\n  }\n}\n\nlet status: LX.OpenAPI.Status = {\n  status: false,\n  message: '',\n  address: '',\n}\n\ntype SubscribeKeys = keyof LX.Player.Status\n\nlet httpServer: http.Server\nlet sockets = new Set<Socket>()\nlet responses = new Map<http.ServerResponse<http.IncomingMessage>, SubscribeKeys[]>()\nlet playerStatusKeys: SubscribeKeys[]\n\nconst defaultFilter = [\n  'status',\n  'name',\n  'singer',\n  'albumName',\n  'lyricLineText',\n  'duration',\n  'progress',\n  'playbackRate',\n] satisfies SubscribeKeys[]\n\nconst parseFilter = (filter: any) => {\n  if (typeof filter != 'string') return defaultFilter\n  filter = filter.split(',')\n  const subKeys = playerStatusKeys.filter(k => filter.includes(k))\n  return subKeys.length ? subKeys : defaultFilter\n}\nconst handleSendStatus = (res: http.ServerResponse<http.IncomingMessage>, query?: string) => {\n  const keys = parseFilter(querystring.parse(query ?? '').filter)\n  const resp: Partial<Record<SubscribeKeys, any>> = {}\n  for (const k of keys) resp[k] = global.lx.player_status[k]\n  sendResponse(res, 200, resp, 'application/json; charset=utf-8')\n}\nconst handleSendAllLyric = (res: http.ServerResponse<http.IncomingMessage>) => {\n  const resp: Partial<Record<SubscribeKeys, any>> = {\n    lyric: global.lx.player_status.lyric,\n    tlyric: global.lx.player_status.tlyric,\n    rlyric: global.lx.player_status.rlyric,\n    lxlyric: global.lx.player_status.lxlyric,\n  }\n  sendResponse(res, 200, resp, 'application/json; charset=utf-8')\n}\nconst handleSubscribePlayerStatus = (req: http.IncomingMessage, res: http.ServerResponse<http.IncomingMessage>, query?: string) => {\n  res.writeHead(200, {\n    'Content-Type': 'text/event-stream',\n    Connection: 'keep-alive',\n    'Cache-Control': 'no-cache',\n    'Access-Control-Allow-Origin': '*',\n  })\n  req.socket.setTimeout(0)\n  req.on('close', () => {\n    res.end('OK')\n    responses.delete(res)\n  })\n  const keys = parseFilter(querystring.parse(query ?? '').filter)\n  responses.set(res, keys)\n  for (const [k, v] of Object.entries(global.lx.player_status)) {\n    if (!keys.includes(k as SubscribeKeys)) continue\n    res.write(`event: ${k}\\n`)\n    res.write(`data: ${JSON.stringify(v)}\\n\\n`)\n  }\n}\n\nconst handleStartServer = async(port: number, ip: string) => new Promise<void>((resolve, reject) => {\n  playerStatusKeys = Object.keys(global.lx.player_status) as SubscribeKeys[]\n  httpServer = http.createServer((req, res): void => {\n    const [endUrl, query] = `/${req.url?.split('/').at(-1) ?? ''}`.split('?')\n    let code = 200\n    let msg = 'OK'\n    switch (endUrl) {\n      case '/status':\n        handleSendStatus(res, query)\n        return\n        // case '/test':\n        //   code = 200\n        //   res.setHeader('Content-Type', 'text/html; charset=utf-8')\n        //   msg = `<!DOCTYPE html>\n        //   <html lang=\"en\">\n        //     <head>\n        //       <meta charset=\"UTF-8\" />\n        //       <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n        //       <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n        //       <title>Nodejs Server-Sent Events</title>\n        //     </head>\n        //     <body>\n        //       <h1>Hello SSE!</h1>\n\n        //       <h2>List of Server-sent events</h2>\n        //       <ul id=\"sse-list\"></ul>\n\n        //       <script>\n        //         const subscription = new EventSource('/subscribe-player-status');\n\n        //       // Default events\n        //       subscription.addEventListener('open', () => {\n        //           console.log('Connection opened')\n        //       });\n\n      //       subscription.addEventListener('error', (err) => {\n      //           console.error(err)\n      //       });\n      //       subscription.addEventListener('lyricLineText', (event) => {\n      //           console.log(event.data)\n      //       });\n      //       subscription.addEventListener('progress', (event) => {\n      //           console.log(event.data)\n      //       });\n      //       subscription.addEventListener('name', (event) => {\n      //           console.log(event.data)\n      //       });\n      //       subscription.addEventListener('singer', (event) => {\n      //           console.log(event.data)\n      //       });\n      //       </script>\n      //     </body>\n      //   </html>`\n      //   break\n      case '/lyric':\n        msg = global.lx.player_status.lyric\n        break\n      case '/lyric-all':\n        handleSendAllLyric(res)\n        return\n      case '/play':\n        sendTaskbarButtonClick('play')\n        break\n      case '/pause':\n        sendTaskbarButtonClick('pause')\n        break\n      case '/skip-next':\n        sendTaskbarButtonClick('next')\n        break\n      case '/skip-prev':\n        sendTaskbarButtonClick('prev')\n        break\n      case '/seek': {\n        const offset = parseFloat(querystring.parse(query ?? '').offset as string)\n        if (Number.isNaN(offset) || offset < 0 || offset > global.lx.player_status.duration) {\n          code = 400\n          msg = 'Invalid offset'\n        } else {\n          sendTaskbarButtonClick('seek', parseFloat(offset.toFixed(3)))\n        }\n        break\n      }\n      case '/collect':\n        sendTaskbarButtonClick('collect')\n        break\n      case '/uncollect':\n        sendTaskbarButtonClick('unCollect')\n        break\n      case '/volume': {\n        const volume = parseInt(querystring.parse(query ?? '').volume as string)\n        if (Number.isNaN(volume) || volume < 0 || volume > 100) {\n          code = 400\n          msg = 'Invalid volume'\n        } else {\n          sendTaskbarButtonClick('volume', volume / 100)\n        }\n        break\n      }\n      case '/mute': {\n        const mute = querystring.parse(query ?? '').mute\n        if (mute == 'true') {\n          sendTaskbarButtonClick('mute', true)\n        } else if (mute == 'false') {\n          sendTaskbarButtonClick('mute', false)\n        } else {\n          code = 400\n          msg = 'Invalid mute value'\n        }\n        break\n      }\n      case '/subscribe-player-status':\n        try {\n          handleSubscribePlayerStatus(req, res, query)\n          return\n        } catch (err) {\n          console.log(err)\n          code = 500\n          msg = 'Error'\n        }\n        break\n      default:\n        code = 401\n        msg = 'Forbidden'\n        break\n    }\n    sendResponse(res, code, msg)\n  })\n  httpServer.on('error', error => {\n    console.log(error)\n    reject(error)\n  })\n  httpServer.on('connection', (socket) => {\n    sockets.add(socket)\n    socket.once('close', () => {\n      sockets.delete(socket)\n    })\n    socket.setTimeout(4000)\n  })\n\n  httpServer.on('listening', () => {\n    const addr = httpServer.address()\n    // console.log(addr)\n    if (!addr) {\n      reject(new Error('address is null'))\n      return\n    }\n    resolve()\n  })\n  httpServer.listen(port, ip)\n})\n\nconst handleStopServer = async() => new Promise<void>((resolve, reject) => {\n  if (!httpServer) return\n  httpServer.close((err) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve()\n  })\n  for (const socket of sockets) socket.destroy()\n  sockets.clear()\n  responses.clear()\n})\n\n\nconst sendStatus = (status: Partial<LX.Player.Status>) => {\n  if (!responses.size) return\n  for (const [resp, keys] of responses) {\n    for (const [k, v] of Object.entries(status)) {\n      if (!keys.includes(k as SubscribeKeys)) continue\n      resp.write(`event: ${k}\\n`)\n      resp.write(`data: ${JSON.stringify(v)}\\n\\n`)\n    }\n  }\n}\nexport const stopServer = async() => {\n  global.lx.event_app.off('player_status', sendStatus)\n  if (!status.status) {\n    status.status = false\n    status.message = ''\n    status.address = ''\n    return status\n  }\n  await handleStopServer().then(() => {\n    status.status = false\n    status.message = ''\n    status.address = ''\n  }).catch(err => {\n    console.log(err)\n    status.message = err.message\n  })\n  return status\n}\nexport const startServer = async(port: number, bindLan: boolean) => {\n  if (status.status) await stopServer()\n  await handleStartServer(port, bindLan ? '0.0.0.0' : '127.0.0.1').then(() => {\n    status.status = true\n    status.message = ''\n    let address = ['127.0.0.1']\n    if (bindLan) address = [...address, ...getAddress()]\n    status.address = address.join(', ')\n  }).catch(err => {\n    console.log(err)\n    status.status = false\n    status.message = err.message\n    status.address = ''\n  })\n  global.lx.event_app.on('player_status', sendStatus)\n  return status\n}\n\nexport const getStatus = (): LX.OpenAPI.Status => status\n"
  },
  {
    "path": "src/main/modules/sync/client/auth.ts",
    "content": "import { request, generateRsaKey } from './utils'\nimport { getSyncAuthKey, setSyncAuthKey } from './data'\nimport log from '../log'\nimport { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils'\nimport { toMD5 } from '@common/utils/nodejs'\nimport { SYNC_CODE } from '@common/constants_sync'\n\n\nconst hello = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)\n  .then(({ text }) => text == SYNC_CODE.helloMsg)\n  .catch((err: any) => {\n    log.error('[auth] hello', err.message)\n    console.log(err)\n    return false\n  })\n\nconst getServerId = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/id`)\n  .then(({ text }) => {\n    if (!text.startsWith(SYNC_CODE.idPrefix)) return ''\n    return text.replace(SYNC_CODE.idPrefix, '')\n  })\n  .catch((err: any) => {\n    log.error('[auth] getServerId', err.message)\n    console.log(err)\n    throw err\n  })\n\nconst codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode: string) => {\n  let key = toMD5(authCode).substring(0, 16)\n  // const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')\n  key = Buffer.from(key).toString('base64')\n  let { publicKey, privateKey } = await generateRsaKey()\n  publicKey = publicKey.replace(/\\n/g, '')\n    .replace('-----BEGIN PUBLIC KEY-----', '')\n    .replace('-----END PUBLIC KEY-----', '')\n  const msg = aesEncrypt(`${SYNC_CODE.authMsg}\\n${publicKey}\\n${getComputerName()}\\nlx_music_desktop`, key)\n  // console.log(msg, key)\n  return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { m: msg } }).then(async({ text, code }) => {\n    // console.log(text)\n    switch (text) {\n      case SYNC_CODE.msgBlockedIp:\n        throw new Error(SYNC_CODE.msgBlockedIp)\n      case SYNC_CODE.authFailed:\n        throw new Error(SYNC_CODE.authFailed)\n      default:\n        if (code != 200) throw new Error(SYNC_CODE.authFailed)\n    }\n    let msg\n    try {\n      msg = rsaDecrypt(Buffer.from(text, 'base64'), privateKey).toString()\n    } catch (err: any) {\n      log.error('[auth] codeAuth decryptMsg error', err.message)\n      throw new Error(SYNC_CODE.authFailed)\n    }\n    // console.log(msg)\n    if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed))\n    const info = JSON.parse(msg) as LX.Sync.ClientKeyInfo\n    void setSyncAuthKey(serverId, info)\n    return info\n  })\n}\n\nconst keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {\n  const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key)\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(({ text, code }) => {\n    if (code != 200) throw new Error(SYNC_CODE.authFailed)\n\n    let msg\n    try {\n      msg = aesDecrypt(text, keyInfo.key)\n    } catch (err: any) {\n      log.error('[auth] keyAuth decryptMsg error', err.message)\n      throw new Error(SYNC_CODE.authFailed)\n    }\n    if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed))\n  })\n}\n\nconst auth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode?: string) => {\n  if (authCode) return codeAuth(urlInfo, serverId, authCode)\n  const keyInfo = await getSyncAuthKey(serverId)\n  if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode)\n  await keyAuth(urlInfo, keyInfo)\n  return keyInfo\n}\n\nexport default async(urlInfo: LX.Sync.Client.UrlInfo, authCode?: string) => {\n  console.log('connect: ', urlInfo.href, authCode)\n  if (!await hello(urlInfo)) throw new Error(SYNC_CODE.connectServiceFailed)\n  const serverId = await getServerId(urlInfo)\n  if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed)\n  return auth(urlInfo, serverId, authCode)\n}\n"
  },
  {
    "path": "src/main/modules/sync/client/client.ts",
    "content": "import WebSocket from 'ws'\nimport { encryptMsg, decryptMsg } from './utils'\nimport { callObj } from './sync'\n// import { action as commonAction } from '@root/store/modules/common'\n// import { getStore } from '@root/store'\n// import registerSyncListHandler from './syncList'\nimport log from '../log'\nimport { dateFormat } from '@common/utils/common'\nimport { aesEncrypt } from '../utils'\nimport { sendClientStatus } from '@main/modules/winMain'\nimport { createMsg2call } from 'message2call'\nimport { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants_sync'\nimport { getAddress } from '@common/utils/nodejs'\n\nlet status: LX.Sync.ClientStatus = {\n  status: false,\n  message: '',\n  address: [],\n}\n\nexport const sendSyncStatus = (newStatus: Omit<LX.Sync.ClientStatus, 'address'>) => {\n  status.status = newStatus.status\n  status.message = newStatus.message\n  if (status.status) {\n    status.address = getAddress()\n  }\n  sendClientStatus(status)\n}\n\nexport const sendSyncMessage = (message: string) => {\n  status.message = message\n  sendClientStatus(status)\n}\n\nconst heartbeatTools = {\n  failedNum: 0,\n  maxTryNum: 100000,\n  stepMs: 3000,\n  connectTimeout: null as NodeJS.Timeout | null,\n  pingTimeout: null as NodeJS.Timeout | null,\n  delayRetryTimeout: null as NodeJS.Timeout | null,\n  handleOpen() {\n    console.log('open')\n    // this.failedNum = 0\n    this.heartbeat()\n  },\n  heartbeat() {\n    if (this.pingTimeout) clearTimeout(this.pingTimeout)\n\n    // Use `WebSocket#terminate()`, which immediately destroys the connection,\n    // instead of `WebSocket#close()`, which waits for the close timer.\n    // Delay should be equal to the interval at which your server\n    // sends out pings plus a conservative assumption of the latency.\n    this.pingTimeout = setTimeout(() => {\n      client?.terminate()\n    }, 30000 + 1000)\n  },\n  reConnnect() {\n    this.clearTimeout()\n    // client = null\n    if (!client) return\n\n    if (++this.failedNum > this.maxTryNum) {\n      this.failedNum = 0\n      sendSyncStatus({\n        status: false,\n        message: 'Connect error',\n      })\n      throw new Error('connect error')\n    }\n\n    const waitTime = Math.min(2000 + Math.floor(this.failedNum / 2) * this.stepMs, 15000)\n\n    // sendSyncStatus({\n    //   status: false,\n    //   message: `Waiting ${waitTime / 1000}s reconnnect...`,\n    // })\n\n    this.delayRetryTimeout = setTimeout(() => {\n      this.delayRetryTimeout = null\n      if (!client) return\n      console.log(dateFormat(new Date()), 'reconnnect...')\n      sendSyncStatus({\n        status: false,\n        message: `Try reconnnect... (${this.failedNum})`,\n      })\n      connect(client.data.urlInfo, client.data.keyInfo)\n    }, waitTime)\n  },\n  clearTimeout() {\n    if (this.connectTimeout) {\n      clearTimeout(this.connectTimeout)\n      this.connectTimeout = null\n    }\n    if (this.delayRetryTimeout) {\n      clearTimeout(this.delayRetryTimeout)\n      this.delayRetryTimeout = null\n    }\n    if (this.pingTimeout) {\n      clearTimeout(this.pingTimeout)\n      this.pingTimeout = null\n    }\n  },\n  connect(socket: LX.Sync.Client.Socket) {\n    console.log('heartbeatTools connect')\n    this.connectTimeout = setTimeout(() => {\n      this.connectTimeout = null\n      if (client) {\n        try {\n          client.close(SYNC_CLOSE_CODE.failed)\n        } catch {}\n      }\n      if (++this.failedNum > this.maxTryNum) {\n        this.failedNum = 0\n        sendSyncStatus({\n          status: false,\n          message: 'Connect error',\n        })\n        throw new Error('connect error')\n      }\n      sendSyncStatus({\n        status: false,\n        message: 'Connect timeout, try reconnect...',\n      })\n      this.reConnnect()\n    }, 2 * 60 * 1000)\n    socket.on('open', () => {\n      if (this.connectTimeout) {\n        clearTimeout(this.connectTimeout)\n        this.connectTimeout = null\n      }\n      this.handleOpen()\n    })\n    socket.on('ping', () => {\n      this.heartbeat()\n    })\n    socket.on('close', (code) => {\n      console.log(code)\n      switch (code) {\n        case SYNC_CLOSE_CODE.normal:\n        case SYNC_CLOSE_CODE.failed:\n          return\n      }\n      this.reConnnect()\n    })\n  },\n}\n\n\nlet client: LX.Sync.Client.Socket | null\n// let listSyncPromise: Promise<void>\nexport const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {\n  client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {\n  }) as LX.Sync.Client.Socket\n  client.data = {\n    keyInfo,\n    urlInfo,\n  }\n  heartbeatTools.connect(client)\n\n  let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []\n  let disconnected = true\n\n  const message2read = createMsg2call<LX.Sync.ServerSyncActions>({\n    funcsObj: {\n      ...callObj,\n      finished() {\n        log.info('sync list success')\n        client!.isReady = true\n        sendSyncStatus({\n          status: true,\n          message: '',\n        })\n        heartbeatTools.failedNum = 0\n      },\n    },\n    timeout: 120 * 1000,\n    sendMessage(data) {\n      if (disconnected) throw new Error('disconnected')\n      void encryptMsg(keyInfo, JSON.stringify(data)).then((data) => {\n        client?.send(data)\n      }).catch((err) => {\n        log.error('encrypt msg error: ', err)\n        client?.close(SYNC_CLOSE_CODE.failed)\n      })\n    },\n    onCallBeforeParams(rawArgs) {\n      return [client, ...rawArgs]\n    },\n    onError(error, path, groupName) {\n      const name = groupName ?? ''\n      log.error(`sync call ${name} ${path.join('.')} error:`, error)\n      // if (groupName == null) return\n      // client?.close(SYNC_CLOSE_CODE.failed)\n      // sendSyncStatus({\n      //   status: false,\n      //   message: error.message,\n      // })\n    },\n  })\n\n  client.remote = message2read.remote\n  client.remoteQueueList = message2read.createQueueRemote('list')\n  client.remoteQueueDislike = message2read.createQueueRemote('dislike')\n\n  client.addEventListener('message', ({ data }) => {\n    if (data == 'ping') return\n    if (typeof data === 'string') {\n      void decryptMsg(keyInfo, data).then((data) => {\n        let syncData: LX.Sync.ServerSyncActions\n        try {\n          syncData = JSON.parse(data)\n        } catch (err) {\n          log.error('parse msg error: ', err)\n          client?.close(SYNC_CLOSE_CODE.failed)\n          return\n        }\n        message2read.message(syncData)\n      }).catch((error) => {\n        log.error('decrypt msg error: ', error)\n        client?.close(SYNC_CLOSE_CODE.failed)\n      })\n    }\n  })\n  client.onClose = function(handler: typeof closeEvents[number]) {\n    closeEvents.push(handler)\n    return () => {\n      closeEvents.splice(closeEvents.indexOf(handler), 1)\n    }\n  }\n\n  const initMessage = 'Wait syncing...'\n  client.addEventListener('open', () => {\n    log.info('connect')\n    // const store = getStore()\n    // global.lx.syncKeyInfo = keyInfo\n    client!.isReady = false\n    client!.moduleReadys = {\n      list: false,\n      dislike: false,\n    }\n    disconnected = false\n    sendSyncStatus({\n      status: false,\n      message: initMessage,\n    })\n  })\n  client.addEventListener('close', ({ code }) => {\n    const err = new Error('closed')\n    try {\n      for (const handler of closeEvents) void handler(err)\n    } catch (err: any) {\n      log.error(err?.message)\n    }\n    closeEvents = []\n    disconnected = true\n    message2read.destroy()\n    switch (code) {\n      case SYNC_CLOSE_CODE.normal:\n      // case SYNC_CLOSE_CODE.failed:\n        sendSyncStatus({\n          status: false,\n          message: '',\n        })\n        break\n      case SYNC_CLOSE_CODE.failed:\n        if (!status.message || status.message == initMessage) {\n          sendSyncStatus({\n            status: false,\n            message: 'failed',\n          })\n        }\n        break\n    }\n  })\n  client.addEventListener('error', ({ message }) => {\n    sendSyncStatus({\n      status: false,\n      message,\n    })\n  })\n}\n\nexport const disconnect = async() => {\n  if (!client) return\n  log.info('disconnecting...')\n  client.close(SYNC_CLOSE_CODE.normal)\n  client = null\n  heartbeatTools.clearTimeout()\n  heartbeatTools.failedNum = 0\n}\n\nexport const getStatus = (): LX.Sync.ClientStatus => status\n"
  },
  {
    "path": "src/main/modules/sync/client/data.ts",
    "content": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { File } from '../../../../common/constants_sync'\nimport { exists } from '../utils'\n\n\nlet syncAuthKeys: Record<string, LX.Sync.ClientKeyInfo>\n\n\nconst saveSyncAuthKeys = async() => {\n  const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON)\n  return fs.promises.writeFile(syncAuthKeysFilePath, JSON.stringify(syncAuthKeys), 'utf8')\n}\n\nexport const initClientInfo = async() => {\n  if (syncAuthKeys != null) return\n  const syncAuthKeysFilePath = path.join(global.lxDataPath, File.clientDataPath, File.syncAuthKeysJSON)\n  if (await fs.promises.stat(syncAuthKeysFilePath).then(() => true).catch(() => false)) {\n    // eslint-disable-next-line require-atomic-updates\n    syncAuthKeys = JSON.parse((await fs.promises.readFile(syncAuthKeysFilePath)).toString())\n  } else {\n    // eslint-disable-next-line require-atomic-updates\n    syncAuthKeys = {}\n    const syncDataPath = path.join(global.lxDataPath, File.clientDataPath)\n    if (!await exists(syncDataPath)) {\n      await fs.promises.mkdir(syncDataPath, { recursive: true })\n    }\n    void saveSyncAuthKeys()\n  }\n}\n\nexport const getSyncAuthKey = async(serverId: string) => {\n  await initClientInfo()\n  return syncAuthKeys[serverId] ?? null\n}\nexport const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => {\n  await initClientInfo()\n  syncAuthKeys[serverId] = info\n  void saveSyncAuthKeys()\n}\n\n// let syncHost: string\n// export const getSyncHost = async() => {\n//   if (syncHost === undefined) {\n//     const store = getStore(STORE_NAMES.SYNC)\n//     syncHost = (store.get('syncHost') as typeof syncHost | null) ?? ''\n//   }\n//   return syncHost\n// }\n// export const setSyncHost = async(host: string) => {\n//   // let hostInfo = await getData(syncHostPrefix) || {}\n//   // hostInfo.host = host\n//   // hostInfo.port = port\n//   syncHost = host\n//   const store = getStore(STORE_NAMES.SYNC)\n//   store.set('syncHost', syncHost)\n// }\n// let syncHostHistory: string[]\n// export const getSyncHostHistory = async() => {\n//   if (syncHostHistory === undefined) {\n//     const store = getStore(STORE_NAMES.SYNC)\n//     syncHostHistory = (store.get('syncHostHistory') as string[]) ?? []\n//   }\n//   return syncHostHistory\n// }\n// export const addSyncHostHistory = async(host: string) => {\n//   let syncHostHistory = await getSyncHostHistory()\n//   if (syncHostHistory.some(h => h == host)) return\n//   syncHostHistory.unshift(host)\n//   if (syncHostHistory.length > 20) syncHostHistory = syncHostHistory.slice(0, 20) // 最多存储20个\n//   const store = getStore(STORE_NAMES.SYNC)\n//   store.set('syncHostHistory', syncHostHistory)\n// }\n// export const removeSyncHostHistory = async(index: number) => {\n//   syncHostHistory.splice(index, 1)\n//   const store = getStore(STORE_NAMES.SYNC)\n//   store.set('syncHostHistory', syncHostHistory)\n// }\n"
  },
  {
    "path": "src/main/modules/sync/client/index.ts",
    "content": "import handleAuth from './auth'\nimport { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client'\n// import { getSyncHost } from '@root/utils/data'\nimport log from '../log'\nimport { parseUrl } from './utils'\nimport migrateData from '../migrate'\nimport { SYNC_CODE } from '@common/constants_sync'\n\nlet connectId = 0\n\nconst handleConnect = async(host: string, authCode?: string) => {\n  // const hostInfo = await getSyncHost()\n  // console.log(hostInfo)\n  // if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error(SYNC_CODE.unknownServiceAddress)\n  const id = connectId\n  const urlInfo = parseUrl(host)\n  await disconnectServer(false)\n  if (id != connectId) return\n  const keyInfo = await handleAuth(urlInfo, authCode)\n  if (id != connectId) return\n  socketConnect(urlInfo, keyInfo)\n}\nconst handleDisconnect = async() => {\n  await socketDisconnect()\n}\n\nconst connectServer = async(host: string, authCode?: string) => {\n  sendSyncStatus({\n    status: false,\n    message: SYNC_CODE.connecting,\n  })\n  const id = connectId\n  await migrateData(global.lxDataPath)\n\n  return handleConnect(host, authCode).catch(async err => {\n    if (id != connectId) return\n    sendSyncStatus({\n      status: false,\n      message: err.message,\n    })\n    switch (err.message) {\n      case SYNC_CODE.connectServiceFailed:\n      case SYNC_CODE.missingAuthCode:\n        break\n      default:\n        log.r_warn(err.message)\n        break\n    }\n\n    return Promise.reject(err)\n  })\n}\n\nconst disconnectServer = async(isResetStatus = true) => handleDisconnect().then(() => {\n  log.info('disconnect...')\n  if (isResetStatus) {\n    connectId++\n    sendSyncStatus({\n      status: false,\n      message: '',\n    })\n  }\n}).catch((err: any) => {\n  log.error(`disconnect error: ${err.message as string}`)\n  sendSyncMessage(err.message)\n})\n\nexport {\n  connectServer,\n  disconnectServer,\n}\n\nexport {\n  getStatus,\n} from './client'\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/dislike/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\nimport { handleRemoteDislikeAction, getLocalDislikeData, setLocalDislikeData } from '@main/modules/sync/dislikeEvent'\nimport { toMD5 } from '@common/utils/nodejs'\nimport { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'\nimport log from '@main/modules/sync/log'\nimport { registerEvent, unregisterEvent } from './localEvent'\n\nconst logInfo = (eventName: string, success = false) => {\n  log.info(`[${eventName}]${eventName.replace('dislike:sync:dislike_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`)\n}\n// const logError = (eventName: string, err: Error) => {\n//   log.error(`[${eventName}]${eventName.replace('dislike:sync:dislike_sync_', '').replaceAll('_', ' ')} error: ${err.message}`)\n// }\nconst getSyncMode = async(socket: LX.Sync.Client.Socket): Promise<LX.Sync.Dislike.SyncMode> => new Promise((resolve, reject) => {\n  const handleDisconnect = (err: Error) => {\n    sendCloseSelectMode()\n    removeSelectModeListener()\n    reject(err)\n  }\n  let removeEventClose = socket.onClose(handleDisconnect)\n  sendSelectMode(socket.data.keyInfo.serverName, 'dislike', (mode) => {\n    if (mode == null) {\n      reject(new Error('cancel'))\n      return\n    }\n    resolve(mode)\n    removeSelectModeListener()\n    removeEventClose()\n  })\n})\nconst handler: LX.Sync.ClientSyncHandlerDislikeActions<LX.Sync.Client.Socket> = {\n  async onDislikeSyncAction(socket, action) {\n    if (!socket.moduleReadys?.dislike) return\n    await handleRemoteDislikeAction(action)\n  },\n\n  async dislike_sync_get_md5(socket) {\n    logInfo('dislike:sync:dislike_sync_get_md5')\n    return toMD5((await getLocalDislikeData()).trim())\n  },\n\n\n  async dislike_sync_get_sync_mode(socket) {\n    return getSyncMode(socket)\n  },\n\n  async dislike_sync_get_list_data(socket) {\n    logInfo('dislike:sync:dislike_sync_get_list_data')\n    return getLocalDislikeData()\n  },\n\n  async dislike_sync_set_list_data(socket, data) {\n    logInfo('dislike:sync:dislike_sync_set_list_data')\n    await setLocalDislikeData(data)\n  },\n\n  async dislike_sync_finished(socket) {\n    logInfo('dislike:sync:finished')\n    socket.moduleReadys.dislike = true\n    registerEvent(socket)\n    socket.onClose(() => {\n      unregisterEvent()\n    })\n  },\n}\n\nexport default handler\n\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/dislike/index.ts",
    "content": "\nexport { default as handler } from './handler'\n\nexport * from './localEvent'\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/dislike/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { registerDislikeActionEvent } from '@main/modules/sync/dislikeEvent'\n\nlet unregisterLocalListAction: (() => void) | null\n\nexport const registerEvent = (socket: LX.Sync.Client.Socket) => {\n  // socket = _socket\n  // socket.onClose(() => {\n  //   unregisterLocalListAction?.()\n  //   unregisterLocalListAction = null\n  // })\n  unregisterEvent()\n  unregisterLocalListAction = registerDislikeActionEvent((action) => {\n    if (!socket.moduleReadys?.dislike) return\n    void socket.remoteQueueDislike.onDislikeSyncAction(action).catch(err => {\n      // TODO send status\n      socket.moduleReadys.dislike = false\n      socket.close(SYNC_CLOSE_CODE.failed)\n      console.log(err.message)\n    })\n  })\n}\n\nexport const unregisterEvent = () => {\n  unregisterLocalListAction?.()\n  unregisterLocalListAction = null\n}\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/index.ts",
    "content": "import * as list from './list'\nimport * as dislike from './dislike'\n// export * as theme from './theme'\n\n\nexport const callObj = Object.assign({},\n  list.handler,\n  dislike.handler,\n)\n\n\nexport const modules = {\n  list,\n  dislike,\n}\n\nexport const featureVersion = {\n  list: 1,\n  dislike: 1,\n} as const\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/list/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\nimport {\n  handleRemoteListAction,\n  getLocalListData,\n  setLocalListData,\n} from '@main/modules/sync/listEvent'\nimport { toMD5 } from '@common/utils/nodejs'\nimport { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'\nimport log from '@main/modules/sync/log'\nimport { registerEvent, unregisterEvent } from './localEvent'\n\nconst logInfo = (eventName: string, success = false) => {\n  log.info(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`)\n}\n// const logError = (eventName: string, err: Error) => {\n//   log.error(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')} error: ${err.message}`)\n// }\nconst getSyncMode = async(socket: LX.Sync.Client.Socket): Promise<LX.Sync.List.SyncMode> => new Promise((resolve, reject) => {\n  const handleDisconnect = (err: Error) => {\n    sendCloseSelectMode()\n    removeSelectModeListener()\n    reject(err)\n  }\n  let removeEventClose = socket.onClose(handleDisconnect)\n  sendSelectMode(socket.data.keyInfo.serverName, 'list', (mode) => {\n    if (mode == null) {\n      reject(new Error('cancel'))\n      return\n    }\n    resolve(mode)\n    removeSelectModeListener()\n    removeEventClose()\n  })\n})\n\nconst handler: LX.Sync.ClientSyncHandlerListActions<LX.Sync.Client.Socket> = {\n  async onListSyncAction(socket, action) {\n    if (!socket.moduleReadys?.list) return\n    await handleRemoteListAction(action)\n  },\n\n  async list_sync_get_md5(socket) {\n    logInfo('list:sync:list_sync_get_md5')\n    return toMD5(JSON.stringify(await getLocalListData()))\n  },\n\n\n  async list_sync_get_sync_mode(socket) {\n    return getSyncMode(socket)\n  },\n\n  async list_sync_get_list_data(socket) {\n    logInfo('list:sync:list_sync_get_list_data')\n    return getLocalListData()\n  },\n\n  async list_sync_set_list_data(socket, data) {\n    logInfo('list:sync:list_sync_set_list_data')\n    await setLocalListData(data)\n  },\n\n  async list_sync_finished(socket) {\n    logInfo('list:sync:finished')\n    socket.moduleReadys.list = true\n    registerEvent(socket)\n    socket.onClose(() => {\n      unregisterEvent()\n    })\n  },\n}\n\nexport default handler\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/list/index.ts",
    "content": "\nexport { default as handler } from './handler'\n\nexport * from './localEvent'\n"
  },
  {
    "path": "src/main/modules/sync/client/modules/list/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { registerListActionEvent } from '@main/modules/sync/listEvent'\n\nlet unregisterLocalListAction: (() => void) | null\n\nexport const registerEvent = (socket: LX.Sync.Client.Socket) => {\n  // socket = _socket\n  // socket.onClose(() => {\n  //   unregisterLocalListAction?.()\n  //   unregisterLocalListAction = null\n  // })\n  unregisterEvent()\n  unregisterLocalListAction = registerListActionEvent((action) => {\n    if (!socket.moduleReadys?.list) return\n    void socket.remoteQueueList.onListSyncAction(action).catch(err => {\n      // TODO send status\n      socket.moduleReadys.list = false\n      socket.close(SYNC_CLOSE_CODE.failed)\n      console.log(err.message)\n    })\n  })\n}\n\nexport const unregisterEvent = () => {\n  unregisterLocalListAction?.()\n  unregisterLocalListAction = null\n}\n"
  },
  {
    "path": "src/main/modules/sync/client/sync/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\n\n// import { getUserSpace } from '@/user'\n// import { modules } from '../modules'\n\nimport { featureVersion } from '../modules'\n\nconst handler: Omit<LX.Sync.ClientSyncHandlerActions<LX.Sync.Client.Socket>, 'finished'> = {\n  async getEnabledFeatures(socket, serverType, supportedFeatures) {\n  // const userSpace = getUserSpace(socket.userInfo.name)\n    const features: LX.Sync.EnabledFeatures = {}\n    switch (serverType) {\n      case 'server':\n        if (featureVersion.list == supportedFeatures.list) {\n          features.list = { skipSnapshot: false }\n        }\n        if (featureVersion.dislike == supportedFeatures.dislike) {\n          features.dislike = { skipSnapshot: false }\n        }\n        return features\n      case 'desktop-app':\n      default:\n        if (featureVersion.list == supportedFeatures.list) {\n          features.list = { skipSnapshot: false }\n        }\n        if (featureVersion.dislike == supportedFeatures.dislike) {\n          features.dislike = { skipSnapshot: false }\n        }\n        return features\n    }\n  },\n}\n\nexport default handler\n"
  },
  {
    "path": "src/main/modules/sync/client/sync/index.ts",
    "content": "import handler from './handler'\nimport { callObj as _callObj } from '../modules'\nexport { modules } from '../modules'\n\nexport const callObj = {\n  ...handler,\n  ..._callObj,\n}\n"
  },
  {
    "path": "src/main/modules/sync/client/utils.ts",
    "content": "import { generateKeyPair } from 'node:crypto'\nimport { httpFetch, type RequestOptions } from '@main/utils/request'\nimport { decodeData, encodeData } from '../utils'\n\nexport const request = async(url: string, options: RequestOptions = { }) => {\n  return httpFetch<string>(url, {\n    ...options,\n    timeout: options.timeout ?? 10000,\n  }).then(response => {\n    return {\n      text: response.body,\n      code: response.statusCode,\n    }\n  })\n  // const controller = new AbortController()\n  // let id: number | null = setTimeout(() => {\n  //   id = null\n  //   controller.abort()\n  // }, timeout)\n  // return fetch(url, {\n  //   ...options,\n  //   signal: controller.signal,\n  // // eslint-disable-next-line @typescript-eslint/promise-function-async\n  // }).then(async(response) => {\n  //   const text = await response.text()\n  //   return {\n  //     text,\n  //     code: response.status,\n  //   }\n  // }).catch(err => {\n  //   // console.log(err, err.code, err.message)\n  //   throw err\n  // }).finally(() => {\n  //   if (id == null) return\n  //   clearTimeout(id)\n  // })\n}\n\n// export const aesEncrypt = (text: string, key: string, iv: string) => {\n//   const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))\n//   return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')\n// }\n\n// export const aesDecrypt = (text: string, key: string, iv: string) => {\n//   const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))\n//   return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()\n// }\n\nexport const generateRsaKey = async() => new Promise<{ publicKey: string, privateKey: string }>((resolve, reject) => {\n  generateKeyPair(\n    'rsa',\n    {\n      modulusLength: 2048, // It holds a number. It is the key size in bits and is applicable for RSA, and DSA algorithm only.\n      publicKeyEncoding: {\n        type: 'spki', // Note the type is pkcs1 not spki\n        format: 'pem',\n      },\n      privateKeyEncoding: {\n        type: 'pkcs8', // Note again the type is set to pkcs1\n        format: 'pem',\n        // cipher: \"aes-256-cbc\", //Optional\n        // passphrase: \"\", //Optional\n      },\n    },\n    (err, publicKey, privateKey) => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve({\n        publicKey,\n        privateKey,\n      })\n    },\n  )\n})\n\nexport const encryptMsg = async(keyInfo: LX.Sync.ClientKeyInfo, msg: string): Promise<string> => {\n  return encodeData(msg)\n  // if (!keyInfo) return ''\n  // return aesEncrypt(msg, keyInfo.key, keyInfo.iv)\n}\n\nexport const decryptMsg = async(keyInfo: LX.Sync.ClientKeyInfo, enMsg: string): Promise<string> => {\n  return decodeData(enMsg)\n  // if (!keyInfo) return ''\n  // let msg = ''\n  // try {\n  //   msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)\n  // } catch (err) {\n  //   console.log(err)\n  // }\n  // return msg\n}\n\n\nexport const parseUrl = (host: string): LX.Sync.Client.UrlInfo => {\n  const url = new URL(host)\n  let hostPath = url.host + url.pathname\n  let href = url.href\n  if (hostPath.endsWith('/')) hostPath = hostPath.replace(/\\/$/, '')\n  if (href.endsWith('/')) href = href.replace(/\\/$/, '')\n\n  return {\n    wsProtocol: url.protocol == 'https:' ? 'wss:' : 'ws:',\n    httpProtocol: url.protocol,\n    hostPath,\n    href,\n  }\n}\n\n\nexport const sendStatus = (status: LX.Sync.ClientStatus) => {\n  // syncLog.log(JSON.stringify(status))\n}\n"
  },
  {
    "path": "src/main/modules/sync/dislikeEvent.ts",
    "content": "\nexport const getLocalDislikeData = async(): Promise<LX.Dislike.DislikeRules> => {\n  return (await global.lx.worker.dbService.getDislikeListInfo()).rules\n}\n\nexport const setLocalDislikeData = async(listData: LX.Dislike.DislikeRules) => {\n  await global.lx.event_dislike.dislike_data_overwrite(listData, true)\n}\n\nexport const registerDislikeActionEvent = (sendDislikeAction: (action: LX.Sync.Dislike.ActionList) => (void | Promise<void>)) => {\n  const dislike_music_add = async(listData: LX.Dislike.DislikeMusicInfo[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendDislikeAction({ action: 'dislike_music_add', data: listData })\n  }\n  const dislike_data_overwrite = async(listInfos: LX.Dislike.DislikeRules, isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendDislikeAction({ action: 'dislike_data_overwrite', data: listInfos })\n  }\n  const dislike_music_clear = async(isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendDislikeAction({ action: 'dislike_music_clear' })\n  }\n\n  global.lx.event_dislike.on('dislike_music_add', dislike_music_add)\n  global.lx.event_dislike.on('dislike_data_overwrite', dislike_data_overwrite)\n  global.lx.event_dislike.on('dislike_music_clear', dislike_music_clear)\n  return () => {\n    global.lx.event_dislike.off('dislike_music_add', dislike_music_add)\n    global.lx.event_dislike.off('dislike_data_overwrite', dislike_data_overwrite)\n    global.lx.event_dislike.off('dislike_music_clear', dislike_music_clear)\n  }\n}\n\nexport const handleRemoteDislikeAction = async(event: LX.Sync.Dislike.ActionList) => {\n  // console.log('handleRemoteDislikeAction', event)\n\n  switch (event.action) {\n    case 'dislike_music_add':\n      await global.lx.event_dislike.dislike_music_add(event.data, true)\n      break\n    case 'dislike_data_overwrite':\n      await global.lx.event_dislike.dislike_data_overwrite(event.data, true)\n      break\n    case 'dislike_music_clear':\n      await global.lx.event_dislike.dislike_music_clear(true)\n      break\n    default:\n      throw new Error('unknown list sync action')\n  }\n}\n"
  },
  {
    "path": "src/main/modules/sync/index.ts",
    "content": "// import Event from './event/event'\n\nimport { disconnectServer } from './client'\nimport { stopServer } from './server'\n\n// import eventNames from './event/name'\nexport {\n  startServer,\n  stopServer,\n  getStatus as getServerStatus,\n  generateCode,\n  getDevices as getServerDevices,\n  removeDevice as removeServerDevice,\n} from './server'\n\nexport {\n  connectServer,\n  disconnectServer,\n  getStatus as getClientStatus,\n} from './client'\n\nexport default () => {\n  global.lx.event_app.on('main_window_close', () => {\n    if (global.lx.appSetting['sync.mode'] == 'server') {\n      void stopServer()\n    } else {\n      void disconnectServer()\n    }\n  })\n}\n"
  },
  {
    "path": "src/main/modules/sync/listEvent.ts",
    "content": "import { LIST_IDS } from '@common/constants'\n\n// 构建列表信息对象，用于统一字段位置顺序\nexport const buildUserListInfoFull = ({ id, name, source, sourceListId, list, locationUpdateTime }: LX.List.UserListInfoFull) => {\n  return {\n    id,\n    name,\n    source,\n    sourceListId,\n    locationUpdateTime,\n    list,\n  }\n}\n\nexport const getLocalListData = async(): Promise<LX.Sync.List.ListData> => {\n  const lists: LX.Sync.List.ListData = {\n    defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),\n    loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),\n    userList: [],\n  }\n\n  const userListInfos = await global.lx.worker.dbService.getAllUserList()\n  for await (const list of userListInfos) {\n    lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)\n      .then(musics => buildUserListInfoFull({ ...list, list: musics })))\n  }\n\n  return lists\n}\n\nexport const setLocalListData = async(listData: LX.Sync.List.ListData) => {\n  await global.lx.event_list.list_data_overwrite(listData, true)\n}\n\n\nexport const registerListActionEvent = (sendListAction: (action: LX.Sync.List.ActionList) => (void | Promise<void>)) => {\n  const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_data_overwrite', data: listData })\n  }\n  const list_create = async(position: number, listInfos: LX.List.UserListInfo[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_create', data: { position, listInfos } })\n  }\n  const list_remove = async(ids: string[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_remove', data: ids })\n  }\n  const list_update = async(lists: LX.List.UserListInfo[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_update', data: lists })\n  }\n  const list_update_position = async(position: number, ids: string[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_update_position', data: { position, ids } })\n  }\n  const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) => {\n    if (isRemote || listId == LIST_IDS.TEMP) return\n    await sendListAction({ action: 'list_music_overwrite', data: { listId, musicInfos } })\n  }\n  const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_music_add', data: { id, musicInfos, addMusicLocationType } })\n  }\n  const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_music_move', data: { fromId, toId, musicInfos, addMusicLocationType } })\n  }\n  const list_music_remove = async(listId: string, ids: string[], isRemote: boolean = false) => {\n    if (isRemote || listId == LIST_IDS.TEMP) return\n    await sendListAction({ action: 'list_music_remove', data: { listId, ids } })\n  }\n  const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) => {\n    musicInfos = musicInfos.filter(item => item.id != LIST_IDS.TEMP)\n    if (isRemote || !musicInfos.length) return\n    await sendListAction({ action: 'list_music_update', data: musicInfos })\n  }\n  const list_music_clear = async(ids: string[], isRemote: boolean = false) => {\n    if (isRemote) return\n    await sendListAction({ action: 'list_music_clear', data: ids })\n  }\n  const list_music_update_position = async(listId: string, position: number, ids: string[], isRemote: boolean = false) => {\n    if (isRemote || listId == LIST_IDS.TEMP) return\n    await sendListAction({ action: 'list_music_update_position', data: { listId, position, ids } })\n  }\n  global.lx.event_list.on('list_data_overwrite', list_data_overwrite)\n  global.lx.event_list.on('list_create', list_create)\n  global.lx.event_list.on('list_remove', list_remove)\n  global.lx.event_list.on('list_update', list_update)\n  global.lx.event_list.on('list_update_position', list_update_position)\n  global.lx.event_list.on('list_music_overwrite', list_music_overwrite)\n  global.lx.event_list.on('list_music_add', list_music_add)\n  global.lx.event_list.on('list_music_move', list_music_move)\n  global.lx.event_list.on('list_music_remove', list_music_remove)\n  global.lx.event_list.on('list_music_update', list_music_update)\n  global.lx.event_list.on('list_music_clear', list_music_clear)\n  global.lx.event_list.on('list_music_update_position', list_music_update_position)\n  return () => {\n    global.lx.event_list.off('list_data_overwrite', list_data_overwrite)\n    global.lx.event_list.off('list_create', list_create)\n    global.lx.event_list.off('list_remove', list_remove)\n    global.lx.event_list.off('list_update', list_update)\n    global.lx.event_list.off('list_update_position', list_update_position)\n    global.lx.event_list.off('list_music_overwrite', list_music_overwrite)\n    global.lx.event_list.off('list_music_add', list_music_add)\n    global.lx.event_list.off('list_music_move', list_music_move)\n    global.lx.event_list.off('list_music_remove', list_music_remove)\n    global.lx.event_list.off('list_music_update', list_music_update)\n    global.lx.event_list.off('list_music_clear', list_music_clear)\n    global.lx.event_list.off('list_music_update_position', list_music_update_position)\n  }\n}\n\nexport const handleRemoteListAction = async({ action, data }: LX.Sync.List.ActionList) => {\n  // console.log('handleRemoteListAction', action)\n\n  switch (action) {\n    case 'list_data_overwrite':\n      await global.lx.event_list.list_data_overwrite(data, true)\n      break\n    case 'list_create':\n      await global.lx.event_list.list_create(data.position, data.listInfos, true)\n      break\n    case 'list_remove':\n      await global.lx.event_list.list_remove(data, true)\n      break\n    case 'list_update':\n      await global.lx.event_list.list_update(data, true)\n      break\n    case 'list_update_position':\n      await global.lx.event_list.list_update_position(data.position, data.ids, true)\n      break\n    case 'list_music_add':\n      await global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)\n      break\n    case 'list_music_move':\n      await global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)\n      break\n    case 'list_music_remove':\n      await global.lx.event_list.list_music_remove(data.listId, data.ids, true)\n      break\n    case 'list_music_update':\n      await global.lx.event_list.list_music_update(data, true)\n      break\n    case 'list_music_update_position':\n      await global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)\n      break\n    case 'list_music_overwrite':\n      await global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)\n      break\n    case 'list_music_clear':\n      await global.lx.event_list.list_music_clear(data, true)\n      break\n    default:\n      throw new Error('unknown list sync action')\n  }\n}\n"
  },
  {
    "path": "src/main/modules/sync/log.ts",
    "content": "import { log as writeLog } from '@common/utils'\n\nexport default {\n  r_info(...params: any[]) {\n    writeLog.info(...params)\n  },\n  r_warn(...params: any[]) {\n    writeLog.warn(...params)\n  },\n  r_error(...params: any[]) {\n    writeLog.error(...params)\n  },\n  info(...params: any[]) {\n    // if (global.lx.isEnableSyncLog) writeLog.info(...params)\n    console.log(...params)\n  },\n  warn(...params: any[]) {\n    // if (global.lx.isEnableSyncLog) writeLog.warn(...params)\n    console.warn(...params)\n  },\n  error(...params: any[]) {\n    // if (global.lx.isEnableSyncLog) writeLog.error(...params)\n    console.warn(...params)\n  },\n}\n"
  },
  {
    "path": "src/main/modules/sync/migrate.ts",
    "content": "import { File } from '../../../common/constants_sync'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { exists } from './utils'\n\ninterface ServerKeyInfo {\n  clientId: string\n  key: string\n  deviceName: string\n  lastSyncDate?: number\n  snapshotKey?: string\n  lastConnectDate?: number\n  isMobile: boolean\n}\n\n\n// 迁移 v2 sync 数据\nexport default async(dataPath: string) => {\n  const syncDataPath = path.join(dataPath, 'sync')\n  // console.log(syncDataPath)\n  if (await exists(syncDataPath)) return\n  const oldInfoPath = path.join(dataPath, 'sync.json')\n  // console.log(oldInfoPath)\n  if (!await exists(oldInfoPath)) return\n  const serverSyncDataPath = path.join(dataPath, File.serverDataPath)\n  const clientSyncDataPath = path.join(dataPath, File.clientDataPath)\n\n  await fs.promises.mkdir(serverSyncDataPath, { recursive: true })\n  await fs.promises.mkdir(clientSyncDataPath, { recursive: true })\n  const info = JSON.parse((await fs.promises.readFile(oldInfoPath)).toString())\n\n\n  const serverInfoPath = path.join(serverSyncDataPath, File.serverInfoJSON)\n  const devicesInfoPath = path.join(serverSyncDataPath, File.userDevicesJSON)\n  const listDir = path.join(serverSyncDataPath, File.listDir)\n  await fs.promises.mkdir(listDir)\n\n\n  const snapshotInfo = info.snapshotInfo\n  delete info.snapshotInfo\n  snapshotInfo.clients = {}\n  for (const device of Object.values<ServerKeyInfo>(info.clients)) {\n    snapshotInfo.clients[device.clientId] = {\n      snapshotKey: device.snapshotKey,\n      lastSyncDate: device.lastSyncDate,\n    }\n    device.lastConnectDate = device.lastSyncDate\n    delete device.lastSyncDate\n    delete device.snapshotKey\n  }\n  const devicesInfo = {\n    userName: 'default',\n    clients: info.clients,\n  }\n  await fs.promises.writeFile(serverInfoPath, JSON.stringify({ serverId: info.serverId, version: 2 }))\n  await fs.promises.writeFile(devicesInfoPath, JSON.stringify(devicesInfo))\n  await fs.promises.writeFile(path.join(listDir, File.listSnapshotInfoJSON), JSON.stringify(snapshotInfo))\n\n  const snapshotPath = path.join(listDir, File.listSnapshotDir)\n  await fs.promises.mkdir(snapshotPath)\n  const snapshots = (await fs.promises.readdir(dataPath)).filter(name => name.startsWith('snapshot_'))\n  if (snapshots.length) {\n    for (const file of snapshots) {\n      await fs.promises.copyFile(path.join(dataPath, file), path.join(snapshotPath, file))\n    }\n  }\n\n\n  await fs.promises.writeFile(path.join(clientSyncDataPath, File.syncAuthKeysJSON), JSON.stringify(info.syncAuthKey))\n\n  for (const file of snapshots) {\n    await fs.promises.unlink(path.join(dataPath, file))\n  }\n  await fs.promises.unlink(oldInfoPath)\n}\n\n"
  },
  {
    "path": "src/main/modules/sync/server/index.ts",
    "content": "export {\n  startServer,\n  stopServer,\n  getStatus,\n  generateCode,\n  getDevices,\n  removeDevice,\n} from './server'\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/index.ts",
    "content": "export * as sync from './sync'\nexport { DislikeManage } from './manage'\n\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/manage.ts",
    "content": "import { type UserDataManage } from '../../user'\nimport { SnapshotDataManage } from './snapshotDataManage'\nimport { toMD5 } from '../../utils'\nimport { getLocalDislikeData } from '@main/modules/sync/dislikeEvent'\n\nexport class DislikeManage {\n  snapshotDataManage: SnapshotDataManage\n\n  constructor(userDataManage: UserDataManage) {\n    this.snapshotDataManage = new SnapshotDataManage(userDataManage)\n  }\n\n  createSnapshot = async() => {\n    const listData = await this.getDislikeRules()\n    const md5 = toMD5(listData.trim())\n    const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo()\n    console.log(md5, snapshotInfo.latest)\n    if (snapshotInfo.latest == md5) return md5\n    if (snapshotInfo.list.includes(md5)) {\n      snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1)\n    } else await this.snapshotDataManage.saveSnapshot(md5, listData)\n    if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest)\n    snapshotInfo.latest = md5\n    snapshotInfo.time = Date.now()\n    this.snapshotDataManage.saveSnapshotInfo(snapshotInfo)\n    return md5\n  }\n\n  getCurrentListInfoKey = async() => {\n    // const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo()\n    // if (snapshotInfo.latest) {\n    //   return snapshotInfo.latest\n    // }\n    // snapshotInfo.latest = toMD5((await this.getDislikeRules()).trim())\n    // this.snapshotDataManage.saveSnapshotInfo(snapshotInfo)\n    // return snapshotInfo.latest\n    return this.createSnapshot()\n  }\n\n  getDeviceCurrentSnapshotKey = async(clientId: string) => {\n    return this.snapshotDataManage.getDeviceCurrentSnapshotKey(clientId)\n  }\n\n  updateDeviceSnapshotKey = async(clientId: string, key: string) => {\n    await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key)\n  }\n\n  removeDevice = async(clientId: string) => {\n    this.snapshotDataManage.removeSnapshotInfo(clientId)\n  }\n\n  getDislikeRules = async() => {\n    return getLocalDislikeData()\n  }\n}\n\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/snapshotDataManage.ts",
    "content": "import { throttle } from '@common/utils/common'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport syncLog from '../../../log'\nimport { getUserConfig, type UserDataManage } from '../../user/data'\nimport { File } from '../../../../../../common/constants_sync'\nimport { checkAndCreateDirSync } from '../../utils'\n\n\ninterface SnapshotInfo {\n  latest: string | null\n  time: number\n  list: string[]\n  clients: Record<string, LX.Sync.Dislike.ListInfo>\n}\nexport class SnapshotDataManage {\n  userDataManage: UserDataManage\n  dislikeDir: string\n  snapshotDir: string\n  snapshotInfoFilePath: string\n  snapshotInfo: SnapshotInfo\n  clientSnapshotKeys: string[]\n  private readonly saveSnapshotInfoThrottle: () => void\n\n  isIncluedsDevice = (key: string) => {\n    return this.clientSnapshotKeys.includes(key)\n  }\n\n  clearOldSnapshot = async() => {\n    if (!this.snapshotInfo) return\n    const snapshotList = this.snapshotInfo.list.filter(key => !this.isIncluedsDevice(key))\n    // console.log(snapshotList.length, lx.config.maxSnapshotNum)\n    const userMaxSnapshotNum = getUserConfig(this.userDataManage.userName).maxSnapshotNum\n    let requiredSave = snapshotList.length > userMaxSnapshotNum\n    while (snapshotList.length > userMaxSnapshotNum) {\n      const name = snapshotList.pop()\n      if (name) {\n        await this.removeSnapshot(name)\n        this.snapshotInfo.list.splice(this.snapshotInfo.list.indexOf(name), 1)\n      } else break\n    }\n    if (requiredSave) this.saveSnapshotInfo(this.snapshotInfo)\n  }\n\n  updateDeviceSnapshotKey = async(clientId: string, key: string) => {\n    // console.log('updateDeviceSnapshotKey', key)\n    let client = this.snapshotInfo.clients[clientId]\n    if (!client) client = this.snapshotInfo.clients[clientId] = { snapshotKey: '', lastSyncDate: 0 }\n    if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1)\n    client.snapshotKey = key\n    client.lastSyncDate = Date.now()\n    this.clientSnapshotKeys.push(key)\n    this.saveSnapshotInfoThrottle()\n  }\n\n  getDeviceCurrentSnapshotKey = async(clientId: string) => {\n    // console.log('updateDeviceSnapshotKey', key)\n    const client = this.snapshotInfo.clients[clientId]\n    return client?.snapshotKey\n  }\n\n  getSnapshotInfo = async(): Promise<SnapshotInfo> => {\n    return this.snapshotInfo\n  }\n\n  saveSnapshotInfo = (info: SnapshotInfo) => {\n    this.snapshotInfo = info\n    this.saveSnapshotInfoThrottle()\n  }\n\n  removeSnapshotInfo = (clientId: string) => {\n    let client = this.snapshotInfo.clients[clientId]\n    if (!client) return\n    if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1)\n    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n    delete this.snapshotInfo.clients[clientId]\n    this.saveSnapshotInfoThrottle()\n  }\n\n  getSnapshot = async(name: string) => {\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    let listData: LX.Dislike.DislikeRules\n    try {\n      listData = (await fs.promises.readFile(filePath)).toString('utf-8')\n    } catch (err) {\n      syncLog.warn(err)\n      return null\n    }\n    return listData\n  }\n\n  saveSnapshot = async(name: string, data: string) => {\n    syncLog.info('saveSnapshot', this.userDataManage.userName, name)\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    try {\n      fs.writeFileSync(filePath, data)\n    } catch (err) {\n      syncLog.error(err)\n      throw err\n    }\n  }\n\n  removeSnapshot = async(name: string) => {\n    syncLog.info('removeSnapshot', this.userDataManage.userName, name)\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    try {\n      fs.unlinkSync(filePath)\n    } catch (err) {\n      syncLog.error(err)\n    }\n  }\n\n\n  constructor(userDataManage: UserDataManage) {\n    this.userDataManage = userDataManage\n\n    this.dislikeDir = path.join(userDataManage.userDir, File.dislikeDir)\n    checkAndCreateDirSync(this.dislikeDir)\n\n    this.snapshotDir = path.join(this.dislikeDir, File.dislikeSnapshotDir)\n    checkAndCreateDirSync(this.snapshotDir)\n\n    this.snapshotInfoFilePath = path.join(this.dislikeDir, File.dislikeSnapshotInfoJSON)\n    this.snapshotInfo = fs.existsSync(this.snapshotInfoFilePath)\n      ? JSON.parse(fs.readFileSync(this.snapshotInfoFilePath).toString())\n      : { latest: null, time: 0, list: [], clients: {} }\n\n    this.saveSnapshotInfoThrottle = throttle(() => {\n      fs.writeFile(this.snapshotInfoFilePath, JSON.stringify(this.snapshotInfo), 'utf8', (err) => {\n        if (err) console.error(err)\n        void this.clearOldSnapshot()\n      })\n    })\n\n    this.clientSnapshotKeys = Object.values(this.snapshotInfo.clients).map(device => device.snapshotKey).filter(k => k)\n  }\n}\n// type UserDataManages = Map<string, UserDataManage>\n\n// export const createUserDataManage = (user: LX.UserConfig) => {\n//   const manage = Object.create(userDataManage) as typeof userDataManage\n//   manage.userDir = user.dataPath\n// }\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/sync/handler.ts",
    "content": "// 这个文件导出的方法将暴露给客户端调用，第一个参数固定为当前 socket 对象\n// import { throttle } from '@common/utils/common'\n// import { sendSyncActionList } from '@main/modules/winMain'\n// import { SYNC_CLOSE_CODE } from '@/constants'\n// import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { getUserSpace } from '@main/modules/sync/server/user'\nimport { handleRemoteDislikeAction } from '@main/modules/sync/dislikeEvent'\n// import { encryptMsg } from '@/utils/tools'\n\n\nconst handler: LX.Sync.ServerSyncHandlerDislikeActions<LX.Sync.Server.Socket> = {\n  async onDislikeSyncAction(socket, action) {\n    if (!socket.moduleReadys.dislike) return\n    await handleRemoteDislikeAction(action)\n    const userSpace = getUserSpace(socket.userInfo.name)\n    const key = await userSpace.dislikeManage.createSnapshot()\n    userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key)\n    const currentUserName = socket.userInfo.name\n    const currentId = socket.keyInfo.clientId\n    socket.broadcast((client) => {\n      if (client.keyInfo.clientId == currentId || !client.moduleReadys?.dislike || client.userInfo.name != currentUserName) return\n      void client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => {\n        return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n      }).catch(err => {\n      // TODO send status\n        client.close(SYNC_CLOSE_CODE.failed)\n        // client.moduleReadys.dislike = false\n        console.log(err.message)\n      })\n    })\n  },\n}\n\nexport default handler\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/sync/index.ts",
    "content": "export { default as handler } from './handler'\nexport { sync } from './sync'\nexport * from './localEvent'\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/sync/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { registerDislikeActionEvent } from '../../../../dislikeEvent'\nimport { getUserSpace } from '../../../user'\n\n// let socket: LX.Sync.Server.Socket | null\nlet unregisterLocalListAction: (() => void) | null\n\n\nconst sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.Dislike.ActionList) => {\n  // console.log('sendListAction', action.action)\n  const userSpace = getUserSpace()\n  let key = ''\n  for (const client of wss.clients) {\n    if (!client.moduleReadys?.dislike) continue\n    // eslint-disable-next-line require-atomic-updates\n    if (!key) key = await userSpace.dislikeManage.createSnapshot()\n    void client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => {\n      return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n    }).catch(err => {\n      // TODO send status\n      client.close(SYNC_CLOSE_CODE.failed)\n      // client.moduleReadys.dislike = false\n      console.log(err.message)\n    })\n  }\n}\n\nexport const registerEvent = (wss: LX.Sync.Server.SocketServer) => {\n  // socket = _socket\n  // socket.onClose(() => {\n  //   unregisterLocalListAction?.()\n  //   unregisterLocalListAction = null\n  // })\n  unregisterEvent()\n  unregisterLocalListAction = registerDislikeActionEvent((action) => {\n    void sendListAction(wss, action)\n  })\n}\n\nexport const unregisterEvent = () => {\n  unregisterLocalListAction?.()\n  unregisterLocalListAction = null\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/sync/sync.ts",
    "content": "// import { SYNC_CLOSE_CODE } from '../../../../constants'\nimport { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'\nimport { getUserSpace } from '../../../user'\nimport { getLocalDislikeData, setLocalDislikeData } from '@main/modules/sync/dislikeEvent'\nimport { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { filterRules } from '../utils'\n// import { LIST_IDS } from '@common/constants'\n\n// type ListInfoType = LX.Dislike.UserListInfoFull | LX.Dislike.MyDefaultListInfoFull | LX.Dislike.MyLoveListInfoFull\n\n// let wss: LX.Sync.Server.SocketServer | null\nlet syncingId: string | null = null\nconst wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))\n\n\nconst getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Dislike.DislikeRules> => {\n  console.log('getRemoteListData')\n  return (await socket.remoteQueueDislike.dislike_sync_get_list_data()) ?? ''\n}\n\nconst getRemoteDataMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => {\n  return socket.remoteQueueDislike.dislike_sync_get_md5()\n}\n\n// const getLocalDislikeData  async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Dislike.ListData> => {\n//   return getUserSpace(socket.userInfo.name).dislikeManage.getListData()\n// }\nconst getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Dislike.SyncMode> => new Promise((resolve, reject) => {\n  const handleDisconnect = (err: Error) => {\n    sendCloseSelectMode()\n    removeSelectModeListener()\n    reject(err)\n  }\n  let removeEventClose = socket.onClose(handleDisconnect)\n  sendSelectMode(socket.keyInfo.deviceName, 'dislike', (mode) => {\n    if (mode == null) {\n      reject(new Error('cancel'))\n      return\n    }\n    resolve(mode)\n    removeSelectModeListener()\n    removeEventClose()\n  })\n})\n// const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Dislike.SyncMode> => {\n//   return socket.remoteQueueDislike.list_sync_get_sync_mode()\n// }\n\nconst finishedSync = async(socket: LX.Sync.Server.Socket) => {\n  await socket.remoteQueueDislike.dislike_sync_finished()\n}\n\n\nconst setLocalList = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules) => {\n  await setLocalDislikeData(listData)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  return userSpace.dislikeManage.createSnapshot()\n}\n\nconst overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules, key: string, excludeIds: string[] = []) => {\n  const action = { action: 'dislike_data_overwrite', data: listData } as const\n  const tasks: Array<Promise<void>> = []\n  const userSpace = getUserSpace(socket.userInfo.name)\n  socket.broadcast((client) => {\n    if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo?.name != socket.userInfo.name || !client.moduleReadys?.dislike) return\n    tasks.push(client.remoteQueueDislike.onDislikeSyncAction(action).then(async() => {\n      return userSpace.dislikeManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n    }).catch(err => {\n      // TODO send status\n      client.close(SYNC_CLOSE_CODE.failed)\n      // client.moduleReadys.list = false\n      console.log(err.message)\n    }))\n  })\n  if (!tasks.length) return\n  await Promise.all(tasks)\n}\nconst setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Dislike.DislikeRules, key: string): Promise<void> => {\n  await socket.remoteQueueDislike.dislike_sync_set_list_data(listData)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  await userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key)\n}\n\n\nconst mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Dislike.DislikeRules, targetListData: LX.Dislike.DislikeRules): LX.Dislike.DislikeRules => {\n  return Array.from(filterRules(sourceListData + '\\n' + targetListData)).join('\\n')\n}\n\nconst handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Dislike.DislikeRules, boolean, boolean]> => {\n  const mode: LX.Sync.Dislike.SyncMode = await getSyncMode(socket)\n\n  if (mode == 'cancel') throw new Error('cancel')\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()])\n  console.log('handleMergeListData', 'remoteListData, localListData')\n  let listData: LX.Dislike.DislikeRules\n  let requiredUpdateLocalListData = true\n  let requiredUpdateRemoteListData = true\n  switch (mode) {\n    case 'merge_local_remote':\n      listData = mergeList(socket, localListData, remoteListData)\n      break\n    case 'merge_remote_local':\n      listData = mergeList(socket, remoteListData, localListData)\n      break\n    case 'overwrite_local_remote':\n      listData = localListData\n      requiredUpdateLocalListData = false\n      break\n    case 'overwrite_remote_local':\n      listData = remoteListData\n      requiredUpdateRemoteListData = false\n      break\n    // case 'none': return null\n    // case 'cancel':\n    default: throw new Error('cancel')\n  }\n  return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData]\n}\n\nconst handleSyncList = async(socket: LX.Sync.Server.Socket) => {\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()])\n  console.log('handleSyncList', 'remoteListData, localListData')\n  console.log('localListData', localListData.length)\n  console.log('remoteListData', remoteListData.length)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  const clientId = socket.keyInfo.clientId\n  if (localListData.length) {\n    if (remoteListData.length) {\n      const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)\n      console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData)\n      let key\n      if (requiredUpdateLocalListData) {\n        key = await setLocalList(socket, mergedList)\n        await overwriteRemoteListData(socket, mergedList, key, [clientId])\n        if (!requiredUpdateRemoteListData) await userSpace.dislikeManage.updateDeviceSnapshotKey(clientId, key)\n      }\n      if (requiredUpdateRemoteListData) {\n        if (!key) key = await userSpace.dislikeManage.getCurrentListInfoKey()\n        await setRemotelList(socket, mergedList, key)\n      }\n    } else {\n      await setRemotelList(socket, localListData, await userSpace.dislikeManage.getCurrentListInfoKey())\n    }\n  } else {\n    let key: string\n    if (remoteListData.length) {\n      key = await setLocalList(socket, remoteListData)\n      await overwriteRemoteListData(socket, remoteListData, key, [clientId])\n    }\n    key ??= await userSpace.dislikeManage.getCurrentListInfoKey()\n    await userSpace.dislikeManage.updateDeviceSnapshotKey(clientId, key)\n  }\n}\n\nconst mergeDataFromSnapshot = (\n  sourceList: LX.Dislike.DislikeRules,\n  targetList: LX.Dislike.DislikeRules,\n  snapshotList: LX.Dislike.DislikeRules,\n): LX.Dislike.DislikeRules => {\n  const removedRules = new Set<string>()\n  const sourceRules = filterRules(sourceList)\n  const targetRules = filterRules(targetList)\n\n  if (snapshotList) {\n    const snapshotRules = filterRules(snapshotList)\n    for (const m of snapshotRules.values()) {\n      if (!sourceRules.has(m) || !targetRules.has(m)) removedRules.add(m)\n    }\n  }\n  return Array.from(new Set(Array.from([...sourceRules, ...targetRules]).filter((rule) => {\n    return !removedRules.has(rule)\n  }))).join('\\n')\n}\nconst checkListLatest = async(socket: LX.Sync.Server.Socket) => {\n  const remoteListMD5 = await getRemoteDataMD5(socket)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  const userCurrentListInfoKey = await userSpace.dislikeManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId)\n  const currentListInfoKey = await userSpace.dislikeManage.getCurrentListInfoKey()\n  const latest = remoteListMD5 == currentListInfoKey\n  if (latest && userCurrentListInfoKey != currentListInfoKey) await userSpace.dislikeManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, currentListInfoKey)\n  return latest\n}\n\nconst handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Dislike.DislikeRules) => {\n  if (await checkListLatest(socket)) return\n\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalDislikeData()])\n  const newDislikeData = mergeDataFromSnapshot(localListData, remoteListData, snapshot)\n\n  const key = await setLocalList(socket, newDislikeData)\n  const err = await setRemotelList(socket, newDislikeData, key).catch(err => err)\n  await overwriteRemoteListData(socket, newDislikeData, key, [socket.keyInfo.clientId])\n  if (err) throw err\n}\n\nconst syncDislike = async(socket: LX.Sync.Server.Socket) => {\n  // socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)\n  // console.log(socket.keyInfo)\n  if (!socket.feature.dislike) throw new Error('dislike feature options not available')\n  if (!socket.feature.dislike.skipSnapshot) {\n    const user = getUserSpace(socket.userInfo.name)\n    const userCurrentDislikeInfoKey = await user.dislikeManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId)\n    if (userCurrentDislikeInfoKey) {\n      const listData = await user.dislikeManage.snapshotDataManage.getSnapshot(userCurrentDislikeInfoKey)\n      if (listData) {\n        console.log('handleMergeDislikeDataFromSnapshot')\n        await handleMergeListDataFromSnapshot(socket, listData)\n        return\n      }\n    }\n  }\n  await handleSyncList(socket)\n}\n\nexport const sync = async(socket: LX.Sync.Server.Socket) => {\n  let disconnected = false\n  socket.onClose(() => {\n    disconnected = true\n    if (syncingId == socket.keyInfo.clientId) syncingId = null\n  })\n\n  while (true) {\n    if (disconnected) throw new Error('disconnected')\n    if (!syncingId) break\n    await wait()\n  }\n\n  syncingId = socket.keyInfo.clientId\n  await syncDislike(socket).then(async() => {\n    await finishedSync(socket)\n    socket.moduleReadys.dislike = true\n  }).finally(() => {\n    syncingId = null\n  })\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/dislike/utils.ts",
    "content": "import { SPLIT_CHAR } from '@common/constants'\n\n\nexport const filterRules = (rules: string) => {\n  const list: string[] = []\n  for (const item of rules.split('\\n')) {\n    if (!item) continue\n    let [name, singer] = item.split(SPLIT_CHAR.DISLIKE_NAME)\n    if (name) {\n      name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      if (singer) {\n        singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n        list.push(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n      } else {\n        list.push(name)\n      }\n    } else if (singer) {\n      singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n    }\n  }\n  return new Set(list)\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/index.ts",
    "content": "import { sync as listSync } from './list'\nimport { sync as dislikeSync } from './dislike'\n\nexport const callObj = Object.assign({},\n  listSync.handler,\n  dislikeSync.handler,\n)\n\nexport const modules = {\n  list: listSync,\n  dislike: dislikeSync,\n}\n\n\nexport { ListManage } from './list'\n\nexport { DislikeManage } from './dislike'\n\nexport const featureVersion = {\n  list: 1,\n  dislike: 1,\n} as const\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/index.ts",
    "content": "export * as sync from './sync'\nexport { ListManage } from './manage'\n\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/manage.ts",
    "content": "import { type UserDataManage } from '../../user'\nimport { SnapshotDataManage } from './snapshotDataManage'\nimport { toMD5 } from '../../utils'\nimport { getLocalListData } from '@main/modules/sync/listEvent'\n\nexport class ListManage {\n  snapshotDataManage: SnapshotDataManage\n\n  constructor(userDataManage: UserDataManage) {\n    this.snapshotDataManage = new SnapshotDataManage(userDataManage)\n  }\n\n  createSnapshot = async() => {\n    const listData = JSON.stringify(await this.getListData())\n    const md5 = toMD5(listData)\n    const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo()\n    console.log(md5, snapshotInfo.latest)\n    if (snapshotInfo.latest == md5) return md5\n    if (snapshotInfo.list.includes(md5)) {\n      snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1)\n    } else await this.snapshotDataManage.saveSnapshot(md5, listData)\n    if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest)\n    snapshotInfo.latest = md5\n    snapshotInfo.time = Date.now()\n    this.snapshotDataManage.saveSnapshotInfo(snapshotInfo)\n    return md5\n  }\n\n  getCurrentListInfoKey = async() => {\n    // const snapshotInfo = await this.snapshotDataManage.getSnapshotInfo()\n    // if (snapshotInfo.latest) {\n    //   return snapshotInfo.latest\n    // }\n    // snapshotInfo.latest = toMD5(JSON.stringify(await this.getListData()))\n    // this.snapshotDataManage.saveSnapshotInfo(snapshotInfo)\n    // return snapshotInfo.latest\n    return this.createSnapshot()\n  }\n\n  getDeviceCurrentSnapshotKey = async(clientId: string) => {\n    return this.snapshotDataManage.getDeviceCurrentSnapshotKey(clientId)\n  }\n\n  updateDeviceSnapshotKey = async(clientId: string, key: string) => {\n    await this.snapshotDataManage.updateDeviceSnapshotKey(clientId, key)\n  }\n\n  removeDevice = async(clientId: string) => {\n    this.snapshotDataManage.removeSnapshotInfo(clientId)\n  }\n\n  getListData = async(): Promise<LX.Sync.List.ListData> => {\n    return getLocalListData()\n  }\n}\n\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/snapshotDataManage.ts",
    "content": "import { throttle } from '@common/utils/common'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport syncLog from '../../../log'\nimport { getUserConfig, type UserDataManage } from '../../user/data'\nimport { File } from '../../../../../../common/constants_sync'\nimport { checkAndCreateDirSync } from '../../utils'\n\n\ninterface SnapshotInfo {\n  latest: string | null\n  time: number\n  list: string[]\n  clients: Record<string, LX.Sync.List.ListInfo>\n}\nexport class SnapshotDataManage {\n  userDataManage: UserDataManage\n  listDir: string\n  snapshotDir: string\n  snapshotInfoFilePath: string\n  snapshotInfo: SnapshotInfo\n  clientSnapshotKeys: string[]\n  private readonly saveSnapshotInfoThrottle: () => void\n\n  isIncluedsDevice = (key: string) => {\n    return this.clientSnapshotKeys.includes(key)\n  }\n\n  clearOldSnapshot = async() => {\n    if (!this.snapshotInfo) return\n    const snapshotList = this.snapshotInfo.list.filter(key => !this.isIncluedsDevice(key))\n    // console.log(snapshotList.length, lx.config.maxSnapshotNum)\n    const userMaxSnapshotNum = getUserConfig(this.userDataManage.userName).maxSnapshotNum\n    let requiredSave = snapshotList.length > userMaxSnapshotNum\n    while (snapshotList.length > userMaxSnapshotNum) {\n      const name = snapshotList.pop()\n      if (name) {\n        await this.removeSnapshot(name)\n        this.snapshotInfo.list.splice(this.snapshotInfo.list.indexOf(name), 1)\n      } else break\n    }\n    if (requiredSave) this.saveSnapshotInfo(this.snapshotInfo)\n  }\n\n  updateDeviceSnapshotKey = async(clientId: string, key: string) => {\n    // console.log('updateDeviceSnapshotKey', key)\n    let client = this.snapshotInfo.clients[clientId]\n    if (!client) client = this.snapshotInfo.clients[clientId] = { snapshotKey: '', lastSyncDate: 0 }\n    if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1)\n    client.snapshotKey = key\n    client.lastSyncDate = Date.now()\n    this.clientSnapshotKeys.push(key)\n    this.saveSnapshotInfoThrottle()\n  }\n\n  getDeviceCurrentSnapshotKey = async(clientId: string) => {\n    // console.log('updateDeviceSnapshotKey', key)\n    const client = this.snapshotInfo.clients[clientId]\n    return client?.snapshotKey\n  }\n\n  getSnapshotInfo = async(): Promise<SnapshotInfo> => {\n    return this.snapshotInfo\n  }\n\n  saveSnapshotInfo = (info: SnapshotInfo) => {\n    this.snapshotInfo = info\n    this.saveSnapshotInfoThrottle()\n  }\n\n  removeSnapshotInfo = (clientId: string) => {\n    let client = this.snapshotInfo.clients[clientId]\n    if (!client) return\n    if (client.snapshotKey) this.clientSnapshotKeys.splice(this.clientSnapshotKeys.indexOf(client.snapshotKey), 1)\n    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n    delete this.snapshotInfo.clients[clientId]\n    this.saveSnapshotInfoThrottle()\n  }\n\n  getSnapshot = async(name: string) => {\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    let listData: LX.Sync.List.ListData\n    try {\n      listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8'))\n    } catch (err) {\n      syncLog.warn(err)\n      return null\n    }\n    return listData\n  }\n\n  saveSnapshot = async(name: string, data: string) => {\n    syncLog.info('saveSnapshot', this.userDataManage.userName, name)\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    try {\n      await fs.promises.writeFile(filePath, data)\n    } catch (err) {\n      syncLog.error(err)\n      throw err\n    }\n  }\n\n  removeSnapshot = async(name: string) => {\n    syncLog.info('removeSnapshot', this.userDataManage.userName, name)\n    const filePath = path.join(this.snapshotDir, `snapshot_${name}`)\n    try {\n      await fs.promises.unlink(filePath)\n    } catch (err) {\n      syncLog.error(err)\n    }\n  }\n\n\n  constructor(userDataManage: UserDataManage) {\n    this.userDataManage = userDataManage\n\n    this.listDir = path.join(userDataManage.userDir, File.listDir)\n    checkAndCreateDirSync(this.listDir)\n\n    this.snapshotDir = path.join(this.listDir, File.listSnapshotDir)\n    checkAndCreateDirSync(this.snapshotDir)\n\n    this.snapshotInfoFilePath = path.join(this.listDir, File.listSnapshotInfoJSON)\n    this.snapshotInfo = fs.existsSync(this.snapshotInfoFilePath)\n      ? JSON.parse(fs.readFileSync(this.snapshotInfoFilePath).toString())\n      : { latest: null, time: 0, list: [], clients: {} }\n\n    this.saveSnapshotInfoThrottle = throttle(() => {\n      fs.writeFile(this.snapshotInfoFilePath, JSON.stringify(this.snapshotInfo), 'utf8', (err) => {\n        if (err) console.error(err)\n        void this.clearOldSnapshot()\n      })\n    })\n\n    this.clientSnapshotKeys = Object.values(this.snapshotInfo.clients).map(device => device.snapshotKey).filter(k => k)\n  }\n}\n// type UserDataManages = Map<string, UserDataManage>\n\n// export const createUserDataManage = (user: LX.UserConfig) => {\n//   const manage = Object.create(userDataManage) as typeof userDataManage\n//   manage.userDir = user.dataPath\n// }\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/sync/handler.ts",
    "content": "// 这个文件导出的方法将暴露给客户端调用，第一个参数固定为当前 socket 对象\n// import { throttle } from '@common/utils/common'\n// import { sendSyncActionList } from '@main/modules/winMain'\n// import { SYNC_CLOSE_CODE } from '@/constants'\n// import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { getUserSpace } from '@main/modules/sync/server/user'\nimport { handleRemoteListAction } from '@main/modules/sync/listEvent'\n// import { encryptMsg } from '@/utils/tools'\n\n// let wss: LX.SocketServer | null\n// let removeListener: (() => void) | null\n\n// type listAction = 'list:action'\n\n// const registerListActionEvent = () => {\n//   const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_data_overwrite', data: listData })\n//   }\n//   const list_create = async(position: number, listInfos: LX.List.UserListInfo[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_create', data: { position, listInfos } })\n//   }\n//   const list_remove = async(ids: string[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_remove', data: ids })\n//   }\n//   const list_update = async(lists: LX.List.UserListInfo[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_update', data: lists })\n//   }\n//   const list_update_position = async(position: number, ids: string[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_update_position', data: { position, ids } })\n//   }\n//   const list_music_overwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_overwrite', data: { listId, musicInfos } })\n//   }\n//   const list_music_add = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_add', data: { id, musicInfos, addMusicLocationType } })\n//   }\n//   const list_music_move = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType, isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_move', data: { fromId, toId, musicInfos, addMusicLocationType } })\n//   }\n//   const list_music_remove = async(listId: string, ids: string[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_remove', data: { listId, ids } })\n//   }\n//   const list_music_update = async(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_update', data: musicInfos })\n//   }\n//   const list_music_clear = async(ids: string[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_clear', data: ids })\n//   }\n//   const list_music_update_position = async(listId: string, position: number, ids: string[], isRemote: boolean = false) => {\n//     if (isRemote) return\n//     await sendListAction({ action: 'list_music_update_position', data: { listId, position, ids } })\n//   }\n//   global.event_list.on('list_data_overwrite', list_data_overwrite)\n//   global.event_list.on('list_create', list_create)\n//   global.event_list.on('list_remove', list_remove)\n//   global.event_list.on('list_update', list_update)\n//   global.event_list.on('list_update_position', list_update_position)\n//   global.event_list.on('list_music_overwrite', list_music_overwrite)\n//   global.event_list.on('list_music_add', list_music_add)\n//   global.event_list.on('list_music_move', list_music_move)\n//   global.event_list.on('list_music_remove', list_music_remove)\n//   global.event_list.on('list_music_update', list_music_update)\n//   global.event_list.on('list_music_clear', list_music_clear)\n//   global.event_list.on('list_music_update_position', list_music_update_position)\n//   return () => {\n//     global.event_list.off('list_data_overwrite', list_data_overwrite)\n//     global.event_list.off('list_create', list_create)\n//     global.event_list.off('list_remove', list_remove)\n//     global.event_list.off('list_update', list_update)\n//     global.event_list.off('list_update_position', list_update_position)\n//     global.event_list.off('list_music_overwrite', list_music_overwrite)\n//     global.event_list.off('list_music_add', list_music_add)\n//     global.event_list.off('list_music_move', list_music_move)\n//     global.event_list.off('list_music_remove', list_music_remove)\n//     global.event_list.off('list_music_update', list_music_update)\n//     global.event_list.off('list_music_clear', list_music_clear)\n//     global.event_list.off('list_music_update_position', list_music_update_position)\n//   }\n// }\n\n// const addMusic = (orderId, callback) => {\n//   // ...\n// }\n\n// const broadcast = async(socket: LX.Socket, key: string, data: any, excludeIds: string[] = []) => {\n//   if (!wss) return\n//   const dataStr = JSON.stringify({ action: 'list:sync:action', data })\n//   const userSpace = getUserSpace(socket.userInfo.name)\n//   for (const client of wss.clients) {\n//     if (excludeIds.includes(client.keyInfo.clientId) || !client.isReady || client.userInfo.name != socket.userInfo.name) continue\n//     client.send(encryptMsg(client.keyInfo, dataStr), (err) => {\n//       if (err) {\n//         client.close(SYNC_CLOSE_CODE.failed)\n//         return\n//       }\n//       userSpace.dataManage.updateDeviceSnapshotKey(client.keyInfo, key)\n//     })\n//   }\n// }\n\n// export const sendListAction = async(action: LX.Sync.List.ActionList) => {\n//   console.log('sendListAction', action.action)\n//   // io.sockets\n//   await broadcast('list:sync:action', action)\n// }\n\n// export const registerListHandler = (_wss: LX.SocketServer, socket: LX.Socket) => {\n//   if (!wss) {\n//     wss = _wss\n//     // removeListener = registerListActionEvent()\n//   }\n\n//   const userSpace = getUserSpace(socket.userInfo.name)\n//   socket.onRemoteEvent('list:sync:action', (action) => {\n//     if (!socket.isReady) return\n//     // console.log(msg)\n//     void handleListAction(socket.userInfo.name, action).then(key => {\n//       if (!key) return\n//       console.log(key)\n//       userSpace.dataManage.updateDeviceSnapshotKey(socket.keyInfo, key)\n//       void broadcast(socket, key, action, [socket.keyInfo.clientId])\n//     })\n//     // socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })\n//   })\n\n//   // socket.on('list:add', addMusic)\n// }\n// export const unregisterListHandler = () => {\n//   wss = null\n\n//   // if (removeListener) {\n//   //   removeListener()\n//   //   removeListener = null\n//   // }\n// }\n\nconst handler: LX.Sync.ServerSyncHandlerListActions<LX.Sync.Server.Socket> = {\n  async onListSyncAction(socket, action) {\n    if (!socket.moduleReadys.list) return\n    await handleRemoteListAction(action)\n    const userSpace = getUserSpace(socket.userInfo.name)\n    const key = await userSpace.listManage.createSnapshot()\n    userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key)\n    const currentUserName = socket.userInfo.name\n    const currentId = socket.keyInfo.clientId\n    socket.broadcast((client) => {\n      if (client.keyInfo.clientId == currentId || !client.moduleReadys?.list || client.userInfo.name != currentUserName) return\n      void client.remoteQueueList.onListSyncAction(action).then(async() => {\n        return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n      }).catch(err => {\n        // TODO send status\n        client.close(SYNC_CLOSE_CODE.failed)\n        // client.moduleReadys.list = false\n        console.log(err.message)\n      })\n    })\n  },\n}\n\nexport default handler\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/sync/index.ts",
    "content": "export { default as handler } from './handler'\nexport { sync } from './sync'\nexport * from './localEvent'\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/sync/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@common/constants_sync'\nimport { registerListActionEvent } from '../../../../listEvent'\nimport { getUserSpace } from '../../../user'\n\n// let socket: LX.Sync.Server.Socket | null\nlet unregisterLocalListAction: (() => void) | null\n\n\nconst sendListAction = async(wss: LX.Sync.Server.SocketServer, action: LX.Sync.List.ActionList) => {\n  // console.log('sendListAction', action.action)\n  const userSpace = getUserSpace()\n  let key = ''\n  for (const client of wss.clients) {\n    if (!client.moduleReadys?.list) continue\n    // eslint-disable-next-line require-atomic-updates\n    if (!key) key = await userSpace.listManage.createSnapshot()\n    void client.remoteQueueList.onListSyncAction(action).then(async() => {\n      return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n    }).catch(err => {\n      // TODO send status\n      client.close(SYNC_CLOSE_CODE.failed)\n      // client.moduleReadys.list = false\n      console.log(err.message)\n    })\n  }\n}\n\nexport const registerEvent = (wss: LX.Sync.Server.SocketServer) => {\n  // socket = _socket\n  // socket.onClose(() => {\n  //   unregisterLocalListAction?.()\n  //   unregisterLocalListAction = null\n  // })\n  unregisterEvent()\n  unregisterLocalListAction = registerListActionEvent((action) => {\n    void sendListAction(wss, action)\n  })\n}\n\nexport const unregisterEvent = () => {\n  unregisterLocalListAction?.()\n  unregisterLocalListAction = null\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/modules/list/sync/sync.ts",
    "content": "// import { SYNC_CLOSE_CODE } from '../../../../constants'\nimport { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'\nimport { getUserSpace, getUserConfig } from '../../../user'\nimport { buildUserListInfoFull, getLocalListData, setLocalListData } from '@main/modules/sync/listEvent'\nimport { SYNC_CLOSE_CODE } from '@common/constants_sync'\n// import { LIST_IDS } from '@common/constants'\n\n// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull\n\n// let wss: LX.Sync.Server.SocketServer | null\nlet syncingId: string | null = null\nconst wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))\n\nconst patchListData = (listData: Partial<LX.Sync.List.ListData>): LX.Sync.List.ListData => {\n  return Object.assign({\n    defaultList: [],\n    loveList: [],\n    userList: [],\n  }, listData)\n}\n\nconst getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.ListData> => {\n  console.log('getRemoteListData')\n  return patchListData(await socket.remoteQueueList.list_sync_get_list_data())\n}\n\nconst getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => {\n  return socket.remoteQueueList.list_sync_get_md5()\n}\n\n// const getLocalListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.ListData> => {\n//   return getUserSpace(socket.userInfo.name).listManage.getListData()\n// }\nconst getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.SyncMode> => new Promise((resolve, reject) => {\n  const handleDisconnect = (err: Error) => {\n    sendCloseSelectMode()\n    removeSelectModeListener()\n    reject(err)\n  }\n  let removeEventClose = socket.onClose(handleDisconnect)\n  sendSelectMode(socket.keyInfo.deviceName, 'list', (mode) => {\n    if (mode == null) {\n      reject(new Error('cancel'))\n      return\n    }\n    resolve(mode)\n    removeSelectModeListener()\n    removeEventClose()\n  })\n})\n// const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.List.SyncMode> => {\n//   return socket.remoteQueueList.list_sync_get_sync_mode()\n// }\n\nconst finishedSync = async(socket: LX.Sync.Server.Socket) => {\n  await socket.remoteQueueList.list_sync_finished()\n}\n\n\nconst setLocalList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData) => {\n  await setLocalListData(listData)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  return userSpace.listManage.createSnapshot()\n}\n\nconst overwriteRemoteListData = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData, key: string, excludeIds: string[] = []) => {\n  const action = { action: 'list_data_overwrite', data: listData } as const\n  const tasks: Array<Promise<void>> = []\n  const userSpace = getUserSpace(socket.userInfo.name)\n  socket.broadcast((client) => {\n    if (excludeIds.includes(client.keyInfo.clientId) || client.userInfo.name != socket.userInfo.name || !client.moduleReadys?.list) return\n    tasks.push(client.remoteQueueList.onListSyncAction(action).then(async() => {\n      return userSpace.listManage.updateDeviceSnapshotKey(client.keyInfo.clientId, key)\n    }).catch(err => {\n      // TODO send status\n      client.close(SYNC_CLOSE_CODE.failed)\n      // client.moduleReadys.list = false\n      console.log(err.message)\n    }))\n  })\n  if (!tasks.length) return\n  await Promise.all(tasks)\n}\nconst setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.List.ListData, key: string): Promise<void> => {\n  await socket.remoteQueueList.list_sync_set_list_data(listData)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, key)\n}\n\ntype UserDataObj = Map<string, LX.List.UserListInfoFull>\nconst createUserListDataObj = (listData: LX.Sync.List.ListData): UserDataObj => {\n  const userListDataObj: UserDataObj = new Map()\n  for (const list of listData.userList) userListDataObj.set(list.id, list)\n  return userListDataObj\n}\n\nconst handleMergeList = (\n  sourceList: LX.Music.MusicInfo[],\n  targetList: LX.Music.MusicInfo[],\n  addMusicLocationType: LX.AddMusicLocationType,\n): LX.Music.MusicInfo[] => {\n  let newList\n  switch (addMusicLocationType) {\n    case 'top':\n      newList = [...targetList, ...sourceList]\n      break\n    case 'bottom':\n    default:\n      newList = [...sourceList, ...targetList]\n      break\n  }\n  const map = new Map<string | number, LX.Music.MusicInfo>()\n  const ids: Array<string | number> = []\n  switch (addMusicLocationType) {\n    case 'top':\n      newList = [...targetList, ...sourceList]\n      for (let i = newList.length - 1; i > -1; i--) {\n        const item = newList[i]\n        if (map.has(item.id)) continue\n        ids.unshift(item.id)\n        map.set(item.id, item)\n      }\n      break\n    case 'bottom':\n    default:\n      newList = [...sourceList, ...targetList]\n      for (const item of newList) {\n        if (map.has(item.id)) continue\n        ids.push(item.id)\n        map.set(item.id, item)\n      }\n      break\n  }\n  return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]\n}\nconst mergeList = (socket: LX.Sync.Server.Socket, sourceListData: LX.Sync.List.ListData, targetListData: LX.Sync.List.ListData): LX.Sync.List.ListData => {\n  const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType']\n  const newListData: LX.Sync.List.ListData = {\n    defaultList: [],\n    loveList: [],\n    userList: [],\n  }\n  newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)\n  newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)\n\n  const userListDataObj = createUserListDataObj(sourceListData)\n  newListData.userList = [...sourceListData.userList]\n\n  targetListData.userList.forEach((list, index) => {\n    const targetUpdateTime = list?.locationUpdateTime ?? 0\n    const sourceList = userListDataObj.get(list.id)\n    if (sourceList) {\n      sourceList.list = handleMergeList(sourceList.list, list.list, addMusicLocationType)\n\n      const sourceUpdateTime = sourceList?.locationUpdateTime ?? 0\n      if (targetUpdateTime >= sourceUpdateTime) return\n      // 调整位置\n      const [newList] = newListData.userList.splice(newListData.userList.findIndex(l => l.id == list.id), 1)\n      newList.locationUpdateTime = targetUpdateTime\n      newListData.userList.splice(index, 0, newList)\n    } else {\n      if (targetUpdateTime) {\n        newListData.userList.splice(index, 0, list)\n      } else {\n        newListData.userList.push(list)\n      }\n    }\n  })\n\n  return newListData\n}\nconst overwriteList = (sourceListData: LX.Sync.List.ListData, targetListData: LX.Sync.List.ListData): LX.Sync.List.ListData => {\n  const newListData: LX.Sync.List.ListData = {\n    defaultList: [],\n    loveList: [],\n    userList: [],\n  }\n  newListData.defaultList = sourceListData.defaultList\n  newListData.loveList = sourceListData.loveList\n\n  const userListDataObj = createUserListDataObj(sourceListData)\n  newListData.userList = [...sourceListData.userList]\n\n  targetListData.userList.forEach((list, index) => {\n    if (userListDataObj.has(list.id)) return\n    if (list?.locationUpdateTime) {\n      newListData.userList.splice(index, 0, list)\n    } else {\n      newListData.userList.push(list)\n    }\n  })\n\n  return newListData\n}\n\nconst handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.List.ListData, boolean, boolean]> => {\n  const mode: LX.Sync.List.SyncMode = await getSyncMode(socket)\n\n  if (mode == 'cancel') throw new Error('cancel')\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])\n  console.log('handleMergeListData', 'remoteListData, localListData')\n  let listData: LX.Sync.List.ListData\n  let requiredUpdateLocalListData = true\n  let requiredUpdateRemoteListData = true\n  switch (mode) {\n    case 'merge_local_remote':\n      listData = mergeList(socket, localListData, remoteListData)\n      break\n    case 'merge_remote_local':\n      listData = mergeList(socket, remoteListData, localListData)\n      break\n    case 'overwrite_local_remote':\n      listData = overwriteList(localListData, remoteListData)\n      break\n    case 'overwrite_remote_local':\n      listData = overwriteList(remoteListData, localListData)\n      break\n    case 'overwrite_local_remote_full':\n      listData = localListData\n      requiredUpdateLocalListData = false\n      break\n    case 'overwrite_remote_local_full':\n      listData = remoteListData\n      requiredUpdateRemoteListData = false\n      break\n    // case 'none': return null\n    // case 'cancel':\n    default: throw new Error('cancel')\n  }\n  return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData]\n}\n\nconst handleSyncList = async(socket: LX.Sync.Server.Socket) => {\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])\n  console.log('handleSyncList', 'remoteListData, localListData')\n  console.log('localListData', localListData.defaultList.length || localListData.loveList.length || localListData.userList.length)\n  console.log('remoteListData', remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  const clientId = socket.keyInfo.clientId\n  if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {\n    if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {\n      const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)\n      console.log('handleMergeListData', 'mergedList', requiredUpdateLocalListData, requiredUpdateRemoteListData)\n      let key\n      if (requiredUpdateLocalListData) {\n        key = await setLocalList(socket, mergedList)\n        await overwriteRemoteListData(socket, mergedList, key, [clientId])\n        if (!requiredUpdateRemoteListData) await userSpace.listManage.updateDeviceSnapshotKey(clientId, key)\n      }\n      if (requiredUpdateRemoteListData) {\n        if (!key) key = await userSpace.listManage.getCurrentListInfoKey()\n        await setRemotelList(socket, mergedList, key)\n      }\n    } else {\n      await setRemotelList(socket, localListData, await userSpace.listManage.getCurrentListInfoKey())\n    }\n  } else {\n    let key: string\n    if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {\n      key = await setLocalList(socket, remoteListData)\n      await overwriteRemoteListData(socket, remoteListData, key, [clientId])\n    }\n    key ??= await userSpace.listManage.getCurrentListInfoKey()\n    await userSpace.listManage.updateDeviceSnapshotKey(clientId, key)\n  }\n}\n\nconst mergeListDataFromSnapshot = (\n  sourceList: LX.Music.MusicInfo[],\n  targetList: LX.Music.MusicInfo[],\n  snapshotList: LX.Music.MusicInfo[],\n  addMusicLocationType: LX.AddMusicLocationType,\n): LX.Music.MusicInfo[] => {\n  const removedListIds = new Set<string | number>()\n  const sourceListItemIds = new Set<string | number>()\n  const targetListItemIds = new Set<string | number>()\n  for (const m of sourceList) sourceListItemIds.add(m.id)\n  for (const m of targetList) targetListItemIds.add(m.id)\n  if (snapshotList) {\n    for (const m of snapshotList) {\n      if (!sourceListItemIds.has(m.id) || !targetListItemIds.has(m.id)) removedListIds.add(m.id)\n    }\n  }\n\n  let newList\n  const map = new Map<string | number, LX.Music.MusicInfo>()\n  const ids = []\n  switch (addMusicLocationType) {\n    case 'top':\n      newList = [...targetList, ...sourceList]\n      for (let i = newList.length - 1; i > -1; i--) {\n        const item = newList[i]\n        if (map.has(item.id) || removedListIds.has(item.id)) continue\n        ids.unshift(item.id)\n        map.set(item.id, item)\n      }\n      break\n    case 'bottom':\n    default:\n      newList = [...sourceList, ...targetList]\n      for (const item of newList) {\n        if (map.has(item.id) || removedListIds.has(item.id)) continue\n        ids.push(item.id)\n        map.set(item.id, item)\n      }\n      break\n  }\n  return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]\n}\nconst checkListLatest = async(socket: LX.Sync.Server.Socket) => {\n  const remoteListMD5 = await getRemoteListMD5(socket)\n  const userSpace = getUserSpace(socket.userInfo.name)\n  const userCurrentListInfoKey = await userSpace.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId)\n  const currentListInfoKey = await userSpace.listManage.getCurrentListInfoKey()\n  const latest = remoteListMD5 == currentListInfoKey\n  if (latest && userCurrentListInfoKey != currentListInfoKey) await userSpace.listManage.updateDeviceSnapshotKey(socket.keyInfo.clientId, currentListInfoKey)\n  return latest\n}\nconst selectData = <T>(snapshot: T | null, local: T, remote: T): T => {\n  return snapshot == local\n    ? remote\n    // ? (snapshot == remote ? snapshot as T : remote)\n    : local\n}\nconst handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.List.ListData) => {\n  if (await checkListLatest(socket)) return\n\n  const addMusicLocationType = getUserConfig(socket.userInfo.name)['list.addMusicLocationType']\n  const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])\n  const newListData: LX.Sync.List.ListData = {\n    defaultList: [],\n    loveList: [],\n    userList: [],\n  }\n  newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)\n  newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)\n  const localUserListData = createUserListDataObj(localListData)\n  const remoteUserListData = createUserListDataObj(remoteListData)\n  const snapshotUserListData = createUserListDataObj(snapshot)\n  const removedListIds = new Set<string | number>()\n  const localUserListIds = new Set<string | number>()\n  const remoteUserListIds = new Set<string | number>()\n\n  for (const l of localListData.userList) localUserListIds.add(l.id)\n  for (const l of remoteListData.userList) remoteUserListIds.add(l.id)\n\n  for (const l of snapshot.userList) {\n    if (!localUserListIds.has(l.id) || !remoteUserListIds.has(l.id)) removedListIds.add(l.id)\n  }\n\n  let newUserList: LX.List.UserListInfoFull[] = []\n  for (const list of localListData.userList) {\n    if (removedListIds.has(list.id)) continue\n    const remoteList = remoteUserListData.get(list.id)\n    let newList: LX.List.UserListInfoFull\n    if (remoteList) {\n      const snapshotList = snapshotUserListData.get(list.id) ?? { name: null, source: null, sourceListId: null, list: [] }\n      newList = buildUserListInfoFull({\n        id: list.id,\n        name: selectData(snapshotList.name, list.name, remoteList.name),\n        source: selectData(snapshotList.source, list.source, remoteList.source),\n        sourceListId: selectData(snapshotList.sourceListId, list.sourceListId, remoteList.sourceListId),\n        locationUpdateTime: list.locationUpdateTime,\n        list: mergeListDataFromSnapshot(list.list, remoteList.list, snapshotList.list, addMusicLocationType),\n      })\n    } else {\n      newList = { ...list }\n    }\n    newUserList.push(newList)\n  }\n\n  remoteListData.userList.forEach((list, index) => {\n    if (removedListIds.has(list.id)) return\n    const remoteUpdateTime = list?.locationUpdateTime ?? 0\n    if (localUserListData.has(list.id)) {\n      const localUpdateTime = localUserListData.get(list.id)?.locationUpdateTime ?? 0\n      if (localUpdateTime >= remoteUpdateTime) return\n      // 调整位置\n      const [newList] = newUserList.splice(newUserList.findIndex(l => l.id == list.id), 1)\n      newList.locationUpdateTime = localUpdateTime\n      newUserList.splice(index, 0, newList)\n    } else {\n      if (remoteUpdateTime) {\n        newUserList.splice(index, 0, { ...list })\n      } else {\n        newUserList.push({ ...list })\n      }\n    }\n  })\n\n  newListData.userList = newUserList\n  const key = await setLocalList(socket, newListData)\n  const err = await setRemotelList(socket, newListData, key).catch(err => err)\n  await overwriteRemoteListData(socket, newListData, key, [socket.keyInfo.clientId])\n  if (err) throw err\n}\n\nconst syncList = async(socket: LX.Sync.Server.Socket) => {\n  // socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)\n  // console.log(socket.keyInfo)\n  if (!socket.feature.list) throw new Error('list feature options not available')\n  if (!socket.feature.list.skipSnapshot) {\n    const user = getUserSpace(socket.userInfo.name)\n    const userCurrentListInfoKey = await user.listManage.getDeviceCurrentSnapshotKey(socket.keyInfo.clientId)\n    if (userCurrentListInfoKey) {\n      const listData = await user.listManage.snapshotDataManage.getSnapshot(userCurrentListInfoKey)\n      if (listData) {\n        console.log('handleMergeListDataFromSnapshot')\n        await handleMergeListDataFromSnapshot(socket, listData)\n        return\n      }\n    }\n  }\n  await handleSyncList(socket)\n}\n\n// export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {\n//   if (!wss) {\n//     wss = _wss\n//     _wss.addListener('close', () => {\n//       wss = null\n//     })\n//   }\n\n//   let disconnected = false\n//   socket.onClose(() => {\n//     disconnected = true\n//     if (syncingId == socket.keyInfo.clientId) syncingId = null\n//   })\n\n//   while (true) {\n//     if (disconnected) throw new Error('disconnected')\n//     if (!syncingId) break\n//     await wait()\n//   }\n\n//   syncingId = socket.keyInfo.clientId\n//   await syncList(socket).then(async() => {\n//     return finishedSync(socket)\n//   }).finally(() => {\n//     syncingId = null\n//   })\n// }\n\n// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {\n//   const filePath = getSnapshotFilePath(keyInfo)\n//   await fsPromises.unlink(filePath)\n// }\n\nexport const sync = async(socket: LX.Sync.Server.Socket) => {\n  let disconnected = false\n  socket.onClose(() => {\n    disconnected = true\n    if (syncingId == socket.keyInfo.clientId) syncingId = null\n  })\n\n  while (true) {\n    if (disconnected) throw new Error('disconnected')\n    if (!syncingId) break\n    await wait()\n  }\n\n  syncingId = socket.keyInfo.clientId\n  await syncList(socket).then(async() => {\n    await finishedSync(socket)\n    socket.moduleReadys.list = true\n  }).finally(() => {\n    syncingId = null\n  })\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/server/auth.ts",
    "content": "import type http from 'http'\nimport {\n  aesEncrypt,\n  aesDecrypt,\n  rsaEncrypt,\n  getIP,\n} from '../utils/tools'\nimport querystring from 'node:querystring'\nimport { getUserSpace, createClientKeyInfo } from '../user'\nimport { toMD5 } from '../utils'\nimport { getComputerName } from '../../utils'\nimport { SYNC_CODE } from '@common/constants_sync'\n\nconst requestIps = new Map<string, number>()\n\nconst getAvailableIP = (req: http.IncomingMessage) => {\n  let ip = getIP(req)\n  return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null\n}\n\nconst verifyByKey = (encryptMsg: string, userId: string) => {\n  const userSpace = getUserSpace()\n  const keyInfo = userSpace.dataManage.getClientKeyInfo(userId)\n  if (!keyInfo) return null\n  let text\n  try {\n    text = aesDecrypt(encryptMsg, keyInfo.key)\n  } catch (err) {\n    return null\n  }\n  // console.log(text)\n  if (text.startsWith(SYNC_CODE.authMsg)) {\n    const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'\n    if (deviceName != keyInfo.deviceName) {\n      keyInfo.deviceName = deviceName\n      userSpace.dataManage.saveClientKeyInfo(keyInfo)\n    }\n    return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)\n  }\n  return null\n}\n\nconst verifyByCode = (encryptMsg: string, password: string) => {\n  let key = toMD5(password).substring(0, 16)\n  // const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')\n  key = Buffer.from(key).toString('base64')\n  // console.log(req.headers.m, authCode, key)\n  let text\n  try {\n    text = aesDecrypt(encryptMsg, key)\n  } catch {\n    return null\n  }\n  // console.log(text)\n  if (text.startsWith(SYNC_CODE.authMsg)) {\n    const data = text.split('\\n')\n    const publicKey = `-----BEGIN PUBLIC KEY-----\\n${data[1]}\\n-----END PUBLIC KEY-----`\n    const deviceName = data[2] || 'Unknown'\n    const isMobile = data[3] == 'lx_music_mobile'\n    const keyInfo = createClientKeyInfo(deviceName, isMobile)\n    const userSpace = getUserSpace()\n    userSpace.dataManage.saveClientKeyInfo(keyInfo)\n    return rsaEncrypt(Buffer.from(JSON.stringify({\n      clientId: keyInfo.clientId,\n      key: keyInfo.key,\n      serverName: getComputerName(),\n    })), publicKey)\n  }\n  return null\n}\n\nexport const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {\n  let code = 401\n  let msg: string = SYNC_CODE.msgAuthFailed\n\n  let ip = getAvailableIP(req)\n  if (ip) {\n    if (typeof req.headers.m == 'string' && req.headers.m) {\n      const userId = req.headers.i\n      const _msg = typeof userId == 'string' && userId\n        ? verifyByKey(req.headers.m, userId)\n        : verifyByCode(req.headers.m, password)\n      if (_msg != null) {\n        msg = _msg\n        code = 200\n      }\n    }\n\n    if (code != 200) {\n      const num = requestIps.get(ip) ?? 0\n      // if (num > 20) return\n      requestIps.set(ip, num + 1)\n    }\n  } else {\n    code = 403\n    msg = SYNC_CODE.msgBlockedIp\n  }\n  // console.log(req.headers)\n\n  res.writeHead(code)\n  res.end(msg)\n}\n\nconst verifyConnection = (encryptMsg: string, userId: string) => {\n  const userSpace = getUserSpace()\n  const keyInfo = userSpace.dataManage.getClientKeyInfo(userId)\n  if (!keyInfo) return false\n  let text\n  try {\n    text = aesDecrypt(encryptMsg, keyInfo.key)\n  } catch (err) {\n    return false\n  }\n  // console.log(text)\n  return text == SYNC_CODE.msgConnect\n}\nexport const authConnect = async(req: http.IncomingMessage) => {\n  let ip = getAvailableIP(req)\n  if (ip) {\n    const query = querystring.parse((req.url!).split('?')[1])\n    const i = query.i\n    const t = query.t\n    if (typeof i == 'string' && typeof t == 'string' && verifyConnection(t, i)) return\n\n    const num = requestIps.get(ip) ?? 0\n    requestIps.set(ip, num + 1)\n  }\n  throw new Error('failed')\n}\n\n"
  },
  {
    "path": "src/main/modules/sync/server/server/index.ts",
    "content": "export {\n  startServer,\n  stopServer,\n  getStatus,\n  generateCode,\n  getDevices,\n  removeDevice,\n} from './server'\n"
  },
  {
    "path": "src/main/modules/sync/server/server/server.ts",
    "content": "import http, { type IncomingMessage } from 'node:http'\nimport { WebSocketServer } from 'ws'\nimport { registerLocalSyncEvent, callObj, sync, unregisterLocalSyncEvent } from './sync'\nimport { authCode, authConnect } from './auth'\nimport { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants_sync'\nimport { getUserSpace, releaseUserSpace, getServerId, initServerInfo } from '../user'\nimport { createMsg2call } from 'message2call'\nimport log from '../../log'\nimport { sendServerStatus } from '@main/modules/winMain'\nimport { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from '../utils/tools'\nimport migrateData from '../../migrate'\nimport type { Socket } from 'node:net'\nimport { getAddress } from '@common/utils/nodejs'\n\n\nlet status: LX.Sync.ServerStatus = {\n  status: false,\n  message: '',\n  address: [],\n  code: '',\n  devices: [],\n}\n\nlet stopingServer = false\n\nlet host = 'http://localhost'\n\nconst codeTools: {\n  timeout: NodeJS.Timeout | null\n  start: () => void\n  stop: () => void\n} = {\n  timeout: null,\n  start() {\n    this.stop()\n    this.timeout = setInterval(() => {\n      void handleGenerateCode()\n    }, 60 * 3 * 1000)\n  },\n  stop() {\n    if (!this.timeout) return\n    clearInterval(this.timeout)\n    this.timeout = null\n  },\n}\n\nconst checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => {\n  for (const client of [...wss!.clients]) {\n    if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue\n    log.info('duplicate client', client.userInfo.name, client.keyInfo.deviceName)\n    client.isReady = false\n    for (const name of Object.keys(client.moduleReadys) as Array<keyof LX.Sync.Server.Socket['moduleReadys']>) {\n      client.moduleReadys[name] = false\n    }\n    client.close(SYNC_CLOSE_CODE.normal)\n  }\n}\n\nconst handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {\n  const queryData = new URL(request.url!, host).searchParams\n  const clientId = queryData.get('i')\n\n  //   // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)\n  const userSpace = getUserSpace()\n  const keyInfo = userSpace.dataManage.getClientKeyInfo(clientId)\n  if (!keyInfo) {\n    socket.close(SYNC_CLOSE_CODE.failed)\n    return\n  }\n  keyInfo.lastConnectDate = Date.now()\n  userSpace.dataManage.saveClientKeyInfo(keyInfo)\n  //   // socket.lx_keyInfo = keyInfo\n  socket.keyInfo = keyInfo\n  socket.userInfo = { name: 'default' }\n\n  checkDuplicateClient(socket)\n\n  try {\n    await sync(socket)\n  } catch (err) {\n    // console.log(err)\n    log.warn(err)\n    return\n  }\n  status.devices.push(keyInfo)\n  // handleConnection(io, socket)\n  sendServerStatus(status)\n  socket.onClose(() => {\n    status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)\n    sendServerStatus(status)\n  })\n\n  // console.log('connection', keyInfo.deviceName)\n  log.info('connection', keyInfo.deviceName)\n  // console.log(socket.handshake.query)\n\n  socket.isReady = true\n}\n\nconst handleUnconnection = () => {\n  // console.log('unconnection')\n  releaseUserSpace()\n}\n\nconst authConnection = (req: http.IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void) => {\n  // console.log(req.headers)\n  // // console.log(req.auth)\n  // console.log(req._query.authCode)\n  authConnect(req).then(() => {\n    callback(null, true)\n  }).catch(err => {\n    callback(err, false)\n  })\n}\n\nlet wss: LX.Sync.Server.SocketServer | null\nlet httpServer: http.Server\nlet sockets = new Set<Socket>()\n\nfunction noop() {}\nfunction onSocketError(err: Error) {\n  console.error(err)\n}\n\nconst handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promise((resolve, reject) => {\n  httpServer = http.createServer((req, res) => {\n    // console.log(req.url)\n    const endUrl = `/${req.url?.split('/').at(-1) ?? ''}`\n    let code\n    let msg\n    switch (endUrl) {\n      case '/hello':\n        code = 200\n        msg = SYNC_CODE.helloMsg\n        break\n      case '/id':\n        code = 200\n        msg = SYNC_CODE.idPrefix + getServerId()\n        break\n      case '/ah':\n        void authCode(req, res, status.code)\n        break\n      default:\n        code = 401\n        msg = 'Forbidden'\n        break\n    }\n    if (!code) return\n    res.writeHead(code)\n    res.end(msg)\n  })\n\n  wss = new WebSocketServer({\n    noServer: true,\n  })\n\n  wss.on('connection', function(socket, request) {\n    socket.isReady = false\n    socket.moduleReadys = {\n      list: false,\n      dislike: false,\n    }\n    socket.feature = {\n      list: false,\n      dislike: false,\n    }\n    socket.on('pong', () => {\n      socket.isAlive = true\n    })\n\n    // const events = new Map<keyof ActionsType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()\n    // const events = new Map<keyof LX.Sync.ActionSyncType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()\n    // let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {}\n    let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []\n    let disconnected = false\n    const msg2call = createMsg2call<LX.Sync.ClientSyncActions>({\n      funcsObj: callObj,\n      timeout: 120 * 1000,\n      sendMessage(data) {\n        if (disconnected) throw new Error('disconnected')\n        void encryptMsg(socket.keyInfo, JSON.stringify(data)).then((data) => {\n          // console.log('sendData', eventName)\n          socket.send(data)\n        }).catch(err => {\n          log.error('encrypt message error:', err)\n          log.error(err.message)\n          socket.close(SYNC_CLOSE_CODE.failed)\n        })\n      },\n      onCallBeforeParams(rawArgs) {\n        return [socket, ...rawArgs]\n      },\n      onError(error, path, groupName) {\n        const name = groupName ?? ''\n        const deviceName = socket.keyInfo?.deviceName ?? ''\n        log.error(`sync call ${deviceName} ${name} ${path.join('.')} error:`, error)\n        // if (groupName == null) return\n        // socket.close(SYNC_CLOSE_CODE.failed)\n      },\n    })\n    socket.remote = msg2call.remote\n    socket.remoteQueueList = msg2call.createQueueRemote('list')\n    socket.remoteQueueDislike = msg2call.createQueueRemote('dislike')\n    socket.addEventListener('message', ({ data }) => {\n      if (typeof data != 'string') return\n      void decryptMsg(socket.keyInfo, data).then((data) => {\n        let syncData: any\n        try {\n          syncData = JSON.parse(data)\n        } catch (err) {\n          log.error('parse message error:', err)\n          socket.close(SYNC_CLOSE_CODE.failed)\n          return\n        }\n        msg2call.message(syncData)\n      }).catch(err => {\n        log.error('decrypt message error:', err)\n        log.error(err.message)\n        socket.close(SYNC_CLOSE_CODE.failed)\n      })\n    })\n    socket.addEventListener('close', () => {\n      const err = new Error('closed')\n      try {\n        for (const handler of closeEvents) void handler(err)\n      } catch (err: any) {\n        log.error(err?.message)\n      }\n      closeEvents = []\n      disconnected = true\n      msg2call.destroy()\n      if (socket.isReady) {\n        log.info('deconnection', socket.userInfo.name, socket.keyInfo.deviceName)\n        // events = {}\n        if (!status.devices.length) handleUnconnection()\n      } else {\n        const queryData = new URL(request.url!, host).searchParams\n        log.info('deconnection', queryData.get('i'))\n      }\n    })\n    socket.onClose = function(handler: typeof closeEvents[number]) {\n      closeEvents.push(handler)\n      return () => {\n        closeEvents.splice(closeEvents.indexOf(handler), 1)\n      }\n    }\n    socket.broadcast = function(handler) {\n      if (!wss) return\n      for (const client of wss.clients) handler(client)\n    }\n\n    void handleConnection(socket, request)\n  })\n\n  httpServer.on('upgrade', function upgrade(request, socket, head) {\n    socket.addListener('error', onSocketError)\n    // This function is not defined on purpose. Implement it with your own logic.\n    authConnection(request, err => {\n      if (err) {\n        console.log(err)\n        socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n        socket.destroy()\n        return\n      }\n      socket.removeListener('error', onSocketError)\n\n      wss?.handleUpgrade(request, socket, head, function done(ws) {\n        wss?.emit('connection', ws, request)\n      })\n    })\n  })\n\n  const interval = setInterval(() => {\n    wss?.clients.forEach(socket => {\n      if (socket.isAlive == false) {\n        log.info('alive check false:', socket.userInfo.name, socket.keyInfo.deviceName)\n        socket.terminate()\n        return\n      }\n\n      socket.isAlive = false\n      socket.ping(noop)\n      if (socket.keyInfo.isMobile) socket.send('ping', noop)\n    })\n  }, 30000)\n\n  wss.on('close', function close() {\n    clearInterval(interval)\n  })\n\n  httpServer.on('error', error => {\n    console.log(error)\n    reject(error)\n  })\n  httpServer.on('connection', (socket) => {\n    sockets.add(socket)\n    socket.once('close', () => {\n      sockets.delete(socket)\n    })\n  })\n\n  httpServer.on('listening', () => {\n    const addr = httpServer.address()\n    // console.log(addr)\n    if (!addr) {\n      reject(new Error('address is null'))\n      return\n    }\n    const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`\n    log.info(`Listening on ${ip} ${bind}`)\n    resolve(null)\n    void registerLocalSyncEvent(wss!)\n  })\n\n  host = `http://${ip}:${port}`\n  httpServer.listen(port, ip)\n})\n\nconst handleStopServer = async() => new Promise<void>((resolve, reject) => {\n  if (!wss) return\n  for (const client of wss.clients) client.close(SYNC_CLOSE_CODE.normal)\n  unregisterLocalSyncEvent()\n  wss.close()\n  wss = null\n  httpServer.close((err) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve()\n  })\n  for (const socket of sockets) socket.destroy()\n  sockets.clear()\n})\n\nexport const stopServer = async() => {\n  codeTools.stop()\n  if (!status.status) {\n    status.status = false\n    status.message = ''\n    status.address = []\n    status.code = ''\n    sendServerStatus(status)\n    return\n  }\n  console.log('stoping sync server...')\n  status.message = 'stoping...'\n  sendServerStatus(status)\n  stopingServer = true\n  await handleStopServer().then(() => {\n    console.log('sync server stoped')\n    status.status = false\n    status.message = ''\n    status.address = []\n    status.code = ''\n  }).catch(err => {\n    console.log(err)\n    status.message = err.message\n  }).finally(() => {\n    sendServerStatus(status)\n    stopingServer = false\n  })\n}\n\nexport const startServer = async(port: number) => {\n  // if (status.status) await handleStopServer()\n  console.log('status.status', status.status, stopingServer)\n  if (stopingServer) return\n  if (status.status) await handleStopServer()\n\n  await migrateData(global.lxDataPath)\n  await initServerInfo()\n\n  log.info('starting sync server')\n  await handleStartServer(port).then(() => {\n    console.log('sync server started')\n    status.status = true\n    status.message = ''\n    status.address = getAddress()\n\n    void generateCode()\n    codeTools.start()\n  }).catch(err => {\n    console.log(err)\n    status.status = false\n    status.message = err.message\n    status.address = []\n    status.code = ''\n  }).finally(() => {\n    sendServerStatus(status)\n  })\n}\n\nexport const getStatus = (): LX.Sync.ServerStatus => status\n\nexport const generateCode = async() => {\n  status.code = handleGenerateCode()\n  sendServerStatus(status)\n  return status.code\n}\n\nexport const getDevices = async() => {\n  const userSpace = getUserSpace()\n  return userSpace.getDecices()\n}\n\nexport const removeDevice = async(clientId: string) => {\n  if (wss) {\n    for (const client of wss.clients) {\n      if (client.keyInfo.clientId == clientId) client.close(SYNC_CLOSE_CODE.normal)\n    }\n  }\n  const userSpace = getUserSpace()\n  await userSpace.removeDevice(clientId)\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/server/sync/event.ts",
    "content": "import { modules } from '../../modules'\n\nexport const registerLocalSyncEvent = async(wss: LX.Sync.Server.SocketServer) => {\n  unregisterLocalSyncEvent()\n  for (const module of Object.values(modules)) {\n    module.registerEvent(wss)\n  }\n}\n\nexport const unregisterLocalSyncEvent = () => {\n  for (const module of Object.values(modules)) {\n    module.unregisterEvent()\n  }\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/server/sync/handler.ts",
    "content": "// 这个文件导出的方法将暴露给客户端调用，第一个参数固定为当前 socket 对象\n// import { getUserSpace } from '@/user'\nimport { FeaturesList } from '../../../../../../common/constants_sync'\nimport { modules } from '../../modules'\n\nconst handler: LX.Sync.ServerSyncHandlerActions<LX.Sync.Server.Socket> = {\n  async onFeatureChanged(socket, feature) {\n    // const userSpace = getUserSpace(socket.userInfo.name)\n    const beforeFeature = socket.feature\n\n    for (const name of FeaturesList) {\n      const newStatus = feature[name]\n      if (newStatus == null) continue\n      beforeFeature[name] = feature[name]\n      socket.moduleReadys[name] = false\n      if (feature[name]) await modules[name].sync(socket).catch(_ => _)\n    }\n  },\n}\n\nexport default handler\n"
  },
  {
    "path": "src/main/modules/sync/server/server/sync/index.ts",
    "content": "import handler from './handler'\nimport { callObj as _callObj } from '../../modules'\nexport { sync } from './sync'\nexport { modules } from '../../modules'\nexport * from './event'\n\nexport const callObj = {\n  ...handler,\n  ..._callObj,\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/server/sync/sync.ts",
    "content": "import { FeaturesList } from '../../../../../../common/constants_sync'\nimport { featureVersion, modules } from '../../modules'\n\n\nexport const sync = async(socket: LX.Sync.Server.Socket) => {\n  let disconnected = false\n  socket.onClose(() => {\n    disconnected = true\n  })\n  const enabledFeatures = await socket.remote.getEnabledFeatures('desktop-app', featureVersion)\n\n  if (disconnected) throw new Error('disconnected')\n  for (const moduleName of FeaturesList) {\n    if (enabledFeatures[moduleName]) {\n      socket.feature[moduleName] = enabledFeatures[moduleName]\n      await modules[moduleName].sync(socket).catch(_ => _)\n    }\n    if (disconnected) throw new Error('disconnected')\n  }\n  await socket.remote.finished()\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/user/data.ts",
    "content": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { randomBytes } from 'node:crypto'\nimport { throttle } from '@common/utils/common'\nimport { filterFileName, toMD5 } from '../utils'\nimport { File } from '@common/constants_sync'\nimport { exists } from '../../utils'\n\n\ninterface ServerInfo {\n  serverId: string\n  version: number\n}\ninterface DevicesInfo {\n  userName: string\n  clients: Record<string, LX.Sync.ServerKeyInfo>\n}\nconst saveServerInfoThrottle = throttle(() => {\n  fs.writeFile(path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON), JSON.stringify(serverInfo), (err) => {\n    if (err) console.error(err)\n  })\n})\nlet serverInfo: ServerInfo\nexport const initServerInfo = async() => {\n  if (serverInfo != null) return\n  const serverInfoFilePath = path.join(global.lxDataPath, File.serverDataPath, File.serverInfoJSON)\n  if (await exists(serverInfoFilePath)) {\n    // eslint-disable-next-line require-atomic-updates\n    serverInfo = JSON.parse((await fs.promises.readFile(serverInfoFilePath)).toString())\n  } else {\n    // eslint-disable-next-line require-atomic-updates\n    serverInfo = {\n      serverId: randomBytes(4 * 4).toString('base64'),\n      version: 2,\n    }\n    const syncDataPath = path.join(global.lxDataPath, File.serverDataPath)\n    if (!await exists(syncDataPath)) {\n      await fs.promises.mkdir(syncDataPath, { recursive: true })\n    }\n    saveServerInfoThrottle()\n  }\n}\nexport const getServerId = () => {\n  return serverInfo.serverId\n}\nexport const getVersion = async() => {\n  await initServerInfo()\n  return serverInfo.version ?? 1\n}\nexport const setVersion = async(version: number) => {\n  await initServerInfo()\n  serverInfo.version = version\n  saveServerInfoThrottle()\n}\n\nexport const getUserDirname = (userName: string) => `${filterFileName(userName)}_${toMD5(userName).substring(0, 6)}`\n\nexport const getUserConfig = (userName: string) => {\n  return {\n    maxSnapshotNum: global.lx.appSetting['sync.server.maxSsnapshotNum'],\n    'list.addMusicLocationType': global.lx.appSetting['list.addMusicLocationType'],\n  }\n}\n\n\n// 读取所有用户目录下的devicesInfo信息，建立clientId与用户的对应关系，用于非首次连接\n// let deviceUserMap: Map<string, string> = new Map<string, string>()\n// const init\n// for (const deviceInfo of fs.readdirSync(syncDataPath).map(dirname => {\n//   const devicesFilePath = path.join(syncDataPath, dirname, File.userDevicesJSON)\n//   if (fs.existsSync(devicesFilePath)) {\n//     const devicesInfo = JSON.parse(fs.readFileSync(devicesFilePath).toString()) as DevicesInfo\n//     if (getUserDirname(devicesInfo.userName) == dirname) return { userName: devicesInfo.userName, devices: devicesInfo.clients }\n//   }\n//   return { userName: '', devices: {} }\n// })) {\n//   for (const device of Object.values(deviceInfo.devices)) {\n//     if (deviceInfo.userName) deviceUserMap.set(device.clientId, deviceInfo.userName)\n//   }\n// }\n// export const getUserName = (clientId: string): string | null => {\n//   if (!clientId) return null\n//   return deviceUserMap.get(clientId) ?? null\n// }\n// export const setUserName = (clientId: string, dir: string) => {\n//   deviceUserMap.set(clientId, dir)\n// }\n// export const deleteUserName = (clientId: string) => {\n//   deviceUserMap.delete(clientId)\n// }\n\nexport const createClientKeyInfo = (deviceName: string, isMobile: boolean): LX.Sync.ServerKeyInfo => {\n  const keyInfo: LX.Sync.ServerKeyInfo = {\n    clientId: randomBytes(4 * 4).toString('base64'),\n    key: randomBytes(16).toString('base64'),\n    deviceName,\n    isMobile,\n    lastConnectDate: 0,\n  }\n  return keyInfo\n}\n\nexport class UserDataManage {\n  userName: string\n  userDir: string\n  devicesFilePath: string\n  devicesInfo: DevicesInfo\n  private readonly saveDevicesInfoThrottle: () => void\n\n  getAllClientKeyInfo = () => {\n    return Object.values(this.devicesInfo.clients).sort((a, b) => (b.lastConnectDate ?? 0) - (a.lastConnectDate ?? 0))\n  }\n\n  saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => {\n    if (this.devicesInfo.clients[keyInfo.clientId] == null && Object.keys(this.devicesInfo.clients).length > 101) throw new Error('max keys')\n    this.devicesInfo.clients[keyInfo.clientId] = keyInfo\n    this.saveDevicesInfoThrottle()\n  }\n\n  getClientKeyInfo = (clientId?: string | null): LX.Sync.ServerKeyInfo | null => {\n    if (!clientId) return null\n    return this.devicesInfo.clients[clientId] ?? null\n  }\n\n  removeClientKeyInfo = async(clientId: string) => {\n    // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n    delete this.devicesInfo.clients[clientId]\n    this.saveDevicesInfoThrottle()\n  }\n\n  isIncluedsClient = (clientId: string) => {\n    return Object.values(this.devicesInfo.clients).some(client => client.clientId == clientId)\n  }\n\n  constructor(userName: string) {\n    this.userName = userName\n    const syncDataPath = path.join(global.lxDataPath, File.serverDataPath)\n    this.userDir = syncDataPath\n    this.devicesFilePath = path.join(this.userDir, File.userDevicesJSON)\n    this.devicesInfo = fs.existsSync(this.devicesFilePath) ? JSON.parse(fs.readFileSync(this.devicesFilePath).toString()) : { userName, clients: {} }\n\n    this.saveDevicesInfoThrottle = throttle(() => {\n      fs.writeFile(this.devicesFilePath, JSON.stringify(this.devicesInfo), 'utf8', (err) => {\n        if (err) console.error(err)\n      })\n    })\n  }\n}\n// type UserDataManages = Map<string, UserDataManage>\n\n// export const createUserDataManage = (user: LX.UserConfig) => {\n//   const manage = Object.create(userDataManage) as typeof userDataManage\n//   manage.userDir = user.dataPath\n// }\n"
  },
  {
    "path": "src/main/modules/sync/server/user/index.ts",
    "content": "import { UserDataManage } from './data'\nimport {\n  ListManage,\n  DislikeManage,\n} from '../modules'\n\nexport interface UserSpace {\n  dataManage: UserDataManage\n  listManage: ListManage\n  dislikeManage: DislikeManage\n  getDecices: () => Promise<LX.Sync.ServerKeyInfo[]>\n  removeDevice: (clientId: string) => Promise<void>\n}\nconst users = new Map<string, UserSpace>()\n\nconst delayTime = 10 * 1000\nconst delayReleaseTimeouts = new Map<string, NodeJS.Timeout>()\nconst clearDelayReleaseTimeout = (userName: string) => {\n  if (!delayReleaseTimeouts.has(userName)) return\n\n  clearTimeout(delayReleaseTimeouts.get(userName))\n  delayReleaseTimeouts.delete(userName)\n}\nconst seartDelayReleaseTimeout = (userName: string) => {\n  clearDelayReleaseTimeout(userName)\n  delayReleaseTimeouts.set(userName, setTimeout(() => {\n    users.delete(userName)\n  }, delayTime))\n}\n\nexport const getUserSpace = (userName = 'default') => {\n  clearDelayReleaseTimeout(userName)\n\n  let user = users.get(userName)\n  if (!user) {\n    console.log('new user data manage:', userName)\n    const dataManage = new UserDataManage(userName)\n    const listManage = new ListManage(dataManage)\n    const dislikeManage = new DislikeManage(dataManage)\n    users.set(userName, user = {\n      dataManage,\n      listManage,\n      dislikeManage,\n      async getDecices() {\n        return this.dataManage.getAllClientKeyInfo()\n      },\n      async removeDevice(clientId) {\n        await listManage.removeDevice(clientId)\n        await dataManage.removeClientKeyInfo(clientId)\n      },\n    })\n  }\n  return user\n}\n\nexport const releaseUserSpace = (userName = 'default', force = false) => {\n  if (force) {\n    clearDelayReleaseTimeout(userName)\n    users.delete(userName)\n  } else seartDelayReleaseTimeout(userName)\n}\n\n\nexport * from './data'\n"
  },
  {
    "path": "src/main/modules/sync/server/utils/index.ts",
    "content": "import fs from 'node:fs'\nimport crypto from 'node:crypto'\n\n\nexport const createDirSync = (path: string) => {\n  if (!fs.existsSync(path)) {\n    try {\n      fs.mkdirSync(path, { recursive: true })\n    } catch (e: any) {\n      if (e.code !== 'EEXIST') {\n        console.error('Could not set up log directory, error was: ', e)\n        process.exit(1)\n      }\n    }\n  }\n}\n\nconst fileNameRxp = /[\\\\/:*?#\"<>|]/g\nexport const filterFileName = (name: string): string => name.replace(fileNameRxp, '')\n\n/**\n * 创建 MD5 hash\n * @param {*} str\n */\nexport const toMD5 = (str: string) => crypto.createHash('md5').update(str).digest('hex')\n\nexport const checkAndCreateDirSync = (path: string) => {\n  if (!fs.existsSync(path)) {\n    fs.mkdirSync(path, { recursive: true })\n  }\n}\n"
  },
  {
    "path": "src/main/modules/sync/server/utils/tools.ts",
    "content": "import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'\n// import { join } from 'node:path'\nimport zlib from 'node:zlib'\nimport type http from 'node:http'\n// import getStore from '@/utils/store'\n// import syncLog from '../../log'\n// import { getUserName } from '../user/data'\n// import { saveClientKeyInfo } from './data'\n\nexport const generateCode = (): string => {\n  return Math.random().toString().substring(2, 8)\n}\n\nexport const getIP = (request: http.IncomingMessage) => {\n  return request.socket.remoteAddress\n}\n\n\nexport const aesEncrypt = (buffer: string | Buffer, key: string): string => {\n  const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')\n  return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')\n}\n\nexport const aesDecrypt = (text: string, key: string): string => {\n  const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')\n  return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()\n}\n\nexport const rsaEncrypt = (buffer: Buffer, key: string): string => {\n  return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')\n}\nexport const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {\n  return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)\n}\n\n\nconst gzip = async(data: string) => new Promise<string>((resolve, reject) => {\n  zlib.gzip(data, (err, buf) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve(buf.toString('base64'))\n  })\n})\nconst unGzip = async(data: string) => new Promise<string>((resolve, reject) => {\n  zlib.gunzip(Buffer.from(data, 'base64'), (err, buf) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve(buf.toString())\n  })\n})\n\nexport const encryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, msg: string): Promise<string> => {\n  return msg.length > 1024\n    ? 'cg_' + await gzip(msg)\n    : msg\n  // if (!keyInfo) return ''\n  // return aesEncrypt(msg, keyInfo.key, keyInfo.iv)\n}\n\nexport const decryptMsg = async(keyInfo: LX.Sync.ServerKeyInfo | null, enMsg: string): Promise<string> => {\n  return enMsg.substring(0, 3) == 'cg_'\n    ? await unGzip(enMsg.replace('cg_', ''))\n    : enMsg\n  // console.log('decmsg raw: ', len.length, 'en: ', enMsg.length)\n\n  // if (!keyInfo) return ''\n  // let msg = ''\n  // try {\n  //   msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)\n  // } catch (err) {\n  //   console.log(err)\n  // }\n  // return msg\n}\n\n// export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => {\n//   return join(global.lx.snapshotPath, `snapshot_${keyInfo.snapshotKey}.json`)\n// }\n\n// export const sendStatus = (status: LX.Sync.ServerStatus) => {\n//   syncLog.info('status', status.devices.map(d => `${getUserName(d.clientId) ?? ''} ${d.deviceName}`))\n// }\n\n"
  },
  {
    "path": "src/main/modules/sync/utils.ts",
    "content": "import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'\nimport os from 'node:os'\nimport fs from 'node:fs'\nimport zlib from 'node:zlib'\nimport cp from 'node:child_process'\n\n\n// https://stackoverflow.com/a/75309339\nexport const getComputerName = () => {\n  let name: string | undefined\n  switch (process.platform) {\n    case 'win32':\n      name = process.env.COMPUTERNAME\n      break\n    case 'darwin':\n      try {\n        name = cp.execSync('scutil --get ComputerName').toString().trim()\n      } catch {}\n      break\n    case 'linux':\n      // Don't fail even if hostnamectl is unavailable\n      try {\n        name = cp.execSync('hostnamectl --pretty').toString().trim()\n      } catch {}\n      break\n  }\n  if (!name) name = os.hostname()\n  return name\n}\n\nconst gzip = async(data: string) => new Promise<string>((resolve, reject) => {\n  zlib.gzip(data, (err, buf) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve(buf.toString('base64'))\n  })\n})\nconst unGzip = async(data: string) => new Promise<string>((resolve, reject) => {\n  zlib.gunzip(Buffer.from(data, 'base64'), (err, buf) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve(buf.toString())\n  })\n})\n\nexport const encodeData = async(data: string) => {\n  return data.length > 1024\n    ? 'cg_' + await gzip(data)\n    : data\n}\n\nexport const decodeData = async(enData: string) => {\n  return enData.substring(0, 3) == 'cg_'\n    ? await unGzip(enData.replace('cg_', ''))\n    : enData\n}\n\n\nexport const aesEncrypt = (text: string, key: string) => {\n  const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')\n  return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')\n}\n\nexport const aesDecrypt = (text: string, key: string) => {\n  const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')\n  return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()\n}\n\nexport const rsaEncrypt = (buffer: Buffer, key: string): string => {\n  return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')\n}\nexport const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {\n  return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)\n}\n\n\nexport const exists = async(path: string) => fs.promises.stat(path).then(() => true).catch(() => false)\n"
  },
  {
    "path": "src/main/modules/tray.ts",
    "content": "import { Tray, Menu, nativeImage } from 'electron'\nimport { isMac, isWin } from '@common/utils'\nimport path from 'node:path'\nimport {\n  hideWindow as hideMainWindow,\n  isExistWindow as isExistMainWindow,\n  isShowWindow as isShowMainWindow,\n  sendTaskbarButtonClick,\n  showWindow as showMainWindow,\n} from './winMain'\nimport { quitApp } from '@main/app'\nimport { TRAY_AUTO_ID } from '@common/constants'\n\nlet tray: Electron.Tray | null\nlet isEnableTray: boolean = false\nlet themeId: number\nlet isShowStatusBarLyric: boolean = false\n\nconst playerState = {\n  empty: false,\n  collect: false,\n  play: false,\n  next: true,\n  prev: true,\n}\n\nconst watchConfigKeys = [\n  'desktopLyric.enable',\n  'desktopLyric.isLock',\n  'desktopLyric.isAlwaysOnTop',\n  'tray.themeId',\n  'tray.enable',\n  'player.isShowStatusBarLyric',\n  'common.langId',\n] satisfies Array<keyof LX.AppSetting>\n\nconst themeList = [\n  {\n    id: 0,\n    fileName: 'trayTemplate',\n    isNative: true,\n  },\n  {\n    id: 1,\n    fileName: 'tray_origin',\n    isNative: false,\n  },\n  {\n    id: 2,\n    fileName: 'tray_black',\n    isNative: false,\n  },\n]\n\nconst messages = {\n  'en-us': {\n    collect: 'Love',\n    uncollect: 'Unlove',\n    play: 'Play',\n    pause: 'Pause',\n    next: 'Next Song',\n    prev: 'Prev Song',\n    hide_win_main: 'Hide Main Window',\n    show_win_main: 'Show Main Window',\n    hide_win_lyric: 'Hide Lyric Window',\n    show_win_lyric: 'Show Lyric Window',\n    lock_win_lyric: 'Lock Lyric Window',\n    unlock_win_lyric: 'Unlock Lyric Window',\n    top_win_lyric: 'On-top Lyric Window',\n    untop_win_lyric: 'Un-top Lyric Window',\n    show_statusbar_lyric: 'Show Lyrics on Statusbar',\n    hide_statusbar_lyric: 'Hide Lyrics on Statusbar',\n    exit: 'Exit',\n    music_name: 'Title: ',\n    music_singer: 'Artist: ',\n  },\n  'zh-cn': {\n    collect: '收藏',\n    uncollect: '取消收藏',\n    play: '播放',\n    pause: '暂停',\n    next: '下一曲',\n    prev: '上一曲',\n    hide_win_main: '隐藏主界面',\n    show_win_main: '显示主界面',\n    hide_win_lyric: '关闭桌面歌词',\n    show_win_lyric: '开启桌面歌词',\n    lock_win_lyric: '锁定桌面歌词',\n    unlock_win_lyric: '解锁桌面歌词',\n    top_win_lyric: '置顶歌词',\n    untop_win_lyric: '取消置顶',\n    show_statusbar_lyric: '显示状态栏歌词',\n    hide_statusbar_lyric: '隐藏状态栏歌词',\n    exit: '退出',\n    music_name: '歌曲名: ',\n    music_singer: '艺术家: ',\n  },\n  'zh-tw': {\n    collect: '收藏',\n    uncollect: '取消收藏',\n    play: '播放',\n    pause: '暫停',\n    next: '下一曲',\n    prev: '上一曲',\n    hide_win_main: '隱藏軟體視窗',\n    show_win_main: '顯示軟體視窗',\n    hide_win_lyric: '關閉歌詞視窗',\n    show_win_lyric: '開啟歌詞視窗',\n    lock_win_lyric: '鎖定歌詞視窗',\n    unlock_win_lyric: '解鎖歌詞視窗',\n    top_win_lyric: '置頂歌詞視窗',\n    untop_win_lyric: '取消置頂歌詞視窗',\n    show_statusbar_lyric: '顯示狀態列歌詞',\n    hide_statusbar_lyric: '隱藏狀態列歌詞',\n    exit: '退出',\n    music_name: '標題: ',\n    music_singer: '演出者: ',\n  },\n} as const\ntype Messages = typeof messages\ntype Langs = keyof Messages\nconst i18n = {\n  message: messages['zh-cn'] as Messages[Langs],\n  fallbackLocale: 'en-us' as 'en-us',\n  getMessage(key: keyof Messages[Langs]) {\n    return this.message[key]\n  },\n  setLang(lang?: Langs | null) {\n    this.message = lang\n      ? messages[lang] ?? messages[this.fallbackLocale]\n      : messages[this.fallbackLocale]\n  },\n}\n\nconst getIconPath = (id: number) => {\n  let theme = id == TRAY_AUTO_ID\n    ? global.lx.theme.shouldUseDarkColors\n      ? themeList[0] : themeList[2]\n    : themeList.find(item => item.id === id) ?? themeList[0]\n  return path.join(global.staticPath, 'images/tray', theme.fileName + (isWin ? '.ico' : '.png'))\n}\n\nexport const createTray = () => {\n  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n  if ((tray && !tray.isDestroyed()) || !global.lx.appSetting['tray.enable']) return\n\n  // 托盘\n  tray = new Tray(nativeImage.createFromPath(getIconPath(global.lx.appSetting['tray.themeId'])))\n\n  // tray.setToolTip('LX Music')\n  // createMenu()\n  tray.setIgnoreDoubleClickEvents(true)\n  if (isWin) {\n    tray.on('click', () => {\n      showMainWindow()\n    })\n  }\n}\n\nexport const destroyTray = () => {\n  if (!tray) return\n  tray.destroy()\n  isEnableTray = false\n  isShowStatusBarLyric = false\n  tray = null\n}\n\nconst handleUpdateConfig = (setting: Partial<LX.AppSetting>) => {\n  global.lx.event_app.update_config(setting)\n}\n\nconst createPlayerMenu = () => {\n  let menu: Electron.MenuItemConstructorOptions[] = []\n  menu.push(playerState.play ? {\n    label: i18n.getMessage('pause'),\n    click() {\n      sendTaskbarButtonClick('pause')\n    },\n  } : {\n    label: i18n.getMessage('play'),\n    click() {\n      sendTaskbarButtonClick('play')\n    },\n  })\n  menu.push({\n    label: i18n.getMessage('prev'),\n    click() {\n      sendTaskbarButtonClick('prev')\n    },\n  })\n  menu.push({\n    label: i18n.getMessage('next'),\n    click() {\n      sendTaskbarButtonClick('next')\n    },\n  })\n  menu.push(playerState.collect ? {\n    label: i18n.getMessage('uncollect'),\n    click() {\n      sendTaskbarButtonClick('unCollect')\n    },\n  } : {\n    label: i18n.getMessage('collect'),\n    click() {\n      sendTaskbarButtonClick('collect')\n    },\n  })\n  return menu\n}\n\nexport const createMenu = () => {\n  if (!tray) return\n  let menu: Electron.MenuItemConstructorOptions[] = createPlayerMenu()\n  if (playerState.empty) for (const m of menu) m.enabled = false\n  menu.push({ type: 'separator' })\n  menu.push(global.lx.appSetting['desktopLyric.enable']\n    ? {\n        label: i18n.getMessage('hide_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.enable': false })\n        },\n      }\n    : {\n        label: i18n.getMessage('show_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.enable': true })\n        },\n      })\n  menu.push(global.lx.appSetting['desktopLyric.isLock']\n    ? {\n        label: i18n.getMessage('unlock_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.isLock': false })\n        },\n      }\n    : {\n        label: i18n.getMessage('lock_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.isLock': true })\n        },\n      })\n  menu.push(global.lx.appSetting['desktopLyric.isAlwaysOnTop']\n    ? {\n        label: i18n.getMessage('untop_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.isAlwaysOnTop': false })\n        },\n      }\n    : {\n        label: i18n.getMessage('top_win_lyric'),\n        click() {\n          handleUpdateConfig({ 'desktopLyric.isAlwaysOnTop': true })\n        },\n      })\n  if (isMac) {\n    menu.push({ type: 'separator' })\n    menu.push(isShowStatusBarLyric\n      ? {\n          label: i18n.getMessage('hide_statusbar_lyric'),\n          click() {\n            handleUpdateConfig({ 'player.isShowStatusBarLyric': false })\n          },\n        }\n      : {\n          label: i18n.getMessage('show_statusbar_lyric'),\n          click() {\n            handleUpdateConfig({ 'player.isShowStatusBarLyric': true })\n          },\n        })\n  }\n  menu.push({ type: 'separator' })\n  if (isExistMainWindow()) {\n    const isShow = isShowMainWindow()\n    menu.push(isShow\n      ? {\n          label: i18n.getMessage('hide_win_main'),\n          click() {\n            hideMainWindow()\n          },\n        }\n      : {\n          label: i18n.getMessage('show_win_main'),\n          click() {\n            showMainWindow()\n          },\n        })\n  }\n  menu.push({\n    label: i18n.getMessage('exit'),\n    click() {\n      quitApp()\n    },\n  })\n  const contextMenu = Menu.buildFromTemplate(menu)\n  tray.setContextMenu(contextMenu)\n}\n\nexport const setTrayImage = (themeId: number) => {\n  if (!tray) return\n  tray.setImage(nativeImage.createFromPath(getIconPath(themeId)))\n}\n\nconst setLyric = (lyricLineText?: string) => {\n  if (isShowStatusBarLyric && tray && lyricLineText != null) {\n    tray.setTitle(lyricLineText)\n  }\n}\n\nconst defaultTip = 'LX Music'\nconst setTip = () => {\n  if (!tray) return\n\n  let name = global.lx.player_status.name\n  let tip: string\n  if (name) {\n    if (name.length > 20) name = name.substring(0, 20) + '...'\n    let singer = global.lx.player_status.singer\n    if (singer?.length > 20) singer = singer.substring(0, 20) + '...'\n\n    tip = `${defaultTip}\\n${i18n.getMessage('music_name')}${name}${singer ? `\\n${i18n.getMessage('music_singer')}${singer}` : ''}`\n  } else tip = defaultTip\n  tray.setToolTip(tip)\n}\n\nconst init = () => {\n  if (themeId != global.lx.appSetting['tray.themeId']) {\n    themeId = global.lx.appSetting['tray.themeId']\n    setTrayImage(themeId)\n  }\n  if (isEnableTray !== global.lx.appSetting['tray.enable']) {\n    isEnableTray = global.lx.appSetting['tray.enable']\n    global.lx.appSetting['tray.enable'] ? createTray() : destroyTray()\n  }\n  if (isShowStatusBarLyric !== global.lx.appSetting['player.isShowStatusBarLyric']) {\n    isShowStatusBarLyric = global.lx.appSetting['player.isShowStatusBarLyric']\n    if (isShowStatusBarLyric) {\n      setLyric(global.lx.player_status.lyricLineText)\n    } else {\n      tray?.setTitle('')\n    }\n  }\n  setTip()\n  createMenu()\n}\n\nexport default () => {\n  global.lx.event_app.on('updated_config', (keys, setting) => {\n    if (!watchConfigKeys.some(key => keys.includes(key))) return\n\n    if (keys.includes('common.langId')) i18n.setLang(setting['common.langId'])\n\n    init()\n  })\n\n  global.lx.event_app.on('main_window_ready_to_show', () => {\n    createMenu()\n  })\n  global.lx.event_app.on('main_window_show', () => {\n    createMenu()\n  })\n  if (!isWin) {\n    global.lx.event_app.on('main_window_focus', () => {\n      createMenu()\n    })\n    global.lx.event_app.on('main_window_blur', () => {\n      createMenu()\n    })\n  }\n  global.lx.event_app.on('main_window_hide', () => {\n    createMenu()\n  })\n  global.lx.event_app.on('main_window_close', () => {\n    destroyTray()\n  })\n\n  global.lx.event_app.on('app_inited', () => {\n    i18n.setLang(global.lx.appSetting['common.langId'])\n    init()\n  })\n\n  global.lx.event_app.on('system_theme_change', () => {\n    if (global.lx.appSetting['tray.themeId'] != TRAY_AUTO_ID) return\n    setTrayImage(global.lx.appSetting['tray.themeId'])\n  })\n\n  global.lx.event_app.on('player_status', (status) => {\n    let updated = false\n    if (status.status) {\n      switch (status.status) {\n        case 'paused':\n          playerState.play = false\n          playerState.empty &&= false\n          setLyric('')\n          break\n        case 'error':\n          playerState.play = false\n          playerState.empty &&= false\n          setLyric('')\n          break\n        case 'playing':\n          playerState.play = true\n          playerState.empty &&= false\n          setLyric(global.lx.player_status.lyricLineText)\n          break\n        case 'stoped':\n          playerState.play &&= false\n          playerState.empty = true\n          setLyric('')\n          break\n      }\n      updated = true\n    } else {\n      setLyric(status.lyricLineText)\n    }\n    if (status.name != null) setTip()\n    if (status.singer != null) setTip()\n    if (status.collect != null) {\n      playerState.collect = status.collect\n      updated = true\n    }\n    if (updated) init()\n  })\n}\n"
  },
  {
    "path": "src/main/modules/userApi/config/index.ts",
    "content": "\nexport const userApis: LX.UserApi.UserApiInfoFull[] = []\n"
  },
  {
    "path": "src/main/modules/userApi/index.ts",
    "content": "import { closeWindow } from './main'\nimport { getUserApis, importApi as handleImportApi, removeApi as handleRemoveApi, setAllowShowUpdateAlert as saveAllowShowUpdateAlert } from './utils'\nimport { loadApi, setAllowShowUpdateAlert as setRendererEventAllowShowUpdateAlert, init } from './rendererEvent/rendererEvent'\n\nlet userApiId: string | null\n\nexport const getApiList = getUserApis\n\nexport const importApi = async(script: string): Promise<LX.UserApi.ImportUserApi> => {\n  return {\n    apiInfo: await handleImportApi(script),\n    apiList: getUserApis(),\n  }\n}\nexport const removeApi = async(ids: string[]): Promise<LX.UserApi.UserApiInfo[]> => {\n  if (userApiId && ids.includes(userApiId)) {\n    userApiId = null\n    await closeWindow()\n  }\n  handleRemoveApi(ids)\n  return getUserApis()\n}\n\nexport const setApi = async(id: string) => {\n  if (userApiId) {\n    userApiId = null\n    await closeWindow()\n  }\n  const apiList = getUserApis()\n  if (!apiList.some(a => a.id === id)) return\n  userApiId ||= id\n  await loadApi(id)\n}\n\nexport const setAllowShowUpdateAlert = (id: string, enable: boolean) => {\n  saveAllowShowUpdateAlert(id, enable)\n  setRendererEventAllowShowUpdateAlert(id, enable)\n}\n\n\nexport * from './rendererEvent/rendererEvent'\n\nexport default () => {\n  init()\n\n  global.lx.event_app.on('main_window_close', () => {\n    void closeWindow()\n  })\n}\n"
  },
  {
    "path": "src/main/modules/userApi/main.ts",
    "content": "import { mainSend } from '@common/mainIpc'\nimport { BrowserWindow } from 'electron'\nimport fs from 'fs'\nimport path from 'node:path'\nimport { openDevTools as handleOpenDevTools } from '@main/utils'\nimport USER_API_RENDERER_EVENT_NAME from './rendererEvent/name'\nimport { getScript } from './utils'\n\nlet browserWindow: Electron.BrowserWindow | null = null\n\nlet html: string | null = null\nlet dir: string | null = null\n\nconst denyEvents = [\n  'will-navigate',\n  'will-redirect',\n  'will-attach-webview',\n  'will-prevent-unload',\n  'media-started-playing',\n] as const\n\n\nexport const getProxy = () => {\n  if (global.lx.appSetting['network.proxy.enable'] && global.lx.appSetting['network.proxy.host']) {\n    return {\n      host: global.lx.appSetting['network.proxy.host'],\n      port: global.lx.appSetting['network.proxy.port'],\n    }\n  }\n  const envProxy = envParams.cmdParams['proxy-server']\n  if (envProxy) {\n    if (envProxy && typeof envProxy == 'string') {\n      const [host, port = ''] = envProxy.split(':')\n      return {\n        host,\n        port,\n      }\n    }\n  }\n  return {\n    host: '',\n    port: '',\n  }\n}\nconst handleUpdateProxy = (keys: Array<keyof LX.AppSetting>) => {\n  if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.startsWith('network.proxy.')))) {\n    sendEvent(USER_API_RENDERER_EVENT_NAME.proxyUpdate, getProxy())\n  }\n}\n\nconst winEvent = () => {\n  if (!browserWindow) return\n  browserWindow.on('closed', () => {\n    browserWindow = null\n  })\n}\n\nexport const createWindow = async(userApi: LX.UserApi.UserApiInfo) => {\n  await closeWindow()\n  dir ??= process.env.NODE_ENV !== 'production' ? webpackUserApiPath : path.join(__dirname, 'userApi')\n\n  if (!html) {\n    // eslint-disable-next-line require-atomic-updates\n    html = await fs.promises.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8')\n  }\n  const preloadUrl = process.env.NODE_ENV !== 'production'\n    ? `${path.join(__dirname, '../dist/user-api-preload.js')}`\n    : `${path.join(__dirname, 'user-api-preload.js')}`\n  // console.log(preloadUrl)\n\n  /**\n   * Initial window options\n   */\n  browserWindow = new BrowserWindow({\n    // enableRemoteModule: false,\n    resizable: false,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    roundedCorners: false,\n    hasShadow: false,\n    show: false,\n    webPreferences: {\n      contextIsolation: true,\n      // worldSafeExecuteJavaScript: true,\n      nodeIntegration: false,\n      nodeIntegrationInWorker: false,\n      sandbox: false,\n\n      spellcheck: false,\n      autoplayPolicy: 'document-user-activation-required',\n      enableWebSQL: false,\n      disableDialogs: true,\n      // nativeWindowOpen: false,\n      webgl: false,\n      images: false,\n\n      preload: preloadUrl,\n    },\n  })\n\n  for (const eventName of denyEvents) {\n    // @ts-expect-error\n    browserWindow.webContents.on(eventName, (event: Electron.Event) => {\n      event.preventDefault()\n    })\n  }\n  browserWindow.webContents.session.setPermissionRequestHandler((webContents, permission, resolve) => {\n    if (webContents === browserWindow?.webContents) {\n      resolve(false)\n      return\n    }\n    resolve(true)\n  })\n  browserWindow.webContents.setWindowOpenHandler(() => {\n    return { action: 'deny' }\n  })\n\n  winEvent()\n\n  // console.log(html.replace('</body>', `<script>${userApi.script}</script></body>`))\n  // const randomNum = Math.random().toString().substring(2, 10)\n  await browserWindow.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(html))\n\n  browserWindow.on('ready-to-show', async() => {\n    global.lx.event_app.on('updated_config', handleUpdateProxy)\n    sendEvent(USER_API_RENDERER_EVENT_NAME.initEnv, { ...userApi, script: await getScript(userApi.id), proxy: getProxy() })\n  })\n\n  // global.modules.userApiWindow.loadFile(join(dir, 'renderer/user-api.html'))\n  // global.modules.userApiWindow.webContents.openDevTools()\n}\n\nexport const closeWindow = async() => {\n  global.lx.event_app.off('updated_config', handleUpdateProxy)\n  if (!browserWindow) return\n  await Promise.all([\n    browserWindow.webContents.session.clearAuthCache(),\n    browserWindow.webContents.session.clearStorageData(),\n    browserWindow.webContents.session.clearCache(),\n  ])\n  browserWindow?.destroy()\n  browserWindow = null\n}\n\nexport const sendEvent = <T = any>(name: string, params?: T) => {\n  if (!browserWindow) return\n  mainSend(browserWindow, name, params)\n}\n\nexport const openDevTools = () => {\n  if (!browserWindow) return\n  handleOpenDevTools(browserWindow.webContents)\n}\n"
  },
  {
    "path": "src/main/modules/userApi/renderer/preload.js",
    "content": "import { contextBridge, ipcRenderer, webFrame } from 'electron'\nimport needle from 'needle'\nimport zlib from 'zlib'\nimport { createCipheriv, publicEncrypt, constants, randomBytes, createHash } from 'crypto'\nimport USER_API_RENDERER_EVENT_NAME from '../rendererEvent/name'\nimport { httpOverHttp, httpsOverHttp } from 'tunnel'\n\n\nconst sendMessage = (action, data, status, message) => {\n  ipcRenderer.send(action, { data, status, message })\n}\n\nlet isInitedApi = false\nconst proxy = {\n  host: '',\n  port: '',\n}\nlet isShowedUpdateAlert = false\nconst EVENT_NAMES = {\n  request: 'request',\n  inited: 'inited',\n  updateAlert: 'updateAlert',\n}\nconst eventNames = Object.values(EVENT_NAMES)\nconst events = {\n  request: null,\n}\nconst allSources = ['kw', 'kg', 'tx', 'wy', 'mg', 'local']\nconst supportQualitys = {\n  kw: ['128k', '320k', 'flac', 'flac24bit'],\n  kg: ['128k', '320k', 'flac', 'flac24bit'],\n  tx: ['128k', '320k', 'flac', 'flac24bit'],\n  wy: ['128k', '320k', 'flac', 'flac24bit'],\n  mg: ['128k', '320k', 'flac', 'flac24bit'],\n  local: [],\n}\nconst supportActions = {\n  kw: ['musicUrl'],\n  kg: ['musicUrl'],\n  tx: ['musicUrl'],\n  wy: ['musicUrl'],\n  mg: ['musicUrl'],\n  xm: ['musicUrl'],\n  local: ['musicUrl', 'lyric', 'pic'],\n}\n\nconst httpsRxp = /^https:/\nconst getRequestAgent = url => {\n  return proxy.host ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({\n    proxy: {\n      host: proxy.host,\n      port: proxy.port,\n    },\n  }) : undefined\n}\n\nconst verifyLyricInfo = (info) => {\n  if (typeof info != 'object' || typeof info.lyric != 'string') throw new Error('failed')\n  if (info.lyric.length > 51200) throw new Error('failed')\n  return {\n    lyric: info.lyric,\n    tlyric: (typeof info.tlyric == 'string' && info.tlyric.length < 5120) ? info.tlyric : null,\n    rlyric: (typeof info.rlyric == 'string' && info.rlyric.length < 5120) ? info.rlyric : null,\n    lxlyric: (typeof info.lxlyric == 'string' && info.lxlyric.length < 8192) ? info.lxlyric : null,\n  }\n}\n\nconst handleRequest = (context, { requestKey, data }) => {\n  // console.log(data)\n  if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, 'Request event is not defined')\n  try {\n    events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => {\n      let sendData = {\n        requestKey,\n      }\n      switch (data.action) {\n        case 'musicUrl':\n          if (typeof response != 'string' || response.length > 2048 || !/^https?:/.test(response)) throw new Error('failed')\n          sendData.result = {\n            source: data.source,\n            action: data.action,\n            data: {\n              type: data.info.type,\n              url: response,\n            },\n          }\n          break\n        case 'lyric':\n          sendData.result = {\n            source: data.source,\n            action: data.action,\n            data: verifyLyricInfo(response),\n          }\n          break\n        case 'pic':\n          if (typeof response != 'string' || response.length > 2048 || !/^https?:/.test(response)) throw new Error('failed')\n          sendData.result = {\n            source: data.source,\n            action: data.action,\n            data: response,\n          }\n          break\n      }\n      sendMessage(USER_API_RENDERER_EVENT_NAME.response, sendData, true)\n    }).catch(err => {\n      sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message)\n    })\n  } catch (err) {\n    sendMessage(USER_API_RENDERER_EVENT_NAME.response, { requestKey }, false, err.message)\n  }\n}\n\n/**\n *\n * @param {*} context\n * @param {*} info {\n *                    openDevTools: false,\n *                    message: 'xxx',\n *                    sources: {\n *                         kw: ['128k', '320k', 'flac', 'flac24bit'],\n *                         kg: ['128k', '320k', 'flac', 'flac24bit'],\n *                         tx: ['128k', '320k', 'flac', 'flac24bit'],\n *                         wy: ['128k', '320k', 'flac', 'flac24bit'],\n *                         mg: ['128k', '320k', 'flac', 'flac24bit'],\n *                     }\n *                 }\n */\nconst handleInit = (context, info) => {\n  if (!info) {\n    sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info')\n    // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')\n    return\n  }\n  if (info.openDevTools === true) {\n    sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools)\n  }\n  // if (!info.status) {\n  //   sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, 'Missing required parameter init info')\n  //   // sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')\n  //   return\n  // }\n  const sourceInfo = {\n    sources: {},\n  }\n  try {\n    for (const source of allSources) {\n      const userSource = info.sources[source]\n      if (!userSource || userSource.type !== 'music') continue\n      const qualitys = supportQualitys[source]\n      const actions = supportActions[source]\n      sourceInfo.sources[source] = {\n        type: 'music',\n        actions: actions.filter(a => userSource.actions.includes(a)),\n        qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),\n      }\n    }\n  } catch (error) {\n    console.log(error)\n    sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, error.message)\n    return\n  }\n  sendMessage(USER_API_RENDERER_EVENT_NAME.init, sourceInfo, true)\n\n  ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => {\n    handleRequest(context, data)\n  })\n}\n\nconst handleShowUpdateAlert = (data, resolve, reject) => {\n  if (!data || typeof data != 'object') return reject(new Error('parameter format error.'))\n  if (!data.log || typeof data.log != 'string') return reject(new Error('log is required.'))\n  if (data.updateUrl && !/^https?:\\/\\/[^\\s$.?#].[^\\s]*$/.test(data.updateUrl) && data.updateUrl.length > 1024) delete data.updateUrl\n  if (data.log.length > 1024) data.log = data.log.substring(0, 1024) + '...'\n  sendMessage(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, {\n    log: data.log,\n    updateUrl: data.updateUrl,\n  })\n  resolve()\n}\n\nconst onError = (errorMessage) => {\n  if (isInitedApi) return\n  isInitedApi = true\n  if (errorMessage.length > 1024) errorMessage = errorMessage.substring(0, 1024) + '...'\n  sendMessage(USER_API_RENDERER_EVENT_NAME.init, null, false, errorMessage)\n}\n\nconst initEnv = (userApi) => {\n  proxy.host = userApi.proxy.host\n  proxy.port = userApi.proxy.port\n\n  contextBridge.exposeInMainWorld('lx', {\n    EVENT_NAMES,\n    request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {\n      let options = {\n        headers,\n        agent: getRequestAgent(url),\n      }\n      let data\n      if (body) {\n        data = body\n      } else if (form) {\n        data = form\n        // data.content_type = 'application/x-www-form-urlencoded'\n        options.json = false\n      } else if (formData) {\n        data = formData\n        // data.content_type = 'multipart/form-data'\n        options.json = false\n      }\n      options.response_timeout = typeof timeout == 'number' && timeout > 0 ? Math.min(timeout, 60_000) : 60_000\n\n      let request = needle.request(method, url, data, options, (err, resp, body) => {\n        // console.log(err, resp, body)\n        try {\n          if (err) {\n            callback.call(this, err, null, null)\n          } else {\n            body = resp.body = resp.raw.toString()\n            try {\n              resp.body = JSON.parse(resp.body)\n            } catch (_) {}\n            body = resp.body\n            callback.call(this, err, {\n              statusCode: resp.statusCode,\n              statusMessage: resp.statusMessage,\n              headers: resp.headers,\n              bytes: resp.bytes,\n              raw: resp.raw,\n              body,\n            }, body)\n          }\n        } catch (err) {\n          onError(err.message)\n        }\n      }).request\n\n      return () => {\n        if (!request.aborted) request.abort()\n        request = null\n      }\n    },\n    send(eventName, data) {\n      return new Promise((resolve, reject) => {\n        if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))\n        switch (eventName) {\n          case EVENT_NAMES.inited:\n            if (isInitedApi) return reject(new Error('Script is inited'))\n            isInitedApi = true\n            handleInit(this, data)\n            resolve()\n            break\n          case EVENT_NAMES.updateAlert:\n            if (isShowedUpdateAlert) return reject(new Error('The update alert can only be called once.'))\n            isShowedUpdateAlert = true\n            handleShowUpdateAlert(data, resolve, reject)\n            break\n          default:\n            reject(new Error('Unknown event name: ' + eventName))\n        }\n      })\n    },\n    on(eventName, handler) {\n      if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))\n      switch (eventName) {\n        case EVENT_NAMES.request:\n          events.request = handler\n          break\n        default: return Promise.reject(new Error('The event is not supported: ' + eventName))\n      }\n      return Promise.resolve()\n    },\n    utils: {\n      crypto: {\n        aesEncrypt(buffer, mode, key, iv) {\n          const cipher = createCipheriv(mode, key, iv)\n          return Buffer.concat([cipher.update(buffer), cipher.final()])\n        },\n        rsaEncrypt(buffer, key) {\n          buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])\n          return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer)\n        },\n        randomBytes(size) {\n          return randomBytes(size)\n        },\n        md5(str) {\n          return createHash('md5').update(str).digest('hex')\n        },\n      },\n      buffer: {\n        from(...args) {\n          return Buffer.from(...args)\n        },\n        bufToString(buf, format) {\n          return Buffer.from(buf, 'binary').toString(format)\n        },\n      },\n      zlib: {\n        inflate(buf) {\n          return new Promise((resolve, reject) => {\n            zlib.inflate(buf, (err, data) => {\n              if (err) reject(new Error(err.message))\n              else resolve(data)\n            })\n          })\n        },\n        deflate(data) {\n          return new Promise((resolve, reject) => {\n            zlib.deflate(data, (err, buf) => {\n              if (err) reject(new Error(err.message))\n              else resolve(buf)\n            })\n          })\n        },\n      },\n    },\n    currentScriptInfo: {\n      name: userApi.name,\n      description: userApi.description,\n      version: userApi.version,\n      author: userApi.author,\n      homepage: userApi.homepage,\n      rawScript: userApi.script,\n    },\n    version: '2.0.0',\n    env: 'desktop',\n    // removeEvent(eventName, handler) {\n    //   if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))\n    //   let handlers\n    //   switch (eventName) {\n    //     case EVENT_NAMES.request:\n    //       handlers = events.request\n    //       break\n    //   }\n    //   for (let index = 0; index < handlers.length; index++) {\n    //     if (handlers[index] === handler) {\n    //       handlers.splice(index, 1)\n    //       break\n    //     }\n    //   }\n    // },\n    // removeAllEvents() {\n    //   for (const handlers of Object.values(events)) {\n    //     handlers.splice(0, handlers.length)\n    //   }\n    // },\n  })\n\n  contextBridge.exposeInMainWorld('__lx_init_error_handler__', {\n    sendError(errorMessage) {\n      onError(errorMessage)\n    },\n  })\n\n  webFrame.executeJavaScript(`(() => {\nwindow.addEventListener('error', (event) => {\n  if (event.isTrusted) globalThis.__lx_init_error_handler__.sendError(event.message.replace(/^Uncaught\\\\sError:\\\\s/, ''))\n})\nwindow.addEventListener('unhandledrejection', (event) => {\n  if (!event.isTrusted) return\n  const message = typeof event.reason === 'string' ? event.reason : event.reason?.message ?? String(event.reason)\n  globalThis.__lx_init_error_handler__.sendError(message.replace(/^Error:\\\\s/, ''))\n})\n})()`)\n\n  webFrame.executeJavaScript(userApi.script).catch(_ => _)\n}\n\n\nipcRenderer.on(USER_API_RENDERER_EVENT_NAME.initEnv, (event, data) => {\n  initEnv(data)\n})\n\nipcRenderer.on(USER_API_RENDERER_EVENT_NAME.proxyUpdate, (event, data) => {\n  proxy.host = data.host\n  proxy.port = data.port\n})\n"
  },
  {
    "path": "src/main/modules/userApi/renderer/user-api.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>User api</title>\n</head>\n<body>\n\n</body>\n</html>\n"
  },
  {
    "path": "src/main/modules/userApi/rendererEvent/name.js",
    "content": "const names = {\n  initEnv: '',\n  init: '',\n  request: '',\n  response: '',\n  openDevTools: '',\n  showUpdateAlert: '',\n  getProxy: '',\n  proxyUpdate: '',\n}\n\n\nfor (const key of Object.keys(names)) {\n  names[key] = `userApi_${key}`\n}\n\nexport default names\n"
  },
  {
    "path": "src/main/modules/userApi/rendererEvent/rendererEvent.ts",
    "content": "import { mainOn } from '@common/mainIpc'\n\nimport USER_API_RENDERER_EVENT_NAME from './name'\nimport { createWindow, getProxy, openDevTools, sendEvent } from '../main'\nimport { getUserApis } from '../utils'\nimport { sendShowUpdateAlert, sendStatusChange } from '@main/modules/winMain'\n\nlet userApi: LX.UserApi.UserApiInfo\nlet apiStatus: LX.UserApi.UserApiStatus = { status: true }\nconst requestQueue = new Map()\nconst timeouts = new Map<string, NodeJS.Timeout>()\ninterface InitParams {\n  params: {\n    status: boolean\n    message: string\n    data: LX.UserApi.UserApiInfo\n  }\n}\ninterface ResponseParams {\n  params: {\n    status: boolean\n    message: string\n    data: {\n      requestKey: string\n      result: any\n    }\n  }\n}\ninterface UpdateInfoParams {\n  params: {\n    data: {\n      log: string\n      updateUrl: string\n    }\n  }\n}\n\nexport const init = () => {\n  const handleInit = ({ params: { status, message, data: apiInfo } }: InitParams) => {\n    // console.log('inited')\n    // if (!status) {\n    //   console.log('init failed:', message)\n    //   global.lx_event.userApi.status(status = { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } })\n    //   return\n    // }\n    apiStatus = status\n      ? { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } }\n      : { status: false, apiInfo: userApi, message }\n    sendStatusChange(apiStatus)\n  }\n  const handleResponse = ({ params: { status, data: { requestKey, result }, message } }: ResponseParams) => {\n    const request = requestQueue.get(requestKey)\n    if (!request) return\n    requestQueue.delete(requestKey)\n    clearRequestTimeout(requestKey)\n    if (status) {\n      request[0](result)\n    } else {\n      request[1](new Error(message))\n    }\n  }\n  const handleOpenDevTools = () => {\n    openDevTools()\n  }\n  const handleShowUpdateAlert = ({ params: { data } }: UpdateInfoParams) => {\n    if (!userApi.allowShowUpdateAlert) return\n    sendShowUpdateAlert({\n      name: userApi.name,\n      description: userApi.description,\n      log: data.log,\n      updateUrl: data.updateUrl,\n    })\n  }\n  const handleGetProxy = () => {\n    sendEvent(USER_API_RENDERER_EVENT_NAME.proxyUpdate, getProxy())\n  }\n  mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit)\n  mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse)\n  mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools)\n  mainOn(USER_API_RENDERER_EVENT_NAME.showUpdateAlert, handleShowUpdateAlert)\n  mainOn(USER_API_RENDERER_EVENT_NAME.getProxy, handleGetProxy)\n}\n\nexport const clearRequestTimeout = (requestKey: string) => {\n  const timeout = timeouts.get(requestKey)\n  if (timeout) {\n    clearTimeout(timeout)\n    timeouts.delete(requestKey)\n  }\n}\n\nexport const loadApi = async(apiId: string) => {\n  if (!apiId) {\n    apiStatus = { status: false, message: 'api id is null' }\n    sendStatusChange(apiStatus)\n    return\n  }\n  const targetApi = getUserApis().find(api => api.id == apiId)\n  if (!targetApi) throw new Error('api not found')\n  userApi = targetApi\n  console.log('load api', userApi.name)\n  await createWindow(userApi)\n  // if (!userApi) return global.lx_event.userApi.status(status = { status: false, message: 'api script is not found' })\n  // if (!global.modules.userApiWindow) {\n  //   global.lx_event.userApi.status(status = { status: false, message: 'user api runtime is not defined' })\n  //   throw new Error('user api window is not defined')\n  // }\n\n  // // const path = require('path')\n  // // // eslint-disable-next-line no-undef\n  // // userApi.script = require('fs').readFileSync(join(process.env.NODE_ENV !== 'production' ? __userApi : __dirname, 'renderer/test-api.js')).toString()\n  // console.log('load api', userApi.name)\n  // mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.init, { userApi })\n}\n\nexport const cancelRequest = (requestKey: string) => {\n  if (!requestQueue.has(requestKey)) return\n  const request = requestQueue.get(requestKey)\n  request[1](new Error('Cancel request'))\n  requestQueue.delete(requestKey)\n  clearRequestTimeout(requestKey)\n}\n\nexport const request = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise<any> => await new Promise((resolve, reject) => {\n  if (!userApi) {\n    reject(new Error('user api is not load'))\n  }\n\n  // const requestKey = `request__${Math.random().toString().substring(2)}`\n  const timeout = timeouts.get(requestKey)\n  if (timeout) {\n    clearTimeout(timeout)\n    timeouts.delete(requestKey)\n    cancelRequest(requestKey)\n  }\n\n  timeouts.set(requestKey, setTimeout(() => {\n    cancelRequest(requestKey)\n  }, 20000))\n\n  requestQueue.set(requestKey, [resolve, reject, data])\n  sendRequest({ requestKey, data })\n})\n\nexport const getStatus = (): LX.UserApi.UserApiStatus => apiStatus\n\nexport const setAllowShowUpdateAlert = (id: string, enable: boolean) => {\n  if (!userApi || userApi.id != id) return\n  userApi.allowShowUpdateAlert = enable\n}\n\nexport const sendRequest = (reqData: { requestKey: string, data: any }) => {\n  sendEvent(USER_API_RENDERER_EVENT_NAME.request, reqData)\n}\n"
  },
  {
    "path": "src/main/modules/userApi/utils.ts",
    "content": "import { userApis as defaultUserApis } from './config'\nimport { STORE_NAMES } from '@common/constants'\nimport getStore from '@main/utils/store'\nimport zlib from 'node:zlib'\n\nlet userApis: LX.UserApi.UserApiInfo[] | null\nlet scripts = new Map<string, string>()\n\nconst saveData = () => {\n  getStore(STORE_NAMES.USER_API).set('userApis', userApis!.map(api => {\n    return {\n      ...api,\n      script: scripts.get(api.id),\n    }\n  }))\n}\n\nexport const getUserApis = (): LX.UserApi.UserApiInfo[] => {\n  if (userApis) return userApis\n\n  const electronStore_userApi = getStore(STORE_NAMES.USER_API)\n  let infoFull = electronStore_userApi.get('userApis') as LX.UserApi.UserApiInfoFull[]\n  let requiredUpdate = false\n  if (infoFull) {\n    for (let i = 0; i < infoFull.length; i++) {\n      const api = infoFull[i]\n      if (api.version != null) continue\n      requiredUpdate ||= true\n      try {\n        infoFull.splice(i, 1, {\n          ...parseScriptInfo(api.script),\n          ...api,\n        })\n      } catch (e) {\n        infoFull.splice(i, 1)\n        i--\n      }\n    }\n  } else {\n    infoFull = defaultUserApis\n    electronStore_userApi.set('userApis', userApis)\n  }\n  userApis = infoFull.map(api => {\n    if (api.allowShowUpdateAlert == null) api.allowShowUpdateAlert = false\n    const { script, ...info } = api\n    scripts.set(api.id, script)\n    return info\n  })\n  if (requiredUpdate) saveData()\n  return userApis\n}\n\nconst INFO_NAMES = {\n  name: 24,\n  description: 36,\n  author: 56,\n  homepage: 1024,\n  version: 36,\n} as const\ntype INFO_NAMES_Type = typeof INFO_NAMES\nconst matchInfo = (scriptInfo: string) => {\n  const infoArr = scriptInfo.split(/\\r?\\n/)\n  const rxp = /^\\s?\\*\\s?@(\\w+)\\s(.+)$/\n  const infos: Partial<Record<keyof typeof INFO_NAMES, string>> = {}\n  for (const info of infoArr) {\n    const result = rxp.exec(info)\n    if (!result) continue\n    const key = result[1] as keyof typeof INFO_NAMES\n    if (INFO_NAMES[key] == null) continue\n    infos[key] = result[2].trim()\n  }\n\n  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]>) {\n    infos[key] ||= ''\n    if (infos[key] == null) infos[key] = ''\n    else if (infos[key].length > len) infos[key] = infos[key].substring(0, len) + '...'\n  }\n\n  return infos as Record<keyof typeof INFO_NAMES, string>\n}\nconst parseScriptInfo = (script: string) => {\n  const result = /^\\/\\*[\\S|\\s]+?\\*\\//.exec(script)\n  if (!result) throw new Error('无效的自定义源文件')\n\n  let scriptInfo = matchInfo(result[0])\n\n  scriptInfo.name ||= `user_api_${new Date().toLocaleString()}`\n  return scriptInfo\n}\nconst deflateScript = async(script: string) => new Promise<string>((resolve, reject) => {\n  zlib.deflate(Buffer.from(script, 'utf8'), (err, buf) => {\n    if (err) {\n      reject(err)\n      return\n    }\n    resolve('gz_' + buf.toString('base64'))\n  })\n})\nconst inflateScript = async(script: string) => new Promise<string>((resolve, reject) => {\n  if (script.startsWith('gz_')) {\n    zlib.inflate(Buffer.from(script.substring(3), 'base64'), (err, buf) => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve(buf.toString('utf8'))\n    })\n  } else resolve(script)\n})\nexport const importApi = async(scriptRaw: string): Promise<LX.UserApi.UserApiInfo> => {\n  let scriptInfo = parseScriptInfo(scriptRaw)\n  const apiInfo = {\n    id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,\n    ...scriptInfo,\n    allowShowUpdateAlert: true,\n  }\n  userApis ??= []\n  userApis.push(apiInfo)\n  const script = await deflateScript(scriptRaw)\n  scripts.set(apiInfo.id, script)\n  saveData()\n  return apiInfo\n}\n\nexport const removeApi = (ids: string[]) => {\n  if (!userApis) return\n  for (let index = userApis.length - 1; index > -1; index--) {\n    if (ids.includes(userApis[index].id)) {\n      scripts.delete(userApis[index].id)\n      userApis.splice(index, 1)\n      ids.splice(index, 1)\n    }\n  }\n  saveData()\n}\n\nexport const setAllowShowUpdateAlert = (id: string, enable: boolean) => {\n  const targetApi = userApis?.find(api => api.id == id)\n  if (!targetApi) return\n  targetApi.allowShowUpdateAlert = enable\n  saveData()\n}\n\nexport const getScript = async(id: string) => {\n  return inflateScript(scripts.get(id) ?? '')\n}\n"
  },
  {
    "path": "src/main/modules/winLyric/config.ts",
    "content": "import { isLinux } from '@common/utils'\nimport { closeWindow, createWindow, getBounds, isExistWindow, alwaysOnTopTools, setBounds, setIgnoreMouseEvents, setSkipTaskbar } from './main'\nimport { sendConfigChange } from './rendererEvent'\nimport { buildLyricConfig, getLyricWindowBounds, initWindowSize, watchConfigKeys } from './utils'\n\nlet isLock: boolean\nlet isEnable: boolean\nlet isAlwaysOnTop: boolean\nlet isAlwaysOnTopLoop: boolean\nlet isShowTaskbar: boolean\nlet isLockScreen: boolean\nlet isHoverHide: boolean\n\n\nexport const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) => {\n  if (!watchConfigKeys.some(key => keys.includes(key))) return\n\n  if (isExistWindow()) {\n    sendConfigChange(buildLyricConfig(setting))\n    if (keys.includes('desktopLyric.isLock') && isLock != global.lx.appSetting['desktopLyric.isLock']) {\n      isLock = global.lx.appSetting['desktopLyric.isLock']\n      if (global.lx.appSetting['desktopLyric.isLock']) {\n        setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })\n      } else {\n        setIgnoreMouseEvents(false, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })\n      }\n    }\n    if (keys.includes('desktopLyric.isHoverHide') && isHoverHide != global.lx.appSetting['desktopLyric.isHoverHide']) {\n      isHoverHide = global.lx.appSetting['desktopLyric.isHoverHide']\n      if (!isLinux) {\n        setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: global.lx.appSetting['desktopLyric.isHoverHide'] })\n      }\n    }\n    if (keys.includes('desktopLyric.isAlwaysOnTop') && isAlwaysOnTop != global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {\n      isAlwaysOnTop = global.lx.appSetting['desktopLyric.isAlwaysOnTop']\n      alwaysOnTopTools.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop'])\n      if (isAlwaysOnTop && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) {\n        alwaysOnTopTools.startLoop()\n      } else alwaysOnTopTools.clearLoop()\n    }\n    if (keys.includes('desktopLyric.isShowTaskbar') && isShowTaskbar != global.lx.appSetting['desktopLyric.isShowTaskbar']) {\n      isShowTaskbar = global.lx.appSetting['desktopLyric.isShowTaskbar']\n      setSkipTaskbar(!global.lx.appSetting['desktopLyric.isShowTaskbar'])\n    }\n    if (keys.includes('desktopLyric.isAlwaysOnTopLoop') && isAlwaysOnTopLoop != global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) {\n      isAlwaysOnTopLoop = global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']\n      if (!global.lx.appSetting['desktopLyric.isAlwaysOnTop']) return\n      if (isAlwaysOnTopLoop) {\n        alwaysOnTopTools.startLoop()\n      } else {\n        alwaysOnTopTools.clearLoop()\n      }\n    }\n    if (keys.includes('desktopLyric.isLockScreen') && isLockScreen != global.lx.appSetting['desktopLyric.isLockScreen']) {\n      isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']\n      if (global.lx.appSetting['desktopLyric.isLockScreen']) {\n        setBounds(getLyricWindowBounds(getBounds(), {\n          x: 0,\n          y: 0,\n          w: global.lx.appSetting['desktopLyric.width'],\n          h: global.lx.appSetting['desktopLyric.height'],\n        }))\n      }\n    }\n    if (keys.includes('desktopLyric.x') && setting['desktopLyric.x'] == null) {\n      setBounds(initWindowSize(\n        global.lx.appSetting['desktopLyric.x'],\n        global.lx.appSetting['desktopLyric.y'],\n        global.lx.appSetting['desktopLyric.width'],\n        global.lx.appSetting['desktopLyric.height'],\n      ))\n    }\n  }\n  if (keys.includes('desktopLyric.enable') && isEnable != global.lx.appSetting['desktopLyric.enable']) {\n    isEnable = global.lx.appSetting['desktopLyric.enable']\n    if (global.lx.appSetting['desktopLyric.enable']) {\n      createWindow()\n    } else {\n      alwaysOnTopTools.clearLoop()\n      closeWindow()\n    }\n  }\n}\n"
  },
  {
    "path": "src/main/modules/winLyric/index.ts",
    "content": "import { APP_EVENT_NAMES } from '@common/constants'\nimport initRendererEvent, { sendMainWindowInitedEvent } from './rendererEvent'\nimport { setLrcConfig } from './config'\nimport { HOTKEY_DESKTOP_LYRIC } from '@common/hotKey'\nimport { closeWindow, createWindow, isExistWindow } from './main'\n// import main from './main'\n// import { Event, EVENT_NAMES } from './event'\n\nlet isMainWidnowFullscreen = false\n\nexport default () => {\n  initRendererEvent()\n  // global.lx.event_app.winLyric = new Event()\n  // global.app_event.winMain.\n\n  global.lx.event_app.on('main_window_inited', () => {\n    isMainWidnowFullscreen = global.lx.appSetting['common.startInFullscreen']\n\n    if (global.lx.appSetting['desktopLyric.enable']) {\n      if (global.lx.appSetting['desktopLyric.fullscreenHide'] && isMainWidnowFullscreen) {\n        closeWindow()\n      } else {\n        if (isExistWindow()) sendMainWindowInitedEvent()\n        else createWindow()\n      }\n    }\n  })\n  global.lx.event_app.on('updated_config', (keys, setting) => {\n    setLrcConfig(keys, setting)\n    if (keys.includes('desktopLyric.fullscreenHide') && global.lx.appSetting['desktopLyric.enable'] && isMainWidnowFullscreen) {\n      if (global.lx.appSetting['desktopLyric.fullscreenHide']) closeWindow()\n      else if (!isExistWindow()) createWindow()\n    }\n  })\n  global.lx.event_app.on('main_window_close', () => {\n    closeWindow()\n  })\n  global.lx.event_app.on('main_window_fullscreen', (isFullscreen) => {\n    isMainWidnowFullscreen = isFullscreen\n    if (global.lx.appSetting['desktopLyric.enable'] && global.lx.appSetting['desktopLyric.fullscreenHide']) {\n      if (isFullscreen) closeWindow()\n      else if (!isExistWindow()) createWindow()\n    }\n  })\n\n\n  // global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.setLyricInfo, info => {\n  //   if (!global.modules.lyricWindow) return\n  //   mainSend(global.modules.lyricWindow, ipcWinLyricNames.set_lyric_info, info)\n  // })\n\n  global.lx.event_app.on('hot_key_down', ({ type, key }) => {\n    let info = global.lx.hotKey.config.global.keys[key]\n    if (!info || info.type != APP_EVENT_NAMES.winLyricName) return\n    let newSetting: Partial<LX.AppSetting> = {}\n    let settingKey: keyof LX.AppSetting\n    switch (info.action) {\n      case HOTKEY_DESKTOP_LYRIC.toggle_visible.action:\n        settingKey = 'desktopLyric.enable'\n        break\n      case HOTKEY_DESKTOP_LYRIC.toggle_lock.action:\n        settingKey = 'desktopLyric.isLock'\n        break\n      case HOTKEY_DESKTOP_LYRIC.toggle_always_top.action:\n        settingKey = 'desktopLyric.isAlwaysOnTop'\n        break\n      default: return\n    }\n    newSetting[settingKey] = !global.lx.appSetting[settingKey]\n\n    global.lx.event_app.update_config(newSetting)\n  })\n}\nexport * from './main'\nexport * from './rendererEvent'\n\n// export {\n//   EVENT_NAMES,\n// }\n"
  },
  {
    "path": "src/main/modules/winLyric/main.ts",
    "content": "import path from 'node:path'\nimport { BrowserWindow } from 'electron'\nimport { debounce, getPlatform, isLinux, isWin } from '@common/utils'\nimport { initWindowSize, minHeight, minWidth } from './utils'\nimport { mainSend } from '@common/mainIpc'\nimport { encodePath } from '@common/utils/electron'\n\n// require('./event')\n// require('./rendererEvent')\n\nlet browserWindow: Electron.BrowserWindow | null = null\nlet isWinBoundsUpdateing = false\n\nconst saveBoundsConfig = debounce((config: Partial<LX.AppSetting>) => {\n  global.lx.event_app.update_config(config)\n  if (isWinBoundsUpdateing) isWinBoundsUpdateing = false\n}, 500)\n\nconst winEvent = () => {\n  if (!browserWindow) return\n\n  // browserWindow.on('close', () => {\n  //   if (global.lx.appSetting['desktopLyric.enable'] && !global.lx.mainWindowClosed) {\n  //     browserWindow = null\n  //     global.lx.event_app.update_config({ 'desktopLyric.enable': false })\n  //   }\n  // })\n\n  browserWindow.on('closed', () => {\n    browserWindow = null\n  })\n\n  browserWindow.on('move', () => {\n    // bounds = browserWindow.getBounds()\n    // console.log('move', isWinBoundsUpdateing)\n    if (isWinBoundsUpdateing) {\n      const bounds = browserWindow!.getBounds()\n      saveBoundsConfig({\n        'desktopLyric.x': bounds.x,\n        'desktopLyric.y': bounds.y,\n        'desktopLyric.width': bounds.width,\n        'desktopLyric.height': bounds.height,\n      })\n    } else if (isWin) { // Linux 不允许将窗口设置出屏幕之外，MacOS未知，故只在Windows下执行强制设置\n      // 非主动调整窗口触发的窗口位置变化将重置回设置值\n      browserWindow!.setBounds({\n        x: global.lx.appSetting['desktopLyric.x'] ?? 0,\n        y: global.lx.appSetting['desktopLyric.y'] ?? 0,\n        width: global.lx.appSetting['desktopLyric.width'],\n        height: global.lx.appSetting['desktopLyric.height'],\n      })\n    }\n  })\n\n  browserWindow.on('resize', () => {\n    // bounds = browserWindow.getBounds()\n    // console.log(bounds)\n    isWinBoundsUpdateing = true\n    const bounds = browserWindow!.getBounds()\n    saveBoundsConfig({\n      'desktopLyric.x': bounds.x,\n      'desktopLyric.y': bounds.y,\n      'desktopLyric.width': bounds.width,\n      'desktopLyric.height': bounds.height,\n    })\n  })\n\n  // browserWindow.on('restore', () => {\n  //   browserWindow.webContents.send('restore')\n  // })\n  // browserWindow.on('focus', () => {\n  //   browserWindow.webContents.send('focus')\n  // })\n\n  browserWindow.once('ready-to-show', () => {\n    showWindow()\n    if (global.lx.appSetting['desktopLyric.isLock']) {\n      browserWindow!.setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })\n    }\n    // linux下每次重开时貌似要重新设置置顶\n    // if (isLinux && global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {\n    //   browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')\n    // }\n    if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop()\n    browserWindow!.blur()\n  })\n}\n\nexport const createWindow = () => {\n  closeWindow()\n  if (!global.envParams.workAreaSize) return\n  let x = global.lx.appSetting['desktopLyric.x']\n  let y = global.lx.appSetting['desktopLyric.y']\n  let width = global.lx.appSetting['desktopLyric.width']\n  let height = global.lx.appSetting['desktopLyric.height']\n  let isAlwaysOnTop = global.lx.appSetting['desktopLyric.isAlwaysOnTop']\n  // let isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']\n  let isShowTaskbar = global.lx.appSetting['desktopLyric.isShowTaskbar']\n  // let { width: screenWidth, height: screenHeight } = global.envParams.workAreaSize\n  const winSize = initWindowSize(x, y, width, height)\n  global.lx.event_app.update_config({\n    'desktopLyric.x': winSize.x,\n    'desktopLyric.y': winSize.y,\n    'desktopLyric.width': winSize.width,\n    'desktopLyric.height': winSize.height,\n  })\n\n  const { shouldUseDarkColors, theme } = global.lx.theme\n\n  /**\n   * Initial window options\n   */\n  browserWindow = new BrowserWindow({\n    height: winSize.height,\n    width: winSize.width,\n    x: winSize.x,\n    y: winSize.y,\n    minWidth,\n    minHeight,\n    useContentSize: true,\n    frame: false,\n    transparent: true,\n    hasShadow: false,\n    // enableRemoteModule: false,\n    // icon: join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'),\n    resizable: isWin,\n    minimizable: false,\n    maximizable: false,\n    fullscreenable: false,\n    roundedCorners: false,\n    show: false,\n    alwaysOnTop: isAlwaysOnTop,\n    skipTaskbar: !isShowTaskbar,\n    webPreferences: {\n      contextIsolation: false,\n      webSecurity: false,\n      sandbox: false,\n      nodeIntegration: true,\n      enableWebSQL: false,\n      webgl: false,\n      spellcheck: false, // 禁用拼写检查器\n      backgroundThrottling: false,\n    },\n  })\n\n  const winURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:9081/lyric.html' : `file://${path.join(encodePath(__dirname), 'lyric.html')}`\n  void browserWindow.loadURL(winURL + `?os=${getPlatform()}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`)\n\n  winEvent()\n  // browserWindow.webContents.openDevTools()\n  global.lx.event_app.desktop_lyric_window_created(browserWindow)\n}\nexport const isExistWindow = (): boolean => !!browserWindow\n\nexport const closeWindow = () => {\n  if (!browserWindow) return\n  browserWindow.close()\n}\n\nexport const showWindow = () => {\n  if (!browserWindow) return\n  browserWindow.show()\n}\n\nexport const setResizeable = (isResizeable: boolean) => {\n  if (!browserWindow) return\n  browserWindow.setResizable(isResizeable)\n}\n\nexport const sendEvent = <T = any>(name: string, params?: T) => {\n  if (!browserWindow) return\n  mainSend(browserWindow, name, params)\n}\n\nexport const getBounds = (): Electron.Rectangle => {\n  if (!browserWindow) throw new Error('window is not available')\n  return browserWindow.getBounds()\n}\n\nexport const setBounds = (bounds: Electron.Rectangle) => {\n  if (!browserWindow) return\n  isWinBoundsUpdateing = true\n  browserWindow.setBounds(bounds)\n}\n\n\nexport const setIgnoreMouseEvents = (ignore: boolean, options?: Electron.IgnoreMouseEventsOptions) => {\n  if (!browserWindow) return\n  browserWindow.setIgnoreMouseEvents(ignore, options)\n}\n\nexport const setSkipTaskbar = (skip: boolean) => {\n  if (!browserWindow) return\n  browserWindow.setSkipTaskbar(skip)\n}\n\nexport const setAlwaysOnTop = (flag: boolean, level?: 'normal' | 'floating' | 'torn-off-menu' | 'modal-panel' | 'main-menu' | 'status' | 'pop-up-menu' | 'screen-saver' | undefined, relativeLevel?: number | undefined) => {\n  if (!browserWindow) return\n  browserWindow.setAlwaysOnTop(flag, level, relativeLevel)\n}\n\nexport const getMainFrame = (): Electron.WebFrameMain | null => {\n  if (!browserWindow) return null\n  return browserWindow.webContents.mainFrame\n}\n\ninterface AlwaysOnTopTools {\n  timeout: NodeJS.Timeout | null\n  setAlwaysOnTop: (isLoop: boolean) => void\n  startLoop: () => void\n  clearLoop: () => void\n}\nexport const alwaysOnTopTools: AlwaysOnTopTools = {\n  timeout: null,\n  setAlwaysOnTop(isLoop) {\n    this.clearLoop()\n    setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')\n    // console.log(isLoop)\n    if (isLoop) this.startLoop()\n  },\n  startLoop() {\n    this.clearLoop()\n    this.timeout = setInterval(() => {\n      if (!isExistWindow()) {\n        this.clearLoop()\n        return\n      }\n      setAlwaysOnTop(true, 'screen-saver')\n    }, 500)\n  },\n  clearLoop() {\n    if (!this.timeout) return\n    clearInterval(this.timeout)\n    this.timeout = null\n  },\n}\n"
  },
  {
    "path": "src/main/modules/winLyric/rendererEvent.ts",
    "content": "import { registerRendererEvents as common } from '@main/modules/commonRenderers/common'\nimport { mainOn, mainHandle } from '@common/mainIpc'\nimport { WIN_LYRIC_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { buildLyricConfig, getLyricWindowBounds } from './utils'\nimport { sendNewDesktopLyricClient } from '@main/modules/winMain'\nimport { getBounds, getMainFrame, sendEvent, setBounds, setResizeable } from './main'\nimport { MessageChannelMain } from 'electron'\n\n\nexport default () => {\n  // mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.get_lyric_info, ({ params: action }) => {\n  //   sendMainEvent(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_info, {\n  //     name: WIN_LYRIC_RENDERER_EVENT_NAME.set_lyric_info,\n  //     modal: 'lyricWindow',\n  //     action,\n  //   })\n  // })\n  common(sendEvent)\n\n  mainHandle<Partial<LX.AppSetting>>(WIN_LYRIC_RENDERER_EVENT_NAME.set_config, async({ params: config }) => {\n    global.lx.event_app.update_config(config)\n  })\n\n  mainHandle<LX.DesktopLyric.Config>(WIN_LYRIC_RENDERER_EVENT_NAME.get_config, async() => {\n    return buildLyricConfig(global.lx.appSetting) as LX.DesktopLyric.Config\n  })\n\n  mainOn<LX.DesktopLyric.NewBounds>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, ({ params: options }) => {\n    setBounds(getLyricWindowBounds(getBounds(), options))\n  })\n\n  mainOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, ({ params: resizable }) => {\n    setResizeable(resizable)\n  })\n\n  mainOn(WIN_LYRIC_RENDERER_EVENT_NAME.request_main_window_channel, ({ event }) => {\n    if (event.senderFrame !== getMainFrame()) return\n    // Create a new channel ...\n    const { port1, port2 } = new MessageChannelMain()\n    // ... send one end to the worker ...\n    sendNewDesktopLyricClient(port1)\n    // ... and the other end to the main window.\n    event.senderFrame?.postMessage(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, null, [port2])\n    // Now the main window and the worker can communicate with each other\n    // without going through the main process!\n    console.log('request_main_window_channel')\n  })\n}\n\nexport const sendConfigChange = (setting: Partial<LX.DesktopLyric.Config>) => {\n  sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, setting)\n}\n\nexport const sendMainWindowInitedEvent = () => {\n  sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited)\n}\n\n"
  },
  {
    "path": "src/main/modules/winLyric/utils.ts",
    "content": "// 设置窗口位置、大小\nexport let minWidth = 38\nexport let minHeight = 38\n\n\n// const updateBounds = (bounds: Bounds) => {\n//   bounds.x = bounds.x\n//   return bounds\n// }\n\n/**\n *\n * @param bounds 当前设置\n * @param param 新设置（相对于当前设置）\n * @returns\n */\nexport const getLyricWindowBounds = (bounds: Electron.Rectangle, { x, y, w, h }: LX.DesktopLyric.NewBounds): Electron.Rectangle => {\n  if (w < minWidth) w = minWidth\n  if (h < minHeight) h = minHeight\n\n  if (global.lx.appSetting['desktopLyric.isLockScreen']) {\n    if (!global.envParams.workAreaSize) return bounds\n    const maxWinW = global.envParams.workAreaSize.width\n    const maxWinH = global.envParams.workAreaSize.height\n\n    if (w > maxWinW) w = maxWinW\n    if (h > maxWinH) h = maxWinH\n\n    const maxX = global.envParams.workAreaSize.width - w\n    const maxY = global.envParams.workAreaSize.height - h\n\n    x += bounds.x\n    y += bounds.y\n\n    if (x > maxX) x = maxX\n    else if (x < 0) x = 0\n\n    if (y > maxY) y = maxY\n    else if (y < 0) y = 0\n  } else {\n    y += bounds.y\n    x += bounds.x\n  }\n\n  // console.log('util bounds', bounds)\n  return { width: w, height: h, x, y }\n}\n\n\nexport const watchConfigKeys = [\n  'desktopLyric.enable',\n  'desktopLyric.isLock',\n  'desktopLyric.isAlwaysOnTop',\n  'desktopLyric.isAlwaysOnTopLoop',\n  'desktopLyric.isShowTaskbar',\n  'desktopLyric.pauseHide',\n  'desktopLyric.audioVisualization',\n  'desktopLyric.width',\n  'desktopLyric.height',\n  'desktopLyric.x',\n  'desktopLyric.y',\n  'desktopLyric.isLockScreen',\n  'desktopLyric.isDelayScroll',\n  'desktopLyric.scrollAlign',\n  'desktopLyric.isHoverHide',\n  'desktopLyric.direction',\n  'desktopLyric.style.align',\n  'desktopLyric.style.lyricUnplayColor',\n  'desktopLyric.style.lyricPlayedColor',\n  'desktopLyric.style.lyricShadowColor',\n  'desktopLyric.style.font',\n  'desktopLyric.style.fontSize',\n  'desktopLyric.style.lineGap',\n  // 'desktopLyric.style.fontWeight',\n  'desktopLyric.style.opacity',\n  'desktopLyric.style.ellipsis',\n  'desktopLyric.style.isFontWeightFont',\n  'desktopLyric.style.isFontWeightLine',\n  'desktopLyric.style.isFontWeightExtended',\n  'desktopLyric.style.isZoomActiveLrc',\n  'common.langId',\n  'player.isShowLyricTranslation',\n  'player.isShowLyricRoma',\n  'player.isSwapLyricTranslationAndRoma',\n  'player.isPlayLxlrc',\n  'player.playbackRate',\n] satisfies Array<keyof LX.AppSetting>\n\nexport const buildLyricConfig = (appSetting: Partial<LX.AppSetting>): Partial<LX.DesktopLyric.Config> => {\n  const setting: Partial<LX.DesktopLyric.Config> = {}\n  for (const key of watchConfigKeys) {\n    // @ts-expect-error\n    if (key in appSetting) setting[key] = appSetting[key]\n  }\n  return setting\n}\n\nexport const initWindowSize = (x: LX.AppSetting['desktopLyric.x'], y: LX.AppSetting['desktopLyric.y'], width: LX.AppSetting['desktopLyric.width'], height: LX.AppSetting['desktopLyric.height']) => {\n  if (x == null || y == null) {\n    if (width < minWidth) width = minWidth\n    if (height < minHeight) height = minHeight\n    if (global.envParams.workAreaSize) {\n      x = global.envParams.workAreaSize.width - width\n      y = global.envParams.workAreaSize.height - height\n    } else {\n      x = y = 0\n    }\n  } else {\n    let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height })\n    x = bounds.x\n    y = bounds.y\n    width = bounds.width\n    height = bounds.height\n  }\n  return {\n    x,\n    y,\n    width,\n    height,\n  }\n}\n"
  },
  {
    "path": "src/main/modules/winMain/autoUpdate.ts",
    "content": "import { autoUpdater } from 'electron-updater'\nimport { log, isWin } from '@common/utils'\nimport { mainOn } from '@common/mainIpc'\nimport { isExistWindow, sendEvent } from './index'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\n\nautoUpdater.logger = log\nautoUpdater.autoDownload = false\n// autoUpdater.forceDevUpdateConfig = true\n// autoUpdater.autoDownload = false\n\n// let isFirstCheckedUpdate = true\n\nlog.info('App starting...')\n\n\n// -------------------------------------------------------------------\n// Open a window that displays the version\n//\n// THIS SECTION IS NOT REQUIRED\n//\n// This isn't required for auto-updates to work, but it's easier\n// for the app to show a window than to have to click \"About\" to see\n// that updates are working.\n// -------------------------------------------------------------------\n// let win\n\nfunction sendStatusToWindow(text: string) {\n  log.info(text)\n  // ipcMain.send('message', text)\n}\n\n\n// -------------------------------------------------------------------\n// Auto updates\n//\n// For details about these events, see the Wiki:\n// https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events\n//\n// The app doesn't need to listen to any events except `update-downloaded`\n//\n// Uncomment any of the below events to listen for them.  Also,\n// look in the previous section to see them being used.\n// -------------------------------------------------------------------\n// autoUpdater.on('checking-for-update', () => {\n// })\n// autoUpdater.on('update-available', (ev, info) => {\n// })\n// autoUpdater.on('update-not-available', (ev, info) => {\n// })\n// autoUpdater.on('error', (ev, err) => {\n// })\n// autoUpdater.on('download-progress', (ev, progressObj) => {\n// })\n// autoUpdater.on('update-downloaded', (ev, info) => {\n//   // Wait 5 seconds, then quit and install\n//   // In your application, you don't need to wait 5 seconds.\n//   // You could call autoUpdater.quitAndInstall(); immediately\n//   // setTimeout(function() {\n//   // autoUpdater.quitAndInstall()\n//   // }, 5000)\n\n// })\n\ninterface WaitEvent {\n  type: string\n  info: any\n}\n\n// let waitEvent: WaitEvent[] = []\nconst handleSendEvent = (action: WaitEvent) => {\n  if (isExistWindow()) {\n    setTimeout(() => { // 延迟发送事件，过早发送可能渲染进程还没启动完成\n      sendEvent(action.type, action.info)\n    }, 1000)\n  }\n}\n\nexport default () => {\n  autoUpdater.on('checking-for-update', () => {\n    sendStatusToWindow('Checking for update...')\n  })\n  autoUpdater.on('update-available', info => {\n    sendStatusToWindow('Update available.')\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_available, info })\n  })\n  autoUpdater.on('update-not-available', info => {\n    sendStatusToWindow('Update not available.')\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, info })\n  })\n  autoUpdater.on('error', err => {\n    sendStatusToWindow('Error in auto-updater.')\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_error, info: err.message })\n  })\n  autoUpdater.on('download-progress', progressObj => {\n    let log_message = `Download speed: ${progressObj.bytesPerSecond}`\n    log_message = `${log_message} - Downloaded ${progressObj.percent}%`\n    log_message = `${log_message} (progressObj.transferred/${progressObj.total})`\n    sendStatusToWindow(log_message)\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_progress, info: progressObj })\n  })\n  autoUpdater.on('update-downloaded', info => {\n    sendStatusToWindow('Update downloaded.')\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, info })\n  })\n\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_check, () => {\n    console.log('check')\n    checkUpdate()\n  })\n\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_download_update, () => {\n    if (!autoUpdater.isUpdaterActive()) return\n    void autoUpdater.downloadUpdate()\n  })\n\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.quit_update, () => {\n    global.lx.isSkipTrayQuit = true\n\n    setTimeout(() => {\n      autoUpdater.quitAndInstall(true, true)\n    }, 1000)\n  })\n}\n\nconst checkUpdate = () => {\n  // if (!isFirstCheckedUpdate) {\n  //   if (waitEvent.length) {\n  //     waitEvent.forEach((event, index) => {\n  //       setTimeout(() => { // 延迟发送事件，过早发送可能渲染进程还没启动完成\n  //         sendEvent(event.type, event.info)\n  //       }, 2000 * (index + 1))\n  //     })\n  //     waitEvent = []\n  //   }\n  //   return\n  // }\n  // isFirstCheckedUpdate = false\n\n  // 由于集合安装包中不包含win arm版，这将会导致arm版更新失败\n  if (isWin && process.arch.includes('arm')) {\n    handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_error, info: 'failed' })\n  } else {\n    autoUpdater.autoDownload = global.lx.appSetting['common.tryAutoUpdate']\n    void autoUpdater.checkForUpdates()\n  }\n}\n"
  },
  {
    "path": "src/main/modules/winMain/index.ts",
    "content": "import initRendererEvent, { handleKeyDown, hotKeyConfigUpdate } from './rendererEvent'\n\nimport { APP_EVENT_NAMES } from '@common/constants'\nimport { createWindow, minimize, setProgressBar, setProxy, setThumbarButtons, toggleHide, toggleMinimize } from './main'\nimport initUpdate from './autoUpdate'\nimport { HOTKEY_COMMON } from '@common/hotKey'\nimport { quitApp } from '@main/app'\n\nexport default () => {\n  initRendererEvent()\n  initUpdate()\n\n  global.lx.event_app.on('hot_key_down', ({ type, key }) => {\n    let info = global.lx.hotKey.config.global.keys[key]\n    if (info?.type != APP_EVENT_NAMES.winMainName) return\n    switch (info.action) {\n      case HOTKEY_COMMON.close.action:\n        quitApp()\n        break\n      case HOTKEY_COMMON.hide_toggle.action:\n        toggleHide()\n        break\n      case HOTKEY_COMMON.min.action:\n        minimize()\n        break\n      case HOTKEY_COMMON.min_toggle.action:\n        toggleMinimize()\n        break\n      default:\n        handleKeyDown(type, key)\n        break\n    }\n  })\n  global.lx.event_app.on('hot_key_config_update', (config) => {\n    hotKeyConfigUpdate(config)\n  })\n\n  global.lx.event_app.on('app_inited', () => {\n    createWindow()\n  })\n\n  const keys = (['status', 'collect'] as const) satisfies Array<keyof LX.Player.Status>\n  const taskBarButtonFlags: LX.TaskBarButtonFlags = {\n    empty: true,\n    collect: false,\n    play: false,\n    next: true,\n    prev: true,\n  }\n  const progressStatus = {\n    progress: -1,\n    status: 'none' as Electron.ProgressBarOptions['mode'],\n  }\n  let showProgress = global.lx.appSetting['player.isShowTaskProgess']\n  global.lx.event_app.on('player_status', (status) => {\n    if (status.status) {\n      switch (status.status) {\n        case 'paused':\n          taskBarButtonFlags.play = false\n          taskBarButtonFlags.empty &&= false\n          progressStatus.status = 'paused'\n          break\n        case 'error':\n          taskBarButtonFlags.play = false\n          taskBarButtonFlags.empty &&= false\n          progressStatus.status = 'error'\n          break\n        case 'playing':\n          taskBarButtonFlags.play = true\n          taskBarButtonFlags.empty &&= false\n          progressStatus.status = 'normal'\n          break\n        case 'stoped':\n          taskBarButtonFlags.play &&= false\n          taskBarButtonFlags.empty = true\n          progressStatus.status = 'none'\n          progressStatus.progress = 0\n          break\n      }\n      if (showProgress) {\n        setProgressBar(progressStatus.progress, {\n          mode: progressStatus.status,\n        })\n      }\n    }\n    if (keys.some(k => status[k] != null)) {\n      if (status.collect != null) taskBarButtonFlags.collect = status.collect\n      setThumbarButtons(taskBarButtonFlags)\n    }\n    if (showProgress && status.progress != null) {\n      const progress = global.lx.player_status.duration ? status.progress / global.lx.player_status.duration : 0\n      if (progress.toFixed(2) != progressStatus.progress.toFixed(2)) {\n        progressStatus.progress = progress < 0.01 ? 0.01 : progress\n        setProgressBar(progressStatus.progress, {\n          mode: progressStatus.status,\n        })\n      }\n    }\n  })\n  global.lx.event_app.on('updated_config', (keys, setting) => {\n    if (keys.includes('player.isShowTaskProgess')) {\n      showProgress = setting['player.isShowTaskProgess']!\n      if (showProgress) {\n        setProgressBar(progressStatus.progress, {\n          mode: progressStatus.status,\n        })\n      } else {\n        setProgressBar(-1, { mode: 'none' })\n      }\n    }\n    if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) {\n      setProxy()\n    }\n  })\n}\n\nexport * from './main'\nexport * from './rendererEvent'\n\n"
  },
  {
    "path": "src/main/modules/winMain/main.ts",
    "content": "import { BrowserWindow, dialog, session } from 'electron'\nimport path from 'node:path'\nimport { createTaskBarButtons, getWindowSizeInfo } from './utils'\nimport { getPlatform, isLinux, isWin } from '@common/utils'\nimport { getProxy, openDevTools as handleOpenDevTools } from '@main/utils'\nimport { mainSend } from '@common/mainIpc'\nimport { sendFocus, sendTaskbarButtonClick } from './rendererEvent'\nimport { encodePath } from '@common/utils/electron'\n\nlet browserWindow: Electron.BrowserWindow | null = null\n\nconst winEvent = () => {\n  if (!browserWindow) return\n\n  browserWindow.on('close', event => {\n    if (global.lx.isSkipTrayQuit || !global.lx.appSetting['tray.enable']) {\n      browserWindow!.setProgressBar(-1)\n      // global.lx.mainWindowClosed = true\n      global.lx.event_app.main_window_close()\n      return\n    }\n\n    event.preventDefault()\n    browserWindow!.hide()\n  })\n\n  browserWindow.on('closed', () => {\n    // global.lx.mainWindowClosed = true\n    browserWindow = null\n  })\n\n  // browserWindow.on('restore', () => {\n  //   browserWindow.webContents.send('restore')\n  // })\n  browserWindow.on('focus', () => {\n    sendFocus()\n    global.lx.event_app.main_window_focus()\n  })\n\n  browserWindow.on('blur', () => {\n    global.lx.event_app.main_window_blur()\n  })\n\n  browserWindow.once('ready-to-show', () => {\n    if (!global.envParams.cmdParams.hidden) {\n      showWindow()\n      setThumbarButtons()\n    }\n    global.lx.event_app.main_window_ready_to_show()\n  })\n\n  browserWindow.on('show', () => {\n    global.lx.event_app.main_window_show()\n\n    // 修复隐藏窗口后再显示时任务栏按钮丢失的问题\n    setThumbarButtons()\n  })\n  browserWindow.on('hide', () => {\n    global.lx.event_app.main_window_hide()\n  })\n}\n\n\nexport const createWindow = () => {\n  closeWindow()\n  const windowSizeInfo = getWindowSizeInfo(global.lx.appSetting['common.windowSizeId'])\n\n  const { shouldUseDarkColors, theme } = global.lx.theme\n  const ses = session.fromPartition('persist:win-main')\n  const proxy = getProxy()\n  setSesProxy(ses, proxy?.host, proxy?.port)\n\n  /**\n   * Initial window options\n   */\n  const options: Electron.BrowserWindowConstructorOptions = {\n    height: windowSizeInfo.height,\n    useContentSize: true,\n    width: windowSizeInfo.width,\n    frame: false,\n    transparent: !global.envParams.cmdParams.dt,\n    hasShadow: global.envParams.cmdParams.dt,\n    // enableRemoteModule: false,\n    // icon: join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'),\n    resizable: false,\n    maximizable: false,\n    fullscreenable: true,\n    roundedCorners: global.envParams.cmdParams.dt,\n    show: false,\n    webPreferences: {\n      session: ses,\n      nodeIntegrationInWorker: true,\n      contextIsolation: false,\n      webSecurity: false,\n      nodeIntegration: true,\n      sandbox: false,\n      enableWebSQL: false,\n      webgl: false,\n      spellcheck: false, // 禁用拼写检查器\n    },\n  }\n  if (global.envParams.cmdParams.dt) options.backgroundColor = theme.colors['--color-primary-light-1000']\n  if (global.lx.appSetting['common.startInFullscreen']) {\n    options.fullscreen = true\n    if (isLinux) options.resizable = true\n  }\n  browserWindow = new BrowserWindow(options)\n\n  const winURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:9080' : `file://${path.join(encodePath(__dirname), 'index.html')}`\n  void browserWindow.loadURL(winURL + `?os=${getPlatform()}&dt=${global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`)\n\n  winEvent()\n\n  if (global.envParams.cmdParams.odt) handleOpenDevTools(browserWindow.webContents)\n\n  // global.lx.mainWindowClosed = false\n  // browserWindow.webContents.openDevTools()\n  global.lx.event_app.main_window_created(browserWindow)\n}\n\nexport const isExistWindow = (): boolean => !!browserWindow\nexport const isShowWindow = (): boolean => {\n  if (!browserWindow) return false\n  return browserWindow.isVisible() && (isWin ? true : browserWindow.isFocused())\n}\n\nexport const closeWindow = () => {\n  if (!browserWindow) return\n  browserWindow.close()\n}\n\nconst setSesProxy = (ses: Electron.Session, host?: string, port?: string | number) => {\n  if (host) {\n    void ses.setProxy({\n      mode: 'fixed_servers',\n      proxyRules: `http://${host}:${port}`,\n    })\n  } else {\n    void ses.setProxy({\n      mode: 'direct',\n    })\n  }\n}\nexport const setProxy = () => {\n  if (!browserWindow) return\n  const proxy = getProxy()\n  setSesProxy(browserWindow.webContents.session, proxy?.host, proxy?.port)\n}\n\n\nexport const sendEvent = <T = any>(name: string, params?: T) => {\n  if (!browserWindow) return\n  mainSend(browserWindow, name, params)\n}\n\nexport const showSelectDialog = async(options: Electron.OpenDialogOptions) => {\n  if (!browserWindow) throw new Error('main window is undefined')\n  return dialog.showOpenDialog(browserWindow, options)\n}\nexport const showDialog = ({ type, message, detail }: Electron.MessageBoxSyncOptions) => {\n  if (!browserWindow) return\n  dialog.showMessageBoxSync(browserWindow, {\n    type,\n    message,\n    detail,\n  })\n}\nexport const showSaveDialog = async(options: Electron.SaveDialogOptions) => {\n  if (!browserWindow) throw new Error('main window is undefined')\n  return dialog.showSaveDialog(browserWindow, options)\n}\nexport const minimize = () => {\n  if (!browserWindow) return\n  browserWindow.minimize()\n}\nexport const maximize = () => {\n  if (!browserWindow) return\n  browserWindow.maximize()\n}\nexport const unmaximize = () => {\n  if (!browserWindow) return\n  browserWindow.unmaximize()\n}\nexport const toggleHide = () => {\n  if (!browserWindow) return\n  browserWindow.isVisible()\n    ? browserWindow.hide()\n    : browserWindow.show()\n}\nexport const toggleMinimize = () => {\n  if (!browserWindow) return\n  if (browserWindow.isVisible()) {\n    if (browserWindow.isMinimized()) browserWindow.restore()\n    else browserWindow.minimize()\n  } else browserWindow.show()\n}\nexport const showWindow = () => {\n  if (!browserWindow) return\n  if (browserWindow.isVisible()) {\n    if (browserWindow.isMinimized()) browserWindow.restore()\n    else browserWindow.focus()\n  } else browserWindow.show()\n}\nexport const hideWindow = () => {\n  if (!browserWindow) return\n  browserWindow.hide()\n}\nexport const setWindowBounds = (options: Partial<Electron.Rectangle>) => {\n  if (!browserWindow) return\n  browserWindow.setBounds(options)\n}\nexport const setProgressBar = (progress: number, options?: Electron.ProgressBarOptions) => {\n  if (!browserWindow) return\n  browserWindow.setProgressBar(progress, options)\n}\nexport const setIgnoreMouseEvents = (ignore: boolean, options?: Electron.IgnoreMouseEventsOptions) => {\n  if (!browserWindow) return\n  browserWindow.setIgnoreMouseEvents(ignore, options)\n}\nexport const toggleDevTools = () => {\n  if (!browserWindow) return\n  if (browserWindow.webContents.isDevToolsOpened()) {\n    browserWindow.webContents.closeDevTools()\n  } else {\n    handleOpenDevTools(browserWindow.webContents)\n  }\n}\n\nexport const setFullScreen = (isFullscreen: boolean): boolean => {\n  if (!browserWindow) return false\n  if (isLinux) { // linux 需要先设置为可调整窗口大小才能全屏\n    if (isFullscreen) {\n      browserWindow.setResizable(isFullscreen)\n      browserWindow.setFullScreen(isFullscreen)\n    } else {\n      browserWindow.setFullScreen(isFullscreen)\n      browserWindow.setResizable(isFullscreen)\n    }\n  } else {\n    browserWindow.setFullScreen(isFullscreen)\n  }\n  return isFullscreen\n}\n\nconst taskBarButtonFlags: LX.TaskBarButtonFlags = {\n  empty: true,\n  collect: false,\n  play: false,\n  next: true,\n  prev: true,\n}\nexport const setThumbarButtons = ({ empty, collect, play, next, prev }: LX.TaskBarButtonFlags = taskBarButtonFlags) => {\n  if (!isWin || !browserWindow) return\n  taskBarButtonFlags.empty = empty\n  taskBarButtonFlags.collect = collect\n  taskBarButtonFlags.play = play\n  taskBarButtonFlags.next = next\n  taskBarButtonFlags.prev = prev\n  browserWindow.setThumbarButtons(createTaskBarButtons(taskBarButtonFlags, action => {\n    sendTaskbarButtonClick(action)\n  }))\n}\n\nexport const setThumbnailClip = (region: Electron.Rectangle) => {\n  if (!browserWindow) return\n  browserWindow.setThumbnailClip(region)\n}\n\n\nexport const clearCache = async() => {\n  if (!browserWindow) throw new Error('main window is undefined')\n  await browserWindow.webContents.session.clearCache()\n}\n\nexport const getCacheSize = async() => {\n  if (!browserWindow) throw new Error('main window is undefined')\n  return browserWindow.webContents.session.getCacheSize()\n}\n\nexport const getWebContents = (): Electron.WebContents => {\n  if (!browserWindow) throw new Error('main window is undefined')\n  return browserWindow.webContents\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/app.ts",
    "content": "// const path = require('path')\nimport { app } from 'electron'\nimport { mainHandle, mainOn } from '@common/mainIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\n// import { name as defaultName } from '../../../../../package.json'\nimport {\n  minimize,\n  maximize,\n  closeWindow,\n  showWindow,\n  setFullScreen,\n  sendEvent,\n  clearCache,\n  getCacheSize,\n  toggleDevTools,\n  setWindowBounds,\n  setIgnoreMouseEvents,\n  // setThumbnailClip,\n  toggleMinimize,\n  toggleHide,\n  showSelectDialog,\n  showDialog,\n  showSaveDialog,\n} from '@main/modules/winMain'\nimport { quitApp } from '@main/app'\nimport { getAllThemes, removeTheme, saveTheme, setPowerSaveBlocker } from '@main/utils'\nimport { openDirInExplorer } from '@common/utils/electron'\n\nexport default () => {\n  // 设置应用名称\n  // mainOn(WIN_MAIN_RENDERER_EVENT_NAME.set_app_name, ({ params: name }) => {\n  //   if (name == null) {\n  //     app.setName(defaultName)\n  //   } else {\n  //     app.setName(name)\n  //   }\n  // })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.quit, () => {\n    quitApp()\n  })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.min_toggle, () => {\n    toggleMinimize()\n  })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.hide_toggle, () => {\n    toggleHide()\n  })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.min, () => {\n    minimize()\n  })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.max, () => {\n    maximize()\n  })\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.focus, () => {\n    showWindow()\n  })\n  mainOn<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.set_power_save_blocker, ({ params: enabled }) => {\n    setPowerSaveBlocker(enabled)\n  })\n  mainOn<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.close, ({ params: isForce }) => {\n    if (isForce) {\n      app.exit(0)\n      return\n    }\n    closeWindow()\n  })\n  // 全屏\n  mainHandle<boolean, boolean>(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, async({ params: isFullscreen }) => {\n    global.lx.event_app.main_window_fullscreen(isFullscreen)\n    return setFullScreen(isFullscreen)\n  })\n\n  // 选择目录\n  mainHandle<Electron.OpenDialogOptions, Electron.OpenDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_select_dialog, async({ params: options }) => {\n    return showSelectDialog(options)\n  })\n  // 显示弹窗信息\n  mainOn<Electron.MessageBoxSyncOptions>(WIN_MAIN_RENDERER_EVENT_NAME.show_dialog, ({ params }) => {\n    showDialog(params)\n  })\n  // 显示保存弹窗\n  mainHandle<Electron.SaveDialogOptions, Electron.SaveDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_save_dialog, async({ params }) => {\n    return showSaveDialog(params)\n  })\n  // 在资源管理器中定位文件\n  mainOn<string>(WIN_MAIN_RENDERER_EVENT_NAME.open_dir_in_explorer, async({ params }) => {\n    return openDirInExplorer(params)\n  })\n\n\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache, async() => {\n    await clearCache()\n  })\n\n  mainHandle<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size, async() => {\n    return getCacheSize()\n  })\n\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.open_dev_tools, () => {\n    toggleDevTools()\n  })\n\n  mainOn<Partial<Electron.Rectangle>>(WIN_MAIN_RENDERER_EVENT_NAME.set_window_size, ({ params }) => {\n    setWindowBounds(params)\n  })\n\n  mainOn<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.set_ignore_mouse_events, ({ params: isIgnored }) => {\n    isIgnored\n      ? setIgnoreMouseEvents(isIgnored, { forward: true })\n      : setIgnoreMouseEvents(false)\n  })\n\n  // mainHandle<Electron.Rectangle>(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_set_thumbnail_clip, async({ params }) => {\n  //   return setThumbnailClip(params)\n  // })\n\n  mainOn<LX.Player.Status>(WIN_MAIN_RENDERER_EVENT_NAME.player_status, ({ params }) => {\n    // setThumbarButtons(params)\n    global.lx.event_app.player_status(params)\n  })\n\n  mainOn(WIN_MAIN_RENDERER_EVENT_NAME.inited, () => {\n    global.lx.event_app.main_window_inited()\n  })\n\n  mainHandle<{ themes: LX.Theme[], userThemes: LX.Theme[] }>(WIN_MAIN_RENDERER_EVENT_NAME.get_themes, async() => {\n    return getAllThemes()\n  })\n  mainHandle<LX.Theme>(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, async({ params: theme }) => {\n    saveTheme(theme)\n  })\n  mainHandle<string>(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, async({ params: id }) => {\n    removeTheme(id)\n  })\n}\n\nexport const sendFocus = () => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.focus)\n}\n\nexport const sendTaskbarButtonClick = (action: LX.Player.StatusButtonActions, data?: unknown) => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, { action, data })\n}\nexport const sendConfigChange = (setting: Partial<LX.AppSetting>) => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, setting)\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/data.ts",
    "content": "import { STORE_NAMES } from '@common/constants'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainOn, mainHandle } from '@common/mainIpc'\nimport getStore from '@main/utils/store'\n\nexport default () => {\n  mainHandle<string, any>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, ({ params: path }) => {\n    return getStore(STORE_NAMES.DATA).get(path) as any\n  })\n\n  mainOn<{\n    path: string\n    data: any\n  }>(WIN_MAIN_RENDERER_EVENT_NAME.save_data, ({ params: { path, data } }) => {\n    getStore(STORE_NAMES.DATA).set(path, data)\n  })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/download.ts",
    "content": "import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainHandle } from '@common/mainIpc'\n\n\nexport default () => {\n  mainHandle<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_get, async() => {\n    return global.lx.worker.dbService.getDownloadList()\n  })\n  mainHandle<LX.Download.saveDownloadMusicInfo>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_add, async({ params: { list, addMusicLocationType } }) => {\n    await global.lx.worker.dbService.downloadInfoSave(list, addMusicLocationType)\n  })\n  mainHandle<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_update, async({ params: list }) => {\n    await global.lx.worker.dbService.downloadInfoUpdate(list)\n  })\n  mainHandle<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, async({ params: ids }) => {\n    await global.lx.worker.dbService.downloadInfoRemove(ids)\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear, async() => {\n    await global.lx.worker.dbService.downloadInfoClear()\n  })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/hotKey.ts",
    "content": "import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainHandle } from '@common/mainIpc'\nimport { sendEvent } from '../main'\n// import getStore from '@common/store'\n\n\n// const { registerHotkey, unRegisterHotkey } = require('../modules/hotKey/utils')\n\n// mainHandle(ipcMainWindowNames.set_hot_key_config, async(event, { action, data }) => {\n//   switch (action) {\n//     case 'config':\n//       global.lx_event.hotKey.saveConfig(data.data, MAIN_WINDOW_EVENT_NAME.source)\n//       return\n//     case 'register':\n//       return registerHotkey(data)\n//     case 'unregister':\n//       return unRegisterHotkey(data)\n//   }\n// })\n\nexport default () => {\n  mainHandle<LX.HotKeyConfigAll>(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key, async() => {\n    // const electronStore_hotKey = getStore('hotKey')\n    return {\n      local: global.lx.hotKey?.config.local,\n      global: global.lx.hotKey?.config.global,\n    }\n  })\n\n  // global.lx.event_app.on(APP_EVENT_NAMES.hotKeyConfig, (config, source) => {\n  //   if (!global.modules.mainWindow || source === MAIN_WINDOW_EVENT_NAME.name) return\n  //   mainSend(global.modules.mainWindow, WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, { config, source })\n  // })\n}\n\nexport const handleKeyDown = (type: string, key: string) => {\n  sendEvent<LX.HotKeyEvent>(WIN_MAIN_RENDERER_EVENT_NAME.key_down, { type, key })\n}\n\nexport const hotKeyConfigUpdate = (config: LX.HotKeyConfigAll) => {\n  sendEvent<LX.HotKeyConfigAll>(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, config)\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/index.ts",
    "content": "import { registerRendererEvents as common } from '@main/modules/commonRenderers/common'\nimport { registerRendererEvents as list } from '@main/modules/commonRenderers/list'\nimport { registerRendererEvents as dislike } from '@main/modules/commonRenderers/dislike'\nimport app, { sendConfigChange } from './app'\nimport hotKey from './hotKey'\nimport kw_decodeLyric from './kw_decodeLyric'\nimport tx_decodeLyric from './tx_decodeLyric'\nimport userApi from './userApi'\nimport sync from './sync'\nimport data from './data'\nimport music from './music'\nimport download from './download'\nimport soundEffect from './soundEffect'\nimport openAPI from './openAPI'\nimport { sendEvent } from '../main'\n\nexport * from './app'\nexport * from './hotKey'\nexport * from './userApi'\nexport * from './sync'\nexport * from './process'\n\nlet isInitialized = false\nexport default () => {\n  if (isInitialized) return\n  isInitialized = true\n\n  common(sendEvent)\n  list(sendEvent)\n  dislike(sendEvent)\n  app()\n  hotKey()\n  kw_decodeLyric()\n  tx_decodeLyric()\n  userApi()\n  sync()\n  data()\n  music()\n  download()\n  soundEffect()\n  openAPI()\n\n  global.lx.event_app.on('updated_config', (keys, setting) => {\n    sendConfigChange(setting)\n  })\n}\n\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/kw_decodeLyric.ts",
    "content": "import { inflate } from 'zlib'\nimport iconv from 'iconv-lite'\nimport { mainHandle } from '@common/mainIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\n\nconst handleInflate = async(data: Buffer) => {\n  return new Promise((resolve: (result: Buffer) => void, reject) => {\n    inflate(data, (err, result) => {\n      if (err) {\n        reject(err)\n        return\n      }\n      resolve(result)\n    })\n  })\n}\n\nconst buf_key = Buffer.from('yeelion')\nconst buf_key_len = buf_key.length\n\nconst decodeLyric = async(buf: Buffer, isGetLyricx: boolean) => {\n  // const info = buf.slice(0, index).toString()\n  // if (!info.startsWith('tp=content')) return null\n  // const isLyric = info.includes('\\r\\nlrcx=0\\r\\n')\n  if (buf.toString('utf8', 0, 10) != 'tp=content') return ''\n  // const index = buf.indexOf('\\r\\n\\r\\n') + 4\n  const lrcData = await handleInflate(buf.subarray(buf.indexOf('\\r\\n\\r\\n') + 4))\n\n  if (!isGetLyricx) return iconv.decode(lrcData, 'gb18030')\n\n  const buf_str = Buffer.from(lrcData.toString(), 'base64')\n  const buf_str_len = buf_str.length\n  const output = new Uint8Array(buf_str_len)\n  let i = 0\n  while (i < buf_str_len) {\n    let j = 0\n    while (j < buf_key_len && i < buf_str_len) {\n      output[i] = buf_str[i] ^ buf_key[j]\n      i++\n      j++\n    }\n  }\n\n  return iconv.decode(Buffer.from(output), 'gb18030')\n}\n\n\nexport default () => {\n  mainHandle<{ lrcBase64: string, isGetLyricx: boolean }, string>(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, async({ params: { lrcBase64, isGetLyricx } }) => {\n    const lrc = await decodeLyric(Buffer.from(lrcBase64, 'base64'), isGetLyricx)\n    return Buffer.from(lrc).toString('base64')\n  })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/music.ts",
    "content": "import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainHandle } from '@common/mainIpc'\n\n\nexport default () => {\n  // =========================歌词=========================\n  mainHandle<string, LX.Player.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_palyer_lyric, async({ params: id }) => {\n    // return (getStore(LRC_EDITED, true, false).get(id) as LX.Music.LyricInfo | undefined) ??\n    // getStore(LRC_RAW, true, false).get(id, {}) as LX.Music.LyricInfo\n    return global.lx.worker.dbService.getPlayerLyric(id)\n  })\n\n  // 原始歌词\n  mainHandle<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw, async({ params: id }) => {\n    return global.lx.worker.dbService.getRawLyric(id)\n  })\n  mainHandle<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, async({ params: { id, lyrics } }) => {\n    await global.lx.worker.dbService.rawLyricAdd(id, lyrics)\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw, async() => {\n    await global.lx.worker.dbService.rawLyricClear()\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw_count, async() => {\n    return global.lx.worker.dbService.rawLyricCount()\n  })\n\n  // 已编辑的歌词\n  mainHandle<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited, async({ params: id }) => {\n    return global.lx.worker.dbService.getEditedLyric(id)\n  })\n  mainHandle<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, async({ params: { id, lyrics } }) => {\n    await global.lx.worker.dbService.editedLyricUpdateAddAndUpdate(id, lyrics)\n  })\n  mainHandle<string>(WIN_MAIN_RENDERER_EVENT_NAME.remove_lyric_edited, async({ params: id }) => {\n    await global.lx.worker.dbService.editedLyricRemove([id])\n  })\n  mainHandle<string>(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_edited, async() => {\n    await global.lx.worker.dbService.editedLyricClear()\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited_count, async() => {\n    return global.lx.worker.dbService.editedLyricCount()\n  })\n\n\n  // =========================歌曲URL=========================\n  mainHandle<string, string>(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url, async({ params: id }) => {\n    return (await global.lx.worker.dbService.getMusicUrl(id)) ?? ''\n  })\n  mainHandle<LX.Music.MusicUrlInfo>(WIN_MAIN_RENDERER_EVENT_NAME.save_music_url, async({ params: { id, url } }) => {\n    await global.lx.worker.dbService.musicUrlSave([{ id, url }])\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_music_url, async() => {\n    await global.lx.worker.dbService.musicUrlClear()\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url_count, async() => {\n    return global.lx.worker.dbService.musicUrlCount()\n  })\n\n  // =========================换源歌曲=========================\n  mainHandle<string, LX.Music.MusicInfoOnline[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source, async({ params: id }) => {\n    return global.lx.worker.dbService.getMusicInfoOtherSource(id)\n  })\n  mainHandle<LX.Music.MusicInfoOtherSourceSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_other_source, async({ params: { id, list } }) => {\n    await global.lx.worker.dbService.musicInfoOtherSourceAdd(id, list)\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_other_source, async() => {\n    await global.lx.worker.dbService.musicInfoOtherSourceClear()\n  })\n  mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source_count, async() => {\n    return global.lx.worker.dbService.musicInfoOtherSourceCount()\n  })\n\n  // mainHandle<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.remove_dislike_music_infos, async({ params: ids }) => {\n  //   await global.lx.worker.dbService.dislikeInfoRemove(ids)\n  // })\n  // mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_dislike_music_infos, async() => {\n  //   await global.lx.worker.dbService.dislikeInfoClear()\n  // })\n\n\n  // =========================我的列表=========================\n  // mainHandle<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.get_playlist, async({ params: isIgnoredError = false }) => {\n  //   const electronStore_list = getStore('playList', isIgnoredError, false)\n\n  //   return {\n  //     defaultList: electronStore_list.get('defaultList'),\n  //     loveList: electronStore_list.get('loveList'),\n  //     tempList: electronStore_list.get('tempList'),\n  //     userList: electronStore_list.get('userList'),\n  //     downloadList: getStore('downloadList').get('list'),\n  //   }\n  // })\n\n  // const handleSaveList = ({ defaultList, loveList, userList, tempList }: Partial<LX.List.MyAllList>) => {\n  //   let data: Partial<LX.List.MyAllList> = {}\n  //   if (defaultList != null) data.defaultList = defaultList\n  //   if (loveList != null) data.loveList = loveList\n  //   if (userList != null) data.userList = userList\n  //   if (tempList != null) data.tempList = tempList\n  //   getStore('playList').set(data)\n  // }\n  // mainOn<LX.List.ListSaveInfo>(WIN_MAIN_RENDERER_EVENT_NAME.save_playlist, ({ params }) => {\n  //   switch (params.type) {\n  //     case 'myList':\n  //       handleSaveList(params.data)\n  //       global.lx.event_app.save_my_list(params.data)\n  //       break\n  //     case 'downloadList':\n  //       getStore('downloadList').set('list', params.data)\n  //       break\n  //   }\n  // })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/openAPI.ts",
    "content": "import { mainHandle } from '@common/mainIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport {\n  startServer,\n  stopServer,\n  getStatus,\n} from '@main/modules/openApi'\n\n\nexport default () => {\n  mainHandle<LX.OpenAPI.Actions, any>(WIN_MAIN_RENDERER_EVENT_NAME.open_api_action, async({ params: data }) => {\n    switch (data.action) {\n      case 'enable':\n        return data.data.enable ? await startServer(parseInt(data.data.port), data.data.bindLan) : await stopServer()\n      case 'status': return getStatus()\n    }\n  })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/process.ts",
    "content": "import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { getWebContents } from '../main'\n\n\n// export default () => {\n\n\n// }\n\n/**\n * 发送桌面歌词进程创建事件\n * @param port 端口\n */\nexport const sendNewDesktopLyricClient = (port: Electron.MessagePortMain) => {\n  getWebContents().postMessage(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, null, [port])\n}\n\n\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/soundEffect.ts",
    "content": "import { STORE_NAMES } from '@common/constants'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainOn, mainHandle } from '@common/mainIpc'\nimport getStore from '@main/utils/store'\n\nexport default () => {\n  mainHandle<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset, async() => {\n    return getStore(STORE_NAMES.SOUND_EFFECT).get('eqPreset') as LX.SoundEffect.EQPreset[] | null ?? []\n  })\n  mainOn<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, ({ params }) => {\n    getStore(STORE_NAMES.SOUND_EFFECT).set('eqPreset', params)\n  })\n\n  mainHandle<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset, async() => {\n    return getStore(STORE_NAMES.SOUND_EFFECT).get('convolutionPreset') as LX.SoundEffect.ConvolutionPreset[] | null ?? []\n  })\n  mainOn<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => {\n    getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params)\n  })\n\n  // mainHandle<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset, async() => {\n  //   return getStore(STORE_NAMES.SOUND_EFFECT).get('pitchShifterPreset') as LX.SoundEffect.PitchShifterPreset[] | null ?? []\n  // })\n  // mainOn<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, ({ params }) => {\n  //   getStore(STORE_NAMES.SOUND_EFFECT).set('pitchShifterPreset', params)\n  // })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/sync.ts",
    "content": "import { mainHandle } from '@common/mainIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport {\n  startServer,\n  stopServer,\n  getServerStatus,\n  generateCode,\n  connectServer,\n  disconnectServer,\n  getClientStatus,\n  getServerDevices,\n  removeServerDevice,\n} from '@main/modules/sync'\nimport { sendEvent } from '../main'\n\n\nlet selectModeListenr: ((mode: LX.Sync.ModeTypes[keyof LX.Sync.ModeTypes] | null) => void) | null = null\n\nexport default () => {\n  mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {\n    switch (data.action) {\n      case 'enable_server':\n        data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()\n        return\n      case 'enable_client':\n        data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer()\n        return\n      case 'get_server_status': return getServerStatus()\n      case 'get_client_status': return getClientStatus()\n      case 'generate_code': return generateCode()\n      case 'select_mode':\n        if (selectModeListenr) {\n          selectModeListenr(data.data.mode)\n          selectModeListenr = null\n        }\n        break\n      default:\n        break\n    }\n  })\n  mainHandle<never, LX.Sync.ServerDevices>(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices, async() => {\n    return getServerDevices()\n  })\n  mainHandle<string>(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, async({ params: clientId }) => {\n    await removeServerDevice(clientId)\n  })\n}\n\n\nexport const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)\n}\n\nexport const sendClientStatus = (status: LX.Sync.ClientStatus) => {\n  sendSyncAction({\n    action: 'client_status',\n    data: status,\n  })\n}\nexport const sendServerStatus = (status: LX.Sync.ServerStatus) => {\n  sendSyncAction({\n    action: 'server_status',\n    data: status,\n  })\n}\nexport const sendSelectMode = <T extends keyof LX.Sync.ModeTypes>(deviceName: string, type: T, listener: (mode: LX.Sync.ModeTypes[T] | null) => void) => {\n  selectModeListenr = listener as typeof selectModeListenr\n  sendSyncAction({ action: 'select_mode', data: { deviceName, type } })\n}\nexport const removeSelectModeListener = () => {\n  if (selectModeListenr) selectModeListenr(null)\n  selectModeListenr = null\n}\nexport const sendCloseSelectMode = () => {\n  sendSyncAction({ action: 'close_select_mode' })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts",
    "content": "import { createInflate, constants as zlibConstants } from 'node:zlib'\n// import path from 'path'\nimport { mainHandle } from '@common/mainIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\n\n// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/quotes\n// const require = module[`require`].bind(module)\n\nlet qrc_decode: (buf: Buffer, len: number) => Buffer\n\nconst inflate = async(lrcBuf: Buffer) => new Promise<string>((resolve, reject) => {\n  const buffer_builder: Buffer[] = []\n  const decompress_stream = createInflate()\n    .on('data', (chunk) => {\n      buffer_builder.push(chunk)\n    })\n    .on('close', () => {\n      resolve(Buffer.concat(buffer_builder).toString())\n    })\n    .on('error', (err: any) => {\n      // console.log(err)\n      if (err.errno !== zlibConstants.Z_BUF_ERROR) { // EOF: expected\n        reject(err)\n      }\n    })\n  // decompress_stream.write(lrcBuf)\n  decompress_stream.end(lrcBuf)\n})\n\nconst decode = async(str: string): Promise<string> => {\n  if (!str) return ''\n  const buf = Buffer.from(str, 'hex')\n  return inflate(qrc_decode(buf, buf.length))\n}\n\n\n// 感谢某位不愿透露姓名的大佬提供的C++算法源码，但由于作者不希望公开，所以将会以预构建二进制文件的形式加入代码仓库中\nconst handleDecode = async(lrc: string, tlrc: string, rlrc: string) => {\n  if (!qrc_decode) {\n    // const nativeBindingPath = path.join(__dirname, '../build/Release/qrc_decode.node')\n    // const nativeBindingPath = process.env.NODE_ENV !== 'production' ? path.join(__dirname, '../build/Release/qrc_decode.node')\n    // eslint-disable-next-line @typescript-eslint/no-var-requires\n    const addon = require('qrc_decode.node')\n    // console.log(addon)\n    qrc_decode = addon.qrc_decode\n  }\n\n  const [lyric, tlyric, rlyric] = await Promise.all([decode(lrc), decode(tlrc), decode(rlrc)])\n  return {\n    lyric,\n    tlyric,\n    rlyric,\n  }\n}\n\n\nexport default () => {\n  mainHandle<{ lrc: string, tlrc: string, rlrc: string }, { lyric: string, tlyric: string, rlyric: string }>(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, async({ params: { lrc, tlrc, rlrc } }) => {\n    return handleDecode(lrc, tlrc, rlrc)\n  })\n}\n"
  },
  {
    "path": "src/main/modules/winMain/rendererEvent/userApi.ts",
    "content": "import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { mainHandle } from '@common/mainIpc'\nimport {\n  getApiList,\n  importApi,\n  removeApi,\n  setApi,\n  getStatus,\n  request,\n  cancelRequest,\n  setAllowShowUpdateAlert,\n} from '@main/modules/userApi'\nimport { sendEvent } from '@main/modules/winMain/main'\n\nexport default () => {\n  mainHandle<string, LX.UserApi.ImportUserApi>(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, async({ params: script }) => {\n    return importApi(script)\n  })\n\n  mainHandle<string[], LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, async({ params: apiIds }) => {\n    return removeApi(apiIds)\n  })\n\n  mainHandle<LX.UserApi.UserApiSetApiParams>(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, async({ params: apiId }) => {\n    await setApi(apiId)\n  })\n\n  mainHandle<LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list, async() => {\n    return getApiList()\n  })\n\n  mainHandle<LX.UserApi.UserApiStatus>(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_status, async() => {\n    return getStatus()\n  })\n\n  mainHandle<LX.UserApi.UserApiSetAllowUpdateAlertParams>(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, async({ params: { id, enable } }) => {\n    setAllowShowUpdateAlert(id, enable)\n  })\n\n  mainHandle<LX.UserApi.UserApiRequestParams>(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, async({ params }) => {\n    return request(params)\n  })\n  mainHandle<LX.UserApi.UserApiRequestCancelParams>(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, async({ params: requestKey }) => {\n    cancelRequest(requestKey)\n  })\n}\n\nexport const sendStatusChange = (status: LX.UserApi.UserApiStatus) => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, status)\n}\nexport const sendShowUpdateAlert = (info: LX.UserApi.UserApiUpdateInfo) => {\n  sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, info)\n}\n\n"
  },
  {
    "path": "src/main/modules/winMain/utils.ts",
    "content": "// import fs from 'fs'\nimport path from 'node:path'\nimport { type WindowSize, windowSizeList } from '@common/config'\nimport { nativeImage } from 'electron'\n\nexport const getWindowSizeInfo = (windowSizeId: number | string): WindowSize => {\n  return windowSizeList.find(i => i.id == windowSizeId) ?? windowSizeList[0]\n}\n\nconst getIconPath = (name: string): Electron.NativeImage => {\n  return nativeImage.createFromPath(path.join(global.staticPath, 'images/taskbar', name + '.png'))\n}\n\nexport const createTaskBarButtons = ({\n  empty = false,\n  collect = false,\n  play = false,\n  next = true,\n  prev = true,\n}: LX.TaskBarButtonFlags, onClick: (action: LX.Player.StatusButtonActions) => void): Electron.ThumbarButton[] => {\n  const buttons: Electron.ThumbarButton[] = [\n    collect\n      ? {\n          icon: getIconPath('collected'),\n          click() {\n            onClick('unCollect')\n          },\n          tooltip: '取消收藏',\n          flags: ['nobackground'],\n        }\n      : {\n          icon: getIconPath('collect'),\n          click() {\n            onClick('collect')\n          },\n          tooltip: '收藏',\n          flags: ['nobackground'],\n        },\n    {\n      icon: getIconPath('prev'),\n      click() {\n        onClick('prev')\n      },\n      tooltip: '上一曲',\n      flags: prev ? ['nobackground'] : ['nobackground', 'disabled'],\n    },\n    play\n      ? {\n          icon: getIconPath('pause'),\n          click() {\n            onClick('pause')\n          },\n          tooltip: '暂停',\n          flags: ['nobackground'],\n        }\n      : {\n          icon: getIconPath('play'),\n          click() {\n            onClick('play')\n          },\n          tooltip: '播放',\n          flags: ['nobackground'],\n        },\n    {\n      icon: getIconPath('next'),\n      click() {\n        onClick('next')\n      },\n      tooltip: '下一曲',\n      flags: next ? ['nobackground'] : ['nobackground', 'disabled'],\n    },\n  ]\n  if (empty) {\n    for (const button of buttons) {\n      button.flags = ['nobackground', 'disabled']\n    }\n  }\n  return buttons\n}\n"
  },
  {
    "path": "src/main/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"typeRoots\": [\n      \"./types\"\n    ],\n    \"paths\": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */\n      \"@common/*\": [\"common/*\"],\n      \"@main/*\": [\"main/*\"],\n      \"@static/*\": [\"static/*\"],\n      \"@/*\": [\"./*\"],\n    },\n  },\n}\n"
  },
  {
    "path": "src/main/types/app.d.ts",
    "content": "/* eslint-disable no-var */\n// import { Event as WinMainEvent } from '@main/modules/winMain/event'\n// import { Event as WinLyricEvent } from '@main/modules/winLyric/event'\nimport { type DislikeType, type AppType, type ListType } from '@main/event'\nimport { type DBSeriveTypes } from '@main/worker/utils'\n\ninterface Lx {\n  inited: boolean\n  appSetting: LX.AppSetting\n  hotKey: {\n    enable: boolean\n    config: LX.HotKeyConfigAll\n    state: LX.HotKeyState\n  }\n  /**\n   * 是否跳过托盘退出\n   */\n  isSkipTrayQuit: boolean\n  /**\n   * main window 是否关闭\n   */\n  // mainWindowClosed: boolean\n  event_app: AppType\n  event_list: ListType\n  event_dislike: DislikeType\n  worker: {\n    dbService: DBSeriveTypes\n  }\n  theme: LX.ThemeSetting\n  player_status: LX.Player.Status\n}\n\ndeclare global {\n  // declare module NodeJS {\n  //   export interface Global {\n  //     lx: {\n  //       app_event: {\n  //         winMain: WinMainEvent\n  //         winLyric: WinLyricEvent\n  //       }\n  //     }\n  //   }\n  // }\n\n  // var isDev: boolean\n  var envParams: LX.EnvParams\n  var staticPath: string\n  var lxDataPath: string\n  var lxOldDataPath: string\n  var lx: Lx\n  var appWorder: AppWorder\n}\n\n\n"
  },
  {
    "path": "src/main/types/common.d.ts",
    "content": "import '@common/types/utils'\nimport '@common/types/app_setting'\nimport '@common/types/common'\nimport '@common/types/user_api'\nimport '@common/types/sync'\nimport '@common/types/list'\nimport '@common/types/list_sync'\nimport '@common/types/download_list'\nimport '@common/types/music'\nimport '@common/types/player'\nimport '@common/types/desktop_lyric'\nimport '@common/types/theme'\nimport '@common/types/ipc_main'\nimport '@common/types/sound_effect'\nimport '@common/types/dislike_list'\nimport '@common/types/dislike_list_sync'\nimport '@common/types/open_api'\n"
  },
  {
    "path": "src/main/types/db_service.d.ts",
    "content": "declare namespace LX {\n  namespace DBService {\n\n    interface MusicInfo {\n      id: string\n      listId: string\n      name: string\n      singer: string\n      interval: string | null\n      source: LX.Music.MusicInfo['source']\n      meta: string\n      order: number\n    }\n\n    interface MusicInfoOrder {\n      listId: string\n      musicInfoId: string\n      order: number\n    }\n\n    interface MusicInfoQuery {\n      listId: string\n    }\n\n    interface MusicInfoRemove {\n      listId: string\n      id: string\n    }\n\n    interface ListMusicInfoQuery {\n      listId: string\n      musicInfoId: string\n    }\n\n    interface UserListInfo {\n      id: string\n      name: string\n      source?: LX.OnlineSource\n      sourceListId?: string\n      position: number\n      locationUpdateTime: number | null\n    }\n\n    type Lyricnfo = {\n      id: string\n      type: 'lyric'\n      text: string\n      source: 'raw' | 'edited'\n    } | {\n      id: string\n      type: keyof Omit<LX.Music.LyricInfo, 'lyric'>\n      text: string | null\n      source: 'raw' | 'edited'\n    }\n\n    interface MusicUrlInfo {\n      id: string\n      url: string\n    }\n\n    interface DownloadMusicInfo {\n      id: string\n      isComplate: 0 | 1\n      status: LX.Download.DownloadTaskStatus\n      statusText: string\n      progress_downloaded: number\n      progress_total: number\n      url: string | null\n      quality: LX.Quality\n      ext: LX.Download.FileExt\n      fileName: string\n      filePath: string\n      musicInfo: string\n      position: number\n    }\n\n    interface DislikeInfo {\n      // type: 'music'\n      content: string\n      // meta: string | null\n    }\n\n    interface MusicInfoOtherSource extends Omit<MusicInfoOnline, 'listId'> {\n      source_id: string\n      order: number\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/main/types/global.d.ts",
    "content": "\n\n// // declare module NodeJS {\n// //   interface Global {\n// //     isDev: boolean\n// //     envParams: LX.EnvParams\n// //     staticPath: string\n// //     lx: Lx\n// //   }\n// // }\n\ndeclare const webpackStaticPath: string\ndeclare const webpackUserApiPath: string\n\n"
  },
  {
    "path": "src/main/types/sync.d.ts",
    "content": "import type WS from 'ws'\n\ntype DefaultEventsMap = Record<string, (...args: any[]) => void>\n\n\ndeclare global {\n  namespace LX {\n    namespace Sync {\n      namespace Client {\n        interface Socket extends WS.WebSocket {\n          isReady: boolean\n          data: {\n            keyInfo: ClientKeyInfo\n            urlInfo: UrlInfo\n          }\n          moduleReadys: {\n            list: boolean\n            dislike: boolean\n          }\n\n          onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void\n          remote: LX.Sync.ServerSyncActions\n          remoteQueueList: LX.Sync.ServerSyncListActions\n          remoteQueueDislike: LX.Sync.ServerSyncDislikeActions\n        }\n\n        interface UrlInfo {\n          wsProtocol: string\n          httpProtocol: string\n          hostPath: string\n          href: string\n        }\n      }\n      namespace Server {\n        interface Socket extends WS.WebSocket {\n          isAlive?: boolean\n          isReady: boolean\n          userInfo: { name: 'default' }\n          keyInfo: ServerKeyInfo\n          feature: LX.Sync.EnabledFeatures\n          moduleReadys: {\n            list: boolean\n            dislike: boolean\n          }\n\n          onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void\n          broadcast: (handler: (client: Socket) => void) => void\n\n          remote: LX.Sync.ClientSyncActions\n          remoteQueueList: LX.Sync.ClientSyncListActions\n          remoteQueueDislike: LX.Sync.ClientSyncDislikeActions\n        }\n        type SocketServer = WS.Server<Socket>\n      }\n    }\n  }\n\n  // interface SyncListActionData_none {\n  //   action: 'finished'\n  // }\n  // interface SyncListActionData_getData {\n  //   action: 'getData'\n  //   data: 'all'\n  // }\n\n  // type SyncListActionData = SyncListActionData_none | SyncListActionData_getData\n}\n\n"
  },
  {
    "path": "src/main/types/sync_common.d.ts",
    "content": "type WarpSyncHandlerActions<Socket, Actions> = {\n  [K in keyof Actions]: (...args: [Socket, ...Parameters<Actions[K]>]) => ReturnType<Actions[K]>\n}\n\ndeclare namespace LX {\n  namespace Sync {\n    type ServerSyncActions = WarpPromiseRecord<{\n      onFeatureChanged: (feature: EnabledFeatures) => void\n    }>\n    type ServerSyncHandlerActions<Socket> = WarpSyncHandlerActions<Socket, ServerSyncActions>\n\n    type ServerSyncListActions = WarpPromiseRecord<{\n      onListSyncAction: (action: LX.Sync.List.ActionList) => void\n    }>\n    type ServerSyncHandlerListActions<Socket> = WarpSyncHandlerActions<Socket, ServerSyncListActions>\n\n    type ServerSyncDislikeActions = WarpPromiseRecord<{\n      onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void\n    }>\n    type ServerSyncHandlerDislikeActions<Socket> = WarpSyncHandlerActions<Socket, ServerSyncDislikeActions>\n\n    type ClientSyncActions = WarpPromiseRecord<{\n      getEnabledFeatures: (serverType: ServerType, supportedFeatures: SupportedFeatures) => EnabledFeatures\n      finished: () => void\n    }>\n    type ClientSyncHandlerActions<Socket> = WarpSyncHandlerActions<Socket, ClientSyncActions>\n\n    type ClientSyncListActions = WarpPromiseRecord<{\n      onListSyncAction: (action: LX.Sync.List.ActionList) => void\n      list_sync_get_md5: () => string\n      list_sync_get_sync_mode: () => LX.Sync.List.SyncMode\n      list_sync_get_list_data: () => LX.Sync.List.ListData\n      list_sync_set_list_data: (data: LX.Sync.List.ListData) => void\n      list_sync_finished: () => void\n    }>\n    type ClientSyncHandlerListActions<Socket> = WarpSyncHandlerActions<Socket, ClientSyncListActions>\n\n    type ClientSyncDislikeActions = WarpPromiseRecord<{\n      onDislikeSyncAction: (action: LX.Sync.Dislike.ActionList) => void\n      dislike_sync_get_md5: () => string\n      dislike_sync_get_sync_mode: () => LX.Sync.Dislike.SyncMode\n      dislike_sync_get_list_data: () => LX.Dislike.DislikeRules\n      dislike_sync_set_list_data: (data: LX.Dislike.DislikeRules) => void\n      dislike_sync_finished: () => void\n    }>\n    type ClientSyncHandlerDislikeActions<Socket> = WarpSyncHandlerActions<Socket, ClientSyncDislikeActions>\n  }\n}\n\n\n"
  },
  {
    "path": "src/main/types/worker.d.ts",
    "content": "import { type workerDBSeriveTypes } from '@main/worker/dbService'\n\ndeclare global {\n  // interface WorkerDBSeriveTypes {\n  //   list: typeof list\n  // }\n  namespace LX {\n    type WorkerDBSeriveListTypes = workerDBSeriveTypes\n  }\n}\n"
  },
  {
    "path": "src/main/utils/fontManage.ts",
    "content": "// const { getAvailableFontFamilies } = require('electron-font-manager')\n\n\n// exports.getAvailableFontFamilies = getAvailableFontFamilies\n\nimport { getFonts } from 'font-list'\n// import { getAvailableFontFamilies } from 'electron-font-manager'\n\n\n// const getFonts = async() => {\n//   switch (process.platform) {\n//     case 'win32':\n//     case 'darwin':\n//       return getAvailableFontFamilies()\n//     default: return getFontsByCommand()\n//   }\n// }\n\nexport {\n  getFonts,\n}\n"
  },
  {
    "path": "src/main/utils/index.ts",
    "content": "import { encodePath, isUrl, throttle } from '@common/utils'\nimport migrateSetting from '@common/utils/migrateSetting'\nimport getStore from '@main/utils/store'\nimport { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants'\nimport defaultSetting from '@common/defaultSetting'\nimport defaultHotKey from '@common/defaultHotKey'\nimport { migrateDataJson, migrateHotKey, migrateUserApi, parseDataFile } from './migrate'\nimport { nativeTheme, powerSaveBlocker } from 'electron'\nimport { joinPath } from '@common/utils/nodejs'\nimport themes from '@common/theme/index.json'\n\nexport const parseEnvParams = (argv = process.argv): { cmdParams: LX.CmdParams, deeplink: string | null } => {\n  const cmdParams: LX.CmdParams = {}\n  let deeplink = null\n  const rx = /^-\\w+/\n  for (let param of argv) {\n    if (URL_SCHEME_RXP.test(param)) {\n      deeplink = param\n    }\n\n    if (!rx.test(param)) continue\n    param = param.substring(1)\n    let index = param.indexOf('=')\n    if (index < 0) {\n      cmdParams[param] = true\n    } else {\n      cmdParams[param.substring(0, index)] = param.substring(index + 1)\n    }\n  }\n  return {\n    cmdParams,\n    deeplink,\n  }\n}\n\nconst primitiveType = ['string', 'boolean', 'number']\nconst checkPrimitiveType = (val: any): boolean => val === null || primitiveType.includes(typeof val)\n// const handleMergeSetting = (defaultSetting: LX.AppSetting, currentSetting: Partial<LX.AppSetting>) => {\n//   const updatedSettingKeys: Array<keyof LX.AppSetting> = []\n//   for (const key of Object.keys(defaultSetting) as Array<keyof LX.AppSetting>) {\n//     const currentValue: any = currentSetting[key]\n//     const isPrimitive = checkPrimitiveType(currentValue)\n//     // if (checkPrimitiveType(value)) {\n//     if (!isPrimitive) continue\n//     updatedSettingKeys.push(key)\n//     // @ts-expect-error\n//     defaultSetting[key] = currentValue\n//     // } else {\n//     //   if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)\n//     // }\n//   }\n//   return {\n//     setting: defaultSetting,\n//     updatedSettingKeys,\n//   }\n// }\n\nexport const mergeSetting = (originSetting: LX.AppSetting, targetSetting?: Partial<LX.AppSetting> | null): {\n  setting: LX.AppSetting\n  updatedSettingKeys: Array<keyof LX.AppSetting>\n  updatedSetting: Partial<LX.AppSetting>\n} => {\n  let originSettingCopy: LX.AppSetting = { ...originSetting }\n  // const defaultVersion = targetSettingCopy.version\n  const updatedSettingKeys: Array<keyof LX.AppSetting> = []\n  const updatedSetting: Partial<LX.AppSetting> = {}\n\n  if (targetSetting) {\n    const originSettingKeys = Object.keys(originSettingCopy)\n    const targetSettingKeys = Object.keys(targetSetting)\n\n    if (originSettingKeys.length > targetSettingKeys.length) {\n      for (const key of targetSettingKeys as Array<keyof LX.AppSetting>) {\n        const targetValue: any = targetSetting[key]\n        const isPrimitive = checkPrimitiveType(targetValue)\n        // if (checkPrimitiveType(value)) {\n        if (!isPrimitive || targetValue == originSettingCopy[key] || originSettingCopy[key] === undefined) continue\n        updatedSettingKeys.push(key)\n        updatedSetting[key] = targetValue\n        // @ts-expect-error\n        originSettingCopy[key] = targetValue\n        // } else {\n        //   if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)\n        // }\n      }\n    } else {\n      for (const key of originSettingKeys as Array<keyof LX.AppSetting>) {\n        const targetValue: any = targetSetting[key]\n        const isPrimitive = checkPrimitiveType(targetValue)\n        // if (checkPrimitiveType(value)) {\n        if (!isPrimitive || targetValue == originSettingCopy[key]) continue\n        updatedSettingKeys.push(key)\n        updatedSetting[key] = targetValue\n        // @ts-expect-error\n        originSettingCopy[key] = targetValue\n        // } else {\n        //   if (!isPrimitive && currentValue != undefined) handleMergeSetting(value, currentValue)\n        // }\n      }\n    }\n  }\n\n  return {\n    setting: originSettingCopy,\n    updatedSettingKeys,\n    updatedSetting,\n  }\n}\n\nconst applyInitSetting = (setting: LX.AppSetting) => {\n  if (global.envParams.cmdParams.hidden && !setting['tray.enable']) {\n    setting['tray.enable'] = true\n  }\n}\n\nexport const updateSetting = (setting?: Partial<LX.AppSetting>, isInit: boolean = false) => {\n  const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)\n\n  let originSetting: LX.AppSetting\n  if (isInit) {\n    setting &&= migrateSetting(setting)\n    applyInitSetting(setting as LX.AppSetting)\n    originSetting = { ...defaultSetting }\n  } else originSetting = global.lx.appSetting\n\n  const result = mergeSetting(originSetting, setting)\n\n  result.setting.version = defaultSetting.version\n\n  electronStore_config.override({ version: result.setting.version, setting: result.setting })\n  return result\n}\n\n/**\n * 初始化设置\n */\nexport const initSetting = async() => {\n  const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)\n\n  let setting = electronStore_config.get('setting') as LX.AppSetting | undefined\n\n  // migrate setting\n  if (!setting) {\n    const config = await parseDataFile<{ setting?: any }>('config.json')\n    if (config?.setting) setting = config.setting as LX.AppSetting\n    await migrateUserApi()\n    await migrateDataJson()\n  }\n\n  // console.log(setting)\n  return updateSetting(setting, true)\n}\n\n/**\n * 初始化快捷键设置\n */\nexport const initHotKey = async() => {\n  const electronStore_hotKey = getStore(STORE_NAMES.HOTKEY)\n\n  let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null\n  let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null\n\n  if (globalConfig) {\n    // 移除v2.2.0及之前设置的全局媒体快捷键注册\n    if (globalConfig.keys.MediaPlayPause) {\n      delete globalConfig.keys.MediaPlayPause\n      delete globalConfig.keys.MediaNextTrack\n      delete globalConfig.keys.MediaPreviousTrack\n      electronStore_hotKey.set('global', globalConfig)\n    }\n  } else {\n    // migrate hotKey\n    const config = await migrateHotKey()\n    if (config) {\n      localConfig = config.local\n      globalConfig = config.global\n    } else {\n      localConfig = JSON.parse(JSON.stringify(defaultHotKey.local))\n      globalConfig = JSON.parse(JSON.stringify(defaultHotKey.global))\n    }\n\n    electronStore_hotKey.set('local', localConfig)\n    electronStore_hotKey.set('global', globalConfig)\n  }\n\n  return {\n    local: localConfig!,\n    global: globalConfig!,\n  }\n}\n\ntype HotKeyType = 'local' | 'global'\n\nconst saveHotKeyConfig = throttle<[LX.HotKeyConfigAll]>((config: LX.HotKeyConfigAll) => {\n  for (const key of Object.keys(config) as HotKeyType[]) {\n    global.lx.hotKey.config[key] = config[key]\n    getStore(STORE_NAMES.HOTKEY).set(key, config[key])\n  }\n})\nexport const saveAppHotKeyConfig = (config: LX.HotKeyConfigAll) => {\n  saveHotKeyConfig(config)\n}\n\nexport const openDevTools = (webContents: Electron.WebContents) => {\n  webContents.openDevTools({\n    mode: 'undocked',\n  })\n}\n\n\nlet userThemes: LX.Theme[]\nexport const getAllThemes = () => {\n  userThemes ??= getStore(STORE_NAMES.THEME).get('themes') as (LX.Theme[] | null) ?? []\n  return {\n    themes,\n    userThemes,\n    dataPath: joinPath(global.lxDataPath, 'theme_images'),\n  }\n}\n\nexport const saveTheme = (theme: LX.Theme) => {\n  const targetTheme = userThemes.find(t => t.id === theme.id)\n  if (targetTheme) Object.assign(targetTheme, theme)\n  else userThemes.push(theme)\n  getStore(STORE_NAMES.THEME).set('themes', userThemes)\n}\n\nexport const removeTheme = (id: string) => {\n  const index = userThemes.findIndex(t => t.id === id)\n  if (index < 0) return\n  userThemes.splice(index, 1)\n  getStore(STORE_NAMES.THEME).set('themes', userThemes)\n}\n\nconst copyTheme = (theme: LX.Theme): LX.Theme => {\n  return {\n    ...theme,\n    config: {\n      ...theme.config,\n      extInfo: { ...theme.config.extInfo },\n      themeColors: { ...theme.config.themeColors },\n    },\n  }\n}\nexport const getTheme = () => {\n  // fs.promises.readdir()\n  const shouldUseDarkColors = nativeTheme.shouldUseDarkColors\n  let themeId = global.lx.appSetting['theme.id'] == 'auto'\n    ? shouldUseDarkColors\n      ? global.lx.appSetting['theme.darkId']\n      : global.lx.appSetting['theme.lightId']\n    : global.lx.appSetting['theme.id']\n  // themeId = 'naruto'\n  // themeId = 'pink'\n  // themeId = 'black'\n  let theme = themes.find(theme => theme.id == themeId)\n  if (!theme) {\n    userThemes = getStore(STORE_NAMES.THEME).get('themes') as LX.Theme[] | null ?? []\n    theme = userThemes.find(theme => theme.id == themeId)\n    if (theme) {\n      if (theme.config.extInfo['--background-image'] != 'none') {\n        theme = copyTheme(theme)\n        theme.config.extInfo['--background-image'] =\n          isUrl(theme.config.extInfo['--background-image'])\n            ? `url(${theme.config.extInfo['--background-image']})`\n            : `url(file:///${encodePath(joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']))})`\n      }\n    } else {\n      themeId = global.lx.appSetting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green'\n      theme = themes.find(theme => theme.id == themeId) as LX.Theme\n    }\n  }\n\n  const colors: Record<string, string> = {\n    ...theme.config.themeColors,\n    ...theme.config.extInfo,\n  }\n\n  return {\n    shouldUseDarkColors,\n    theme: {\n      id: global.lx.appSetting['theme.id'],\n      name: theme.name,\n      isDark: theme.isDark,\n      isDarkFont: theme.isDarkFont,\n      colors,\n    },\n  }\n}\n\nlet powerSaveBlockerId: number | null = null\nexport const setPowerSaveBlocker = (enabled: boolean) => {\n  let isEnabled = powerSaveBlockerId != null && powerSaveBlocker.isStarted(powerSaveBlockerId)\n  if (enabled) {\n    if (isEnabled) return\n    powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension')\n  } else {\n    if (!isEnabled) return\n    powerSaveBlocker.stop(powerSaveBlockerId!)\n    powerSaveBlockerId = null\n  }\n}\n\n\nlet envProxy: null | { host: string, port: number } = null\nexport const getProxy = () => {\n  if (global.lx.appSetting['network.proxy.enable'] && global.lx.appSetting['network.proxy.host']) {\n    return {\n      host: global.lx.appSetting['network.proxy.host'],\n      port: parseInt(global.lx.appSetting['network.proxy.port'] || '80'),\n    }\n  }\n  if (envProxy) {\n    return {\n      host: envProxy.host,\n      port: envProxy.port,\n    }\n  } else {\n    const envProxyStr = envParams.cmdParams['proxy-server']\n    if (envProxyStr && typeof envProxyStr == 'string') {\n      const [host, port = ''] = envProxyStr.split(':')\n      return envProxy = {\n        host,\n        port: parseInt(port || '80'),\n      }\n    }\n  }\n\n  return null\n}\n"
  },
  {
    "path": "src/main/utils/logInit.ts",
    "content": "import log from 'electron-log/node'\n\nlog.transports.file.level = 'info'\n// log.initialize()\n"
  },
  {
    "path": "src/main/utils/migrate.ts",
    "content": "import fs from 'node:fs'\nimport { checkPath, joinPath } from '@common/utils/nodejs'\nimport { log } from '@common/utils'\nimport { filterMusicList, toNewMusicInfo } from '@common/utils/tools'\nimport { APP_EVENT_NAMES, STORE_NAMES } from '@common/constants'\n\n/**\n * 读取配置文件\n * @returns\n */\nexport const parseDataFile = async<T>(name: string): Promise<T | null> => {\n  const path = joinPath(global.lxOldDataPath, name)\n  if (await checkPath(path)) {\n    try {\n      return JSON.parse((await fs.promises.readFile(path)).toString())\n    } catch (err) {\n      log.error(err)\n    }\n  }\n  return null\n}\n\ninterface OldUserListInfo {\n  name: string\n  id: string\n  source?: LX.OnlineSource\n  sourceListId?: string\n  locationUpdateTime?: number\n  list: any[]\n}\n\n/**\n * 迁移 v2.0.0 之前的 list data\n * @returns\n */\nexport const migrateDBData = async() => {\n  let playList = await parseDataFile<{ defaultList?: { list: any[] }, loveList?: { list: any[] }, tempList?: { list: any[] }, userList?: OldUserListInfo[] }>('playList.json')\n  let listDataAll: LX.List.ListDataFull = {\n    defaultList: [],\n    loveList: [],\n    userList: [],\n    tempList: [],\n  }\n  let isRequiredSave = false\n  if (playList) {\n    if (playList.defaultList) listDataAll.defaultList = filterMusicList(playList.defaultList.list.map(m => toNewMusicInfo(m)))\n    if (playList.loveList) listDataAll.loveList = filterMusicList(playList.loveList.list.map(m => toNewMusicInfo(m)))\n    if (playList.tempList) listDataAll.tempList = filterMusicList(playList.tempList.list.map(m => toNewMusicInfo(m)))\n    if (playList.userList) {\n      listDataAll.userList = playList.userList.map(l => {\n        return {\n          ...l,\n          locationUpdateTime: l.locationUpdateTime ?? null,\n          list: filterMusicList(l.list.map(m => toNewMusicInfo(m))),\n        }\n      })\n    }\n    isRequiredSave = true\n  } else {\n    const config = await parseDataFile<{ list?: { defaultList?: any[], loveList?: any[] } }>('config.json')\n    if (config?.list) {\n      const list = config.list\n      if (list.defaultList) listDataAll.defaultList = filterMusicList(list.defaultList.map(m => toNewMusicInfo(m)))\n      if (list.loveList) listDataAll.loveList = filterMusicList(list.loveList.map(m => toNewMusicInfo(m)))\n      isRequiredSave = true\n    }\n  }\n  if (isRequiredSave) await global.lx.worker.dbService.listDataOverwrite(listDataAll)\n\n  const lyricData = await parseDataFile<Record<string, LX.Music.LyricInfo>>('lyrics_edited.json')\n  if (lyricData) {\n    for await (const [id, info] of Object.entries(lyricData)) {\n      await global.lx.worker.dbService.editedLyricAdd(id, info)\n    }\n  }\n}\n\n// 迁移文件\nconst migrateFile = async(name: string, targetName: string) => {\n  let path = joinPath(global.lxDataPath, targetName)\n  let oldPath = joinPath(global.lxOldDataPath, name)\n  if (!await checkPath(path) && await checkPath(oldPath)) {\n    await fs.promises.copyFile(oldPath, path).catch(err => {\n      log.error(err)\n    }).catch(err => {\n      log.error(err)\n    })\n  }\n}\n\n/**\n * 迁移 v2.0.0 之前的 data.json\n * @returns\n */\nexport const migrateDataJson = async() => {\n  const path = joinPath(global.lxDataPath, 'data.json')\n  if (await checkPath(path)) return\n  const oldDataFile = await parseDataFile<{\n    searchHistoryList?: string[]\n    playInfo?: any\n    listPrevSelectId?: any\n    listPosition?: any\n    listUpdateInfo?: any\n  }>('data.json')\n  if (!oldDataFile) return\n  const newData: any = {}\n  if (oldDataFile.searchHistoryList) newData.searchHistoryList = oldDataFile.searchHistoryList\n  if (oldDataFile.playInfo) newData.playInfo = oldDataFile.playInfo\n  if (oldDataFile.listPrevSelectId) newData.listPrevSelectId = oldDataFile.listPrevSelectId\n  if (oldDataFile.listPosition) newData.listScrollPosition = oldDataFile.listPosition\n  if (oldDataFile.listUpdateInfo) newData.listUpdateInfo = oldDataFile.listUpdateInfo\n\n  await fs.promises.writeFile(path, JSON.stringify(newData)).catch(err => {\n    log.error(err)\n  })\n}\n\n\nconst hotKeyNameMap = {\n  mainWindow: APP_EVENT_NAMES.winMainName,\n  winLyric: APP_EVENT_NAMES.winLyricName,\n} as const\nconst updateHotKeyTypeName = (config: LX.HotKeyConfig) => {\n  for (const keyConfig of Object.values(config.keys)) {\n    if (hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]) keyConfig.type = hotKeyNameMap[keyConfig.type as keyof typeof hotKeyNameMap]\n  }\n}\n/**\n * 迁移 v2.0.0 之前的 hotkey\n * @returns\n */\nexport const migrateHotKey = async() => {\n  const oldConfig = await parseDataFile<LX.HotKeyConfigAll>('hotKey.json')\n  if (oldConfig) {\n    let localConfig: LX.HotKeyConfig\n    let globalConfig: LX.HotKeyConfig\n    updateHotKeyTypeName(oldConfig.local)\n    updateHotKeyTypeName(oldConfig.global)\n\n    localConfig = oldConfig.local\n    globalConfig = oldConfig.global\n\n    // 移除v1.0.1及之前设置的全局声音媒体快捷键接管\n    if (globalConfig.keys.VolumeUp) {\n      delete globalConfig.keys.VolumeUp\n      delete globalConfig.keys.VolumeDown\n      delete globalConfig.keys.VolumeMute\n    }\n    return {\n      local: localConfig,\n      global: globalConfig,\n    }\n  }\n  return null\n}\n\n/**\n * 迁移 v2.0.0 之前的user api\n * @returns\n */\nexport const migrateUserApi = async() => migrateFile('userApi.json', STORE_NAMES.USER_API + '.json')\n"
  },
  {
    "path": "src/main/utils/request.ts",
    "content": "// import progress from 'request-progress'\nimport { request, type Options } from '@common/utils/request'\n// import fs from 'fs'\n\nexport const requestMsg = {\n  fail: '请求异常😮，可以多试几次，若还是不行就换一首吧。。。',\n  unachievable: '哦No😱...接口无法访问了！',\n  timeout: '请求超时',\n  // unachievable: '哦No😱...接口无法访问了！已帮你切换到临时接口，重试下看能不能播放吧~',\n  notConnectNetwork: '无法连接到服务器',\n  cancelRequest: '取消http请求',\n} as const\n\n// var proxyUrl = \"http://\" + user + \":\" + password + \"@\" + host + \":\" + port;\n// var proxiedRequest = request.defaults({'proxy': proxyUrl});\n\n// interface RequestPromise extends Promise<RequestResponse> {\n//   abort: () => void\n// }\n\n/**\n * 请求超时自动重试\n * @param {*} url\n * @param {*} options\n */\nexport const httpFetch = async<T = unknown> (url: string, options: Options) => {\n  return request<T>(url, options).catch(async(err: any) => {\n    // console.log('出错', err)\n    if (err.message === 'socket hang up') {\n      // window.globalObj.apiSource = 'temp'\n      throw new Error(requestMsg.unachievable)\n    }\n    switch (err.code) {\n      case 'ETIMEDOUT':\n      case 'ESOCKETTIMEDOUT':\n        throw new Error(requestMsg.timeout)\n      case 'ENOTFOUND':\n        throw new Error(requestMsg.notConnectNetwork)\n      default:\n        throw err\n    }\n  })\n  // requestObj.promise = requestObj.promise.catch(async err => {\n  //   // console.log('出错', err)\n  //   if (err.message === 'socket hang up') {\n  //     // window.globalObj.apiSource = 'temp'\n  //     return Promise.reject(new Error(requestMsg.unachievable))\n  //   }\n  //   switch (err.code) {\n  //     case 'ETIMEDOUT':\n  //     case 'ESOCKETTIMEDOUT':\n  //       return Promise.reject(new Error(requestMsg.timeout))\n  //     case 'ENOTFOUND':\n  //       return Promise.reject(new Error(requestMsg.notConnectNetwork))\n  //     default:\n  //       return Promise.reject(err)\n  //   }\n  // })\n  // return requestPromise\n}\n\nexport type RequestOptions = Options\n"
  },
  {
    "path": "src/main/utils/store.ts",
    "content": "// import { writeFileSync } from 'atomically'\nimport { dialog, shell } from 'electron'\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { log } from '@common/utils'\n\ntype Stores = Record<string, Store>\n\nconst stores: Stores = {}\n\n\nclass Store {\n  private readonly filePath: string\n  private readonly dirPath: string\n  private store: Record<string, any>\n\n  private writeFile() {\n    const tempPath = this.filePath + '.' + Math.random().toString().substring(2, 10) + '.temp'\n    try {\n      fs.writeFileSync(tempPath, JSON.stringify(this.store, null, '\\t'), 'utf8')\n    } catch (err: any) {\n      if (err.code === 'ENOENT') {\n        fs.mkdirSync(this.dirPath, { recursive: true })\n        fs.writeFileSync(tempPath, JSON.stringify(this.store, null, '\\t'), 'utf8')\n      } else throw err\n    }\n    fs.renameSync(tempPath, this.filePath)\n  }\n\n  constructor(filePath: string, clearInvalidConfig: boolean = false) {\n    this.filePath = filePath\n    this.dirPath = path.dirname(this.filePath)\n\n    let store: Record<string, any>\n    if (fs.existsSync(this.filePath)) {\n      if (clearInvalidConfig) {\n        try {\n          store = JSON.parse(fs.readFileSync(this.filePath, 'utf8'))\n        } catch {\n          store = {}\n        }\n      } else store = JSON.parse(fs.readFileSync(this.filePath, 'utf8'))\n    } else store = {}\n\n    if (typeof store != 'object') {\n      if (clearInvalidConfig) store = {}\n      else throw new Error('parse data error: ' + String(store))\n    }\n    this.store = store\n  }\n\n  get<Value>(key: string): Value {\n    return this.store[key]\n  }\n\n  has(key: string): boolean {\n    return key in this.store\n  }\n\n  set(key: string, value: any) {\n    this.store[key] = value\n    this.writeFile()\n  }\n\n  override(value: Record<string, any>) {\n    this.store = value\n    this.writeFile()\n  }\n}\n\n/**\n * 获取 Store 对象\n * @param name store 名\n * @param isIgnoredError 是否忽略错误\n * @param isShowErrorAlert=true 是否显示错误弹窗\n * @returns Store\n */\nexport default (name: string, isIgnoredError = true, isShowErrorAlert = true): Store => {\n  if (stores[name]) return stores[name]\n  let store: Store\n  const storePath = path.join(global.lxDataPath, name + '.json')\n  try {\n    store = stores[name] = new Store(storePath, false)\n  } catch (err: any) {\n    const error = err as Error\n    log.error(error)\n\n    if (!isIgnoredError) throw error\n\n\n    const backPath = storePath + '.bak'\n    fs.renameSync(storePath, backPath)\n    if (isShowErrorAlert) {\n      dialog.showMessageBoxSync({\n        type: 'error',\n        message: name + ' data load error',\n        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}`,\n      })\n      shell.showItemInFolder(backPath)\n    }\n\n\n    store = new Store(storePath, true)\n  }\n  return store\n}\n\nexport {\n  Store,\n}\n"
  },
  {
    "path": "src/main/worker/dbService/db.ts",
    "content": "import Database from 'better-sqlite3'\nimport path from 'path'\nimport tables, { DB_VERSION } from './tables'\nimport verifyDB from './verifyDB'\nimport migrateData from './migrate'\n\nlet db: Database.Database\n\n\nconst initTables = (db: Database.Database) => {\n  db.exec(`\n    ${Array.from(tables.values()).join('\\n')}\n    INSERT INTO \"main\".\"db_info\" (\"field_name\", \"field_value\") VALUES ('version', '${DB_VERSION}');\n  `)\n}\n\n\n// 打开、初始化数据库\nexport const init = (lxDataPath: string): boolean | null => {\n  const databasePath = path.join(lxDataPath, 'lx.data.db')\n  const nativeBinding = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')\n  let dbFileExists = true\n\n  try {\n    db = new Database(databasePath, {\n      fileMustExist: true,\n      nativeBinding,\n      // verbose: process.env.NODE_ENV !== 'production' ? console.log : undefined,\n    })\n  } catch (error) {\n    console.log(error)\n    db = new Database(databasePath, {\n      nativeBinding,\n      // verbose: process.env.NODE_ENV !== 'production' ? console.log : undefined,\n    })\n    initTables(db)\n    dbFileExists = false\n  }\n  db.pragma('journal_mode = WAL')\n\n  if (dbFileExists) migrateData(db)\n\n  // https://www.sqlite.org/pragma.html#pragma_optimize\n  if (dbFileExists) db.exec('PRAGMA optimize;')\n  if (!verifyDB(db)) {\n    db.close()\n    return null\n  }\n\n  // https://www.sqlite.org/lang_vacuum.html\n  // db.exec('VACUUM \"main\"')\n\n  process.on('exit', () => db.close())\n  console.log('db inited')\n  // require('./test')\n  return dbFileExists\n}\n\n// 获取数据库实例\nexport const getDB = (): Database.Database => db\n"
  },
  {
    "path": "src/main/worker/dbService/index.ts",
    "content": "import { init } from './db'\nimport { exposeWorker } from '../utils/worker'\nimport { list, lyric, music_url, music_other_source, download, dislike_list } from './modules/index'\n\n\nconst common = {\n  init,\n}\n\nexposeWorker(Object.assign(common, list, lyric, music_url, music_other_source, download, dislike_list))\n\nexport type workerDBSeriveTypes = typeof common\n  & typeof list\n  & typeof lyric\n  & typeof music_url\n  & typeof music_other_source\n  & typeof download\n  & typeof dislike_list\n"
  },
  {
    "path": "src/main/worker/dbService/migrate.ts",
    "content": "import type Database from 'better-sqlite3'\nimport tables, { DB_VERSION } from './tables'\n\n// const migrateV1 = (db: Database.Database) => {\n//   const sql = `\n//     DROP TABLE \"main\".\"download_list\";\n\n//     CREATE TABLE \"download_list\" (\n//       \"id\" TEXT NOT NULL,\n//       \"isComplate\" INTEGER NOT NULL,\n//       \"status\" TEXT NOT NULL,\n//       \"statusText\" TEXT NOT NULL,\n//       \"progress_downloaded\" INTEGER NOT NULL,\n//       \"progress_total\" INTEGER NOT NULL,\n//       \"url\" TEXT,\n//       \"quality\" TEXT NOT NULL,\n//       \"ext\" TEXT NOT NULL,\n//       \"fileName\" TEXT NOT NULL,\n//       \"filePath\" TEXT NOT NULL,\n//       \"musicInfo\" TEXT NOT NULL,\n//       \"position\" INTEGER NOT NULL,\n//       PRIMARY KEY(\"id\")\n//     );\n//   `\n//   db.exec(sql)\n//   db.prepare('UPDATE \"main\".\"db_info\" SET \"field_value\"=@value WHERE \"field_name\"=@name').run({ name: 'version', value: '2' })\n// }\n\nconst migrateV1 = (db: Database.Database) => {\n  // 修复 v2.4.0 的默认数据库版本号不对的问题\n  const existsTable = db.prepare('SELECT name FROM \"main\".sqlite_master WHERE type=\\'table\\' AND name=\\'dislike_list\\';').get()\n  if (!existsTable) {\n    const sql = tables.get('dislike_list')!\n    db.exec(sql)\n  }\n}\n\nexport default (db: Database.Database) => {\n  // PRAGMA user_version = x\n  // console.log(db.prepare('PRAGMA user_version').get().user_version)\n  // https://github.com/WiseLibs/better-sqlite3/issues/668#issuecomment-1145285728\n  const version = (db.prepare<[string]>('SELECT \"field_value\" FROM \"main\".\"db_info\" WHERE \"field_name\" = ?').get('version') as { field_value: string }).field_value\n  switch (version) {\n    case '1':\n      migrateV1(db)\n      db.prepare('UPDATE \"main\".\"db_info\" SET \"field_value\"=@value WHERE \"field_name\"=@name').run({ name: 'version', value: DB_VERSION })\n      break\n  }\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/dislike_list/dbHelper.ts",
    "content": "// import type Database from 'better-sqlite3'\nimport { getDB } from '../../db'\nimport {\n  createQueryStatement,\n  createInsertStatement,\n  // createDeleteStatement,\n  // createUpdateStatement,\n  createClearStatement,\n} from './statements'\n\n/**\n * 查询不喜欢歌曲列表\n */\nexport const queryDislikeList = () => {\n  const queryStatement = createQueryStatement()\n  return queryStatement.all() as LX.DBService.DislikeInfo[]\n}\n\n/**\n * 批量插入不喜欢歌曲并刷新顺序\n * @param infos 列表\n */\nexport const insertDislikeList = async(infos: LX.DBService.DislikeInfo[]) => {\n  const db = getDB()\n  const insertStatement = createInsertStatement()\n  db.transaction((infos: LX.DBService.DislikeInfo[]) => {\n    for (const info of infos) insertStatement.run(info)\n  })(infos)\n}\n\n/**\n * 覆盖并批量插入不喜欢歌曲并刷新顺序\n * @param infos 列表\n */\nexport const overwirteDislikeList = async(infos: LX.DBService.DislikeInfo[]) => {\n  const db = getDB()\n  const clearStatement = createClearStatement()\n  const insertStatement = createInsertStatement()\n  db.transaction((infos: LX.DBService.DislikeInfo[]) => {\n    clearStatement.run()\n    for (const info of infos) insertStatement.run(info)\n  })(infos)\n}\n\n// /**\n//  * 批量删除不喜欢歌曲\n//  * @param ids 列表\n//  */\n// export const deleteDislikeList = (ids: string[]) => {\n//   const db = getDB()\n//   const deleteStatement = createDeleteStatement()\n//   db.transaction((ids: string[]) => {\n//     for (const id of ids) deleteStatement.run(BigInt(id))\n//   })(ids)\n// }\n\n// /**\n//  * 批量更新不喜欢歌曲\n//  * @param urlInfo 列表\n//  */\n// export const updateDislikeList = async(infos: LX.DBService.DislikeInfo[]) => {\n//   const db = getDB()\n//   const updateStatement = createUpdateStatement()\n//   db.transaction((infos: LX.DBService.DislikeInfo[]) => {\n//     for (const info of infos) updateStatement.run(info)\n//   })(infos)\n// }\n\n// /**\n//  * 清空不喜欢歌曲列表\n//  */\n// export const clearDislikeList = () => {\n//   const clearStatement = createClearStatement()\n//   clearStatement.run()\n// }\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/dislike_list/index.ts",
    "content": "import { SPLIT_CHAR } from '@common/constants'\nimport {\n  queryDislikeList,\n  insertDislikeList,\n  overwirteDislikeList,\n  // updateDislikeList,\n  // deleteDislikeList,\n  // clearDislikeList,\n} from './dbHelper'\n\n// let dislikeInfo: LX.Dislike.DislikeInfo\n\nconst toDBDislikeInfo = (musicInfos: string[]): LX.DBService.DislikeInfo[] => {\n  const list: LX.DBService.DislikeInfo[] = []\n  for (const item of musicInfos) {\n    if (!item.trim()) continue\n    list.push({\n      content: item,\n    })\n  }\n  return list\n}\n\nconst initDislikeList = () => {\n  const dislikeInfo: LX.Dislike.DislikeInfo = {\n    // musicIds: new Set<string>(),\n    names: new Set<string>(),\n    singerNames: new Set<string>(),\n    musicNames: new Set<string>(),\n    rules: '',\n  }\n  const list: string[] = []\n  for (const item of queryDislikeList()) {\n    if (!item) continue\n    let [name, singer] = item.content.split(SPLIT_CHAR.DISLIKE_NAME)\n    if (name) {\n      name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      if (singer) {\n        singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n        const rule = `${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`\n        dislikeInfo.names.add(rule)\n        list.push(rule)\n      } else {\n        dislikeInfo.musicNames.add(name)\n        list.push(name)\n      }\n    } else if (singer) {\n      singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      dislikeInfo.singerNames.add(singer)\n      list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n    }\n  }\n\n  dislikeInfo.rules = Array.from(new Set(list)).join('\\n')\n\n  return dislikeInfo\n}\n\n/**\n * 获取不喜欢列表信息\n * @returns 不喜欢列表信息\n */\nexport const getDislikeListInfo = (): LX.Dislike.DislikeInfo => {\n  // if (!dislikeInfo) initDislikeList()\n  return initDislikeList()\n}\n\n\n/**\n * 添加信息\n * @param lists 列表信息\n */\nexport const dislikeInfoAdd = async(lists: LX.Dislike.DislikeMusicInfo[]) => {\n  await insertDislikeList(lists.map(info => ({ content: `${info.name}${SPLIT_CHAR.DISLIKE_NAME}${info.singer}` })))\n}\n\n/**\n * 覆盖列表信息\n * @param rules 规则信息\n */\nexport const dislikeInfoOverwrite = async(rules: string) => {\n  await overwirteDislikeList(toDBDislikeInfo(rules.split('\\n')))\n}\n\n\n// /**\n//  * 删除不喜欢列表\n//  * @param ids 歌曲id\n//  */\n// export const dislikeInfoRemove = (ids: string[]) => {\n//   deleteDislikeList(ids)\n// }\n\n// /**\n//  * 清空不喜欢列表\n//  */\n// export const dislikeInfoClear = () => {\n//   clearDislikeList()\n// }\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/dislike_list/statements.ts",
    "content": "import { getDB } from '../../db'\n\n/**\n * 创建不喜欢列表查询语句\n * @returns 查询语句\n */\nexport const createQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    SELECT \"content\"\n    FROM dislike_list\n    WHERE \"type\"='music'\n  `)\n}\n\n/**\n * 创建不喜欢记录插入语句\n * @returns 插入语句\n */\nexport const createInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.DislikeInfo]>(`\n    INSERT INTO \"main\".\"dislike_list\" (\"type\", \"content\")\n    VALUES ('music', @content)`)\n}\n\n/**\n * 创建不喜欢记录清空语句\n * @returns 清空语句\n */\nexport const createClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"dislike_list\"\n  `)\n}\n\n// /**\n//  * 创建不喜欢记录删除语句\n//  * @returns 删除语句\n//  */\n// export const createDeleteStatement = () => {\n//   const db = getDB()\n//   return db.prepare<[bigint]>(`\n//     DELETE FROM \"main\".\"dislike_list\"\n//     WHERE \"id\"=?\n//   `)\n// }\n\n// /**\n//  * 创建不喜欢记录更新语句\n//  * @returns 更新语句\n//  */\n// export const createUpdateStatement = () => {\n//   const db = getDB()\n//   return db.prepare<[LX.DBService.DislikeInfo]>(`\n//     UPDATE \"main\".\"dislike_list\"\n//     SET \"name\"=@name, \"singer\"=@singer\n//     WHERE \"id\"=@id\n//   `)\n// }\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/download/dbHelper.ts",
    "content": "import { getDB } from '../../db'\nimport {\n  createQueryStatement,\n  createInsertStatement,\n  createDeleteStatement,\n  createUpdateStatement,\n  createUpdatePositionStatement,\n  createClearStatement,\n} from './statements'\n\n/**\n * 查询下载歌曲列表\n */\nexport const queryDownloadList = () => {\n  const queryStatement = createQueryStatement()\n  return queryStatement.all() as LX.DBService.DownloadMusicInfo[]\n}\n\n/**\n * 批量插入下载歌曲并刷新顺序\n * @param mInfos 列表\n */\nexport const insertDownloadList = (mInfos: LX.DBService.DownloadMusicInfo[], listPositions: Array<{ id: string, position: number }>) => {\n  const db = getDB()\n  const insertStatement = createInsertStatement()\n  const updatePositionStatement = createUpdatePositionStatement()\n  db.transaction((mInfos: LX.DBService.DownloadMusicInfo[]) => {\n    for (const info of mInfos) insertStatement.run(info)\n    for (const info of listPositions) updatePositionStatement.run(info)\n  })(mInfos)\n}\n\n/**\n * 批量删除下载歌曲\n * @param ids 列表\n */\nexport const deleteDownloadList = (ids: string[]) => {\n  const db = getDB()\n  const deleteStatement = createDeleteStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) deleteStatement.run(id)\n  })(ids)\n}\n\n/**\n * 批量更新下载歌曲\n * @param urlInfo 列表\n */\nexport const updateDownloadList = (urlInfo: LX.DBService.DownloadMusicInfo[]) => {\n  const db = getDB()\n  const updateStatement = createUpdateStatement()\n  db.transaction((urlInfo: LX.DBService.DownloadMusicInfo[]) => {\n    for (const info of urlInfo) updateStatement.run(info)\n  })(urlInfo)\n}\n\n/**\n * 清空下载歌曲列表\n */\nexport const clearDownloadList = () => {\n  const clearStatement = createClearStatement()\n  clearStatement.run()\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/download/index.ts",
    "content": "import { arrPush, arrUnshift } from '@common/utils/common'\nimport {\n  queryDownloadList,\n  insertDownloadList,\n  updateDownloadList,\n  deleteDownloadList,\n  clearDownloadList,\n} from './dbHelper'\n\nlet list: LX.Download.ListItem[]\n\nconst toDBDownloadInfo = (musicInfos: LX.Download.ListItem[], offset: number = 0): LX.DBService.DownloadMusicInfo[] => {\n  return musicInfos.map((info, index) => {\n    return {\n      id: info.id,\n      isComplate: info.isComplate ? 1 : 0,\n      status: info.status,\n      statusText: info.statusText,\n      progress_downloaded: info.downloaded,\n      progress_total: info.total,\n      url: info.metadata.url,\n      quality: info.metadata.quality,\n      ext: info.metadata.ext,\n      fileName: info.metadata.fileName,\n      filePath: info.metadata.filePath,\n      musicInfo: JSON.stringify(info.metadata.musicInfo),\n      position: offset + index,\n    }\n  })\n}\n\nconst initDownloadList = () => {\n  list = queryDownloadList().map(item => {\n    const musicInfo = JSON.parse(item.musicInfo) as LX.Music.MusicInfoOnline\n    return {\n      id: item.id,\n      isComplate: item.isComplate == 1,\n      status: item.status,\n      statusText: item.statusText,\n      downloaded: item.progress_downloaded,\n      total: item.progress_total,\n      progress: item.progress_total ? parseInt((item.progress_downloaded / item.progress_total).toFixed(2)) * 100 : 0,\n      speed: '',\n      writeQueue: 0,\n      metadata: {\n        musicInfo,\n        url: item.url,\n        quality: item.quality,\n        ext: item.ext,\n        fileName: item.fileName,\n        filePath: item.filePath,\n      },\n    }\n  })\n}\n\n/**\n * 获取下载列表\n * @returns 下载列表\n */\nexport const getDownloadList = (): LX.Download.ListItem[] => {\n  if (!list) initDownloadList()\n  return list\n}\n\n/**\n * 添加下载歌曲信息\n * @param downloadInfos url信息\n */\nexport const downloadInfoSave = (downloadInfos: LX.Download.ListItem[], addMusicLocationType: LX.AddMusicLocationType) => {\n  if (!list) initDownloadList()\n  if (addMusicLocationType == 'top') {\n    let newList = [...list]\n    arrUnshift(newList, downloadInfos)\n    insertDownloadList(toDBDownloadInfo(downloadInfos), newList.slice(downloadInfos.length - 1).map((info, index) => {\n      return { id: info.id, position: index }\n    }))\n    list = newList\n  } else {\n    insertDownloadList(toDBDownloadInfo(downloadInfos, list.length), [])\n    arrPush(list, downloadInfos)\n  }\n}\n\n/**\n * 批量更新列表信息\n * @param lists 列表信息\n */\nexport const downloadInfoUpdate = (lists: LX.Download.ListItem[]) => {\n  updateDownloadList(toDBDownloadInfo(lists))\n  if (list) {\n    for (const item of lists) {\n      const index = list.findIndex(info => info.id === item.id)\n      if (index < 0) continue\n      list.splice(index, 1, item)\n    }\n  }\n}\n\n\n/**\n * 删除下载列表\n * @param ids 歌曲id\n */\nexport const downloadInfoRemove = (ids: string[]) => {\n  deleteDownloadList(ids)\n  if (list) {\n    const idSet = new Set<string>(ids)\n    list = list.filter(task => !idSet.has(task.id))\n  }\n}\n\n/**\n * 清空下载列表\n */\nexport const downloadInfoClear = () => {\n  clearDownloadList()\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/download/statements.ts",
    "content": "import { getDB } from '../../db'\n\n/**\n * 创建下载列表查询语句\n * @returns 查询语句\n */\nexport const createQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    SELECT \"id\", \"isComplate\", \"status\", \"statusText\", \"progress_downloaded\", \"progress_total\", \"url\", \"quality\", \"ext\", \"fileName\", \"filePath\", \"musicInfo\", \"position\"\n    FROM download_list\n    ORDER BY \"position\" ASC\n  `)\n}\n\n/**\n * 创建下载记录插入语句\n * @returns 插入语句\n */\nexport const createInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.DownloadMusicInfo]>(`\n    INSERT INTO \"main\".\"download_list\" (\"id\", \"isComplate\", \"status\", \"statusText\", \"progress_downloaded\", \"progress_total\", \"url\", \"quality\", \"ext\", \"fileName\", \"filePath\", \"musicInfo\", \"position\")\n    VALUES (@id, @isComplate, @status, @statusText, @progress_downloaded, @progress_total, @url, @quality, @ext, @fileName, @filePath, @musicInfo, @position)`)\n}\n\n/**\n * 创建下载记录清空语句\n * @returns 清空语句\n */\nexport const createClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"download_list\"\n  `)\n}\n\n/**\n * 创建下载记录删除语句\n * @returns 删除语句\n */\nexport const createDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    DELETE FROM \"main\".\"download_list\"\n    WHERE \"id\"=?\n  `)\n}\n\n/**\n * 创建下载记录更新语句\n * @returns 更新语句\n */\nexport const createUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.DownloadMusicInfo]>(`\n    UPDATE \"main\".\"download_list\"\n    SET \"isComplate\"=@isComplate, \"status\"=@status, \"statusText\"=@statusText, \"progress_downloaded\"=@progress_downloaded, \"progress_total\"=@progress_total, \"url\"=@url, \"filePath\"=@filePath\n    WHERE \"id\"=@id`)\n}\n\n/**\n * 创建下载记录顺序更新语句\n * @returns 更新语句\n */\nexport const createUpdatePositionStatement = () => {\n  const db = getDB()\n  return db.prepare<[{ id: string, position: number }]>(`\n    UPDATE \"main\".\"download_list\"\n    SET \"position\"=@position\n    WHERE \"id\"=@id`)\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/index.ts",
    "content": "\nexport * as list from './list'\nexport * as lyric from './lyric'\nexport * as music_url from './music_url'\nexport * as music_other_source from './music_other_source'\nexport * as download from './download'\nexport * as dislike_list from './dislike_list'\n"
  },
  {
    "path": "src/main/worker/dbService/modules/list/dbHelper.ts",
    "content": "import { getDB } from '../../db'\nimport {\n  createListQueryStatement,\n  createListInsertStatement,\n  createListDeleteStatement,\n  createListClearStatement,\n  createListUpdateStatement,\n  createMusicInfoQueryStatement,\n  createMusicInfoInsertStatement,\n  createMusicInfoUpdateStatement,\n  createMusicInfoDeleteStatement,\n  createMusicInfoDeleteByListIdStatement,\n  createMusicInfoOrderInsertStatement,\n  createMusicInfoOrderDeleteStatement,\n  createMusicInfoOrderDeleteByListIdStatement,\n  createMusicInfoClearStatement,\n  createMusicInfoOrderClearStatement,\n  createMusicInfoByListAndMusicInfoIdQueryStatement,\n  createMusicInfoByMusicInfoIdQueryStatement,\n} from './statements'\n\nconst idFixRxp = /\\.0$/\n/**\n * 获取用户列表\n * @returns\n */\nexport const queryAllUserList = () => {\n  const list = createListQueryStatement().all() as LX.DBService.UserListInfo[]\n  for (const info of list) {\n    // 兼容v2.3.0之前版本插入数字类型的ID导致其意外在末尾追加 .0 的问题\n    if (info.sourceListId?.endsWith?.('.0')) {\n      info.sourceListId = info.sourceListId.replace(idFixRxp, '')\n    }\n  }\n  return list\n}\n\n/**\n * 批量插入用户列表\n * @param lists 列表\n * @param isClear 是否清空列表\n */\nexport const insertUserLists = (lists: LX.DBService.UserListInfo[], isClear: boolean = false) => {\n  const db = getDB()\n  const listClearStatement = createListClearStatement()\n  const listInsertStatement = createListInsertStatement()\n  db.transaction((lists: LX.DBService.UserListInfo[]) => {\n    if (isClear) listClearStatement.run()\n    for (const list of lists) {\n      listInsertStatement.run({\n        id: list.id,\n        name: list.name,\n        source: list.source,\n        sourceListId: list.sourceListId,\n        locationUpdateTime: list.locationUpdateTime,\n        position: list.position,\n      })\n    }\n  })(lists)\n}\n\n/**\n * 批量删除用户列表及列表内歌曲\n * @param listIds 列表id\n */\nexport const deleteUserLists = (listIds: string[]) => {\n  const db = getDB()\n  const listDeleteStatement = createListDeleteStatement()\n  const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n  db.transaction((listIds: string[]) => {\n    for (const id of listIds) {\n      listDeleteStatement.run(id)\n      musicInfoDeleteByListIdStatement.run(id)\n      musicInfoOrderDeleteByListIdStatement.run(id)\n    }\n  })(listIds)\n}\n\n/**\n * 批量更新用户列表\n * @param lists 列表\n */\nexport const updateUserLists = (lists: LX.DBService.UserListInfo[]) => {\n  const db = getDB()\n  const listUpdateStatement = createListUpdateStatement()\n  db.transaction((lists: LX.DBService.UserListInfo[]) => {\n    for (const list of lists) listUpdateStatement.run(list)\n  })(lists)\n}\n\n\n/**\n * 批量添加歌曲\n * @param list\n */\nexport const insertMusicInfoList = (list: LX.DBService.MusicInfo[]) => {\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  const db = getDB()\n  db.transaction((musics: LX.DBService.MusicInfo[]) => {\n    for (const music of musics) {\n      musicInfoInsertStatement.run(music)\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n  })(list)\n}\n\n/**\n * 批量添加歌曲并刷新排序\n * @param list 新增歌曲\n * @param listId 列表Id\n * @param listAll 原始列表歌曲，列表去重后\n */\nexport const insertMusicInfoListAndRefreshOrder = (list: LX.DBService.MusicInfo[], listId: string, listAll: LX.DBService.MusicInfo[]) => {\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n\n  const db = getDB()\n  db.transaction((list: LX.DBService.MusicInfo[], listId: string, listAll: LX.DBService.MusicInfo[]) => {\n    musicInfoOrderDeleteByListIdStatement.run(listId)\n    for (const music of list) {\n      musicInfoInsertStatement.run(music)\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n    for (const music of listAll) {\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n  })(list, listId, listAll)\n}\n\n/**\n * 批量更新歌曲\n * @param list\n */\nexport const updateMusicInfos = (list: LX.DBService.MusicInfo[]) => {\n  const musicInfoUpdateStatement = createMusicInfoUpdateStatement()\n  const db = getDB()\n  db.transaction((musics: LX.DBService.MusicInfo[]) => {\n    for (const music of musics) {\n      musicInfoUpdateStatement.run(music)\n    }\n  })(list)\n}\n\n/**\n * 获取列表内的歌曲\n * @param listId 列表Id\n * @returns 列表歌曲\n */\nexport const queryMusicInfoByListId = (listId: string) => {\n  const musicInfoQueryStatement = createMusicInfoQueryStatement()\n  return musicInfoQueryStatement.all({ listId }) as LX.DBService.MusicInfo[]\n}\n\n/**\n * 批量移动歌曲\n * @param fromId 源列表Id\n * @param ids 要移动的歌曲\n * @param musicInfos 音乐信息\n */\nexport const moveMusicInfo = (fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[]) => {\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  const musicInfoDeleteStatement = createMusicInfoDeleteStatement()\n  const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement()\n  // const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n\n  const db = getDB()\n  db.transaction((fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[]) => {\n    // musicInfoOrderDeleteByListIdStatement.run(fromId)\n    for (const id of ids) {\n      musicInfoDeleteStatement.run({ listId: fromId, id })\n      musicInfoOrderDeleteStatement.run({ listId: fromId, id })\n    }\n    for (const music of musicInfos) {\n      musicInfoInsertStatement.run(music)\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n  })(fromId, ids, musicInfos)\n}\n\n/**\n * 批量移动歌曲并刷新排序\n * @param fromId 源列表Id\n * @param ids 要移动的歌曲id，原始选择的歌曲\n * @param musicInfos 要移动的歌曲，目标列表去重后\n * @param toListAll 目标列表歌曲\n */\nexport const moveMusicInfoAndRefreshOrder = (fromId: string, ids: string[], toId: string, musicInfos: LX.DBService.MusicInfo[], toListAll: LX.DBService.MusicInfo[]) => {\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoDeleteStatement = createMusicInfoDeleteStatement()\n  const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n\n  const db = getDB()\n  db.transaction((fromId: string, ids: string[], musicInfos: LX.DBService.MusicInfo[], toListAll: LX.DBService.MusicInfo[]) => {\n    for (const id of ids) {\n      musicInfoDeleteStatement.run({ listId: fromId, id })\n      musicInfoOrderDeleteStatement.run({ listId: fromId, id })\n    }\n    musicInfoOrderDeleteByListIdStatement.run(toId)\n    for (const music of musicInfos) {\n      musicInfoInsertStatement.run(music)\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n    for (const music of toListAll) {\n      musicInfoOrderInsertStatement.run({\n        listId: music.listId,\n        musicInfoId: music.id,\n        order: music.order,\n      })\n    }\n  })(fromId, ids, musicInfos, toListAll)\n}\n\n/**\n * 批量移除列表内音乐\n * @param listId 列表id\n * @param ids 音乐id\n */\nexport const removeMusicInfos = (listId: string, ids: string[]) => {\n  const musicInfoDeleteStatement = createMusicInfoDeleteStatement()\n  const musicInfoOrderDeleteStatement = createMusicInfoOrderDeleteStatement()\n  const db = getDB()\n  db.transaction((listId: string, ids: string[]) => {\n    for (const id of ids) {\n      musicInfoDeleteStatement.run({ listId, id })\n      musicInfoOrderDeleteStatement.run({ listId, id })\n    }\n  })(listId, ids)\n}\n\n/**\n * 清空列表内歌曲\n * @param listId 列表id\n */\nexport const removeMusicInfoByListId = (ids: string[]) => {\n  const db = getDB()\n  const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) {\n      musicInfoDeleteByListIdStatement.run(id)\n      musicInfoOrderDeleteByListIdStatement.run(id)\n    }\n  })(ids)\n}\n\n/**\n * 创建根据列表Id与音乐id查询音乐信息\n * @param listId 列表id\n * @param musicInfoId 音乐id\n * @returns\n */\nexport const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string) => {\n  const musicInfoByListAndMusicInfoIdQueryStatement = createMusicInfoByListAndMusicInfoIdQueryStatement()\n  return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId }) as LX.DBService.MusicInfo | null\n}\n\n/**\n * 创建根据音乐id查询所有列表的音乐信息\n * @param id 音乐id\n * @returns\n */\nexport const queryMusicInfoByMusicInfoId = (id: string) => {\n  const musicInfoByMusicInfoIdQueryStatement = createMusicInfoByMusicInfoIdQueryStatement()\n  return musicInfoByMusicInfoIdQueryStatement.all(id) as LX.DBService.MusicInfo[]\n}\n\n/**\n * 批量更新歌曲位置\n * @param listId 列表id\n * @param musicInfoOrders 音乐顺序\n */\nexport const updateMusicInfoOrder = (listId: string, musicInfoOrders: LX.DBService.MusicInfoOrder[]) => {\n  const db = getDB()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n  db.transaction((listId: string, musicInfoOrders: LX.DBService.MusicInfoOrder[]) => {\n    musicInfoOrderDeleteByListIdStatement.run(listId)\n    for (const orderInfo of musicInfoOrders) musicInfoOrderInsertStatement.run(orderInfo)\n  })(listId, musicInfoOrders)\n}\n\n/**\n * 覆盖列表内的歌曲\n * @param listId 列表id\n * @param musicInfos 歌曲列表\n */\nexport const overwriteMusicInfo = (listId: string, musicInfos: LX.DBService.MusicInfo[]) => {\n  const db = getDB()\n  const musicInfoDeleteByListIdStatement = createMusicInfoDeleteByListIdStatement()\n  const musicInfoOrderDeleteByListIdStatement = createMusicInfoOrderDeleteByListIdStatement()\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  db.transaction((listId: string, musicInfos: LX.DBService.MusicInfo[]) => {\n    musicInfoDeleteByListIdStatement.run(listId)\n    musicInfoOrderDeleteByListIdStatement.run(listId)\n    for (const musicInfo of musicInfos) {\n      musicInfoInsertStatement.run(musicInfo)\n      musicInfoOrderInsertStatement.run({\n        listId: musicInfo.listId,\n        musicInfoId: musicInfo.id,\n        order: musicInfo.order,\n      })\n    }\n  })(listId, musicInfos)\n}\n\n/**\n * 覆盖整个列表\n * @param lists 列表\n * @param musicInfos 歌曲列表\n */\nexport const overwriteListData = (lists: LX.DBService.UserListInfo[], musicInfos: LX.DBService.MusicInfo[]) => {\n  const db = getDB()\n  const listClearStatement = createListClearStatement()\n  const listInsertStatement = createListInsertStatement()\n  const musicInfoClearStatement = createMusicInfoClearStatement()\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  const musicInfoOrderClearStatement = createMusicInfoOrderClearStatement()\n  const musicInfoOrderInsertStatement = createMusicInfoOrderInsertStatement()\n  db.transaction((lists: LX.DBService.UserListInfo[], musicInfos: LX.DBService.MusicInfo[]) => {\n    listClearStatement.run()\n    for (const list of lists) {\n      listInsertStatement.run({\n        id: list.id,\n        name: list.name,\n        source: list.source,\n        sourceListId: list.sourceListId,\n        locationUpdateTime: list.locationUpdateTime,\n        position: list.position,\n      })\n    }\n    musicInfoClearStatement.run()\n    musicInfoOrderClearStatement.run()\n    for (const musicInfo of musicInfos) {\n      musicInfoInsertStatement.run(musicInfo)\n      musicInfoOrderInsertStatement.run({\n        listId: musicInfo.listId,\n        musicInfoId: musicInfo.id,\n        order: musicInfo.order,\n      })\n    }\n  })(lists, musicInfos)\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/list/index.ts",
    "content": "import { LIST_IDS } from '@common/constants'\nimport { arrPush, arrPushByPosition, arrUnshift } from '@common/utils/common'\nimport {\n  deleteUserLists,\n  insertUserLists,\n  insertMusicInfoList,\n  insertMusicInfoListAndRefreshOrder,\n  moveMusicInfo,\n  moveMusicInfoAndRefreshOrder,\n  overwriteListData,\n  overwriteMusicInfo,\n  queryAllUserList,\n  queryMusicInfoByListId,\n  queryMusicInfoByListIdAndMusicInfoId,\n  queryMusicInfoByMusicInfoId,\n  removeMusicInfoByListId,\n  removeMusicInfos,\n  updateMusicInfoOrder,\n  updateMusicInfos,\n  updateUserLists as updateUserListsFromDB,\n} from './dbHelper'\n\nlet userLists: LX.DBService.UserListInfo[]\nlet musicLists = new Map<string, LX.Music.MusicInfo[]>()\n\nconst toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset: number = 0): LX.DBService.MusicInfo[] => {\n  return musicInfos.map((info, index) => {\n    return {\n      ...info,\n      listId,\n      meta: JSON.stringify(info.meta),\n      order: offset + index,\n    }\n  })\n}\n\n/**\n * 获取所有用户列表\n * @returns\n */\nexport const getAllUserList = (): LX.List.UserListInfo[] => {\n  userLists ??= queryAllUserList()\n\n  return userLists.map(list => {\n    const { position, ...newList } = list\n    return newList\n  })\n}\n\n/**\n * 批量创建列表\n * @param position 列表位置\n * @param lists 列表信息\n */\nexport const createUserLists = (position: number, lists: LX.List.UserListInfo[]) => {\n  userLists ??= queryAllUserList()\n  if (position < 0 || position >= userLists.length) {\n    const newLists: LX.DBService.UserListInfo[] = lists.map((list, index) => {\n      return {\n        ...list,\n        position: position + index,\n      }\n    })\n    insertUserLists(newLists)\n    userLists = [...userLists, ...newLists]\n  } else {\n    const newUserLists = [...userLists]\n    // @ts-expect-error\n    newUserLists.splice(position, 0, ...lists)\n    newUserLists.forEach((list, index) => {\n      list.position = index\n    })\n    insertUserLists(newUserLists, true)\n    userLists = newUserLists\n  }\n}\n\n/**\n * 覆盖列表\n * @param lists 列表信息\n */\n// const setUserLists = (lists: LX.List.UserListInfo[]) => {\n//   const newUserLists: LX.DBService.UserListInfo[] = lists.map((list, index) => {\n//     return {\n//       ...list,\n//       position: index,\n//     }\n//   })\n//   insertUserLists(newUserLists, true)\n//   userLists = newUserLists\n// }\n\n/**\n * 批量删除列表\n * @param ids 列表ids\n */\nexport const removeUserLists = (ids: string[]) => {\n  deleteUserLists(ids)\n  userLists &&= queryAllUserList()\n}\n\n/**\n * 批量更新列表信息\n * @param lists 列表信息\n */\nexport const updateUserLists = (lists: LX.List.UserListInfo[]) => {\n  const positionMap = new Map<string, number>()\n  for (const list of userLists) {\n    positionMap.set(list.id, list.position)\n  }\n  const dbList: LX.DBService.UserListInfo[] = lists.map(list => {\n    const position = positionMap.get(list.id)\n    if (position == null) return null\n    return {\n      ...list,\n      position,\n    }\n  }).filter(Boolean) as LX.DBService.UserListInfo[]\n  updateUserListsFromDB(dbList)\n  userLists &&= queryAllUserList()\n}\n\n/**\n * 批量更新列表位置\n * @param position 列表位置\n * @param ids 列表ids\n */\nexport const updateUserListsPosition = (position: number, ids: string[]) => {\n  userLists ??= queryAllUserList()\n\n  const newUserLists = [...userLists]\n\n  const updateLists: LX.DBService.UserListInfo[] = []\n\n  for (let i = newUserLists.length - 1; i >= 0; i--) {\n    if (ids.includes(newUserLists[i].id)) {\n      const list = newUserLists.splice(i, 1)[0]\n      list.locationUpdateTime = Date.now()\n      updateLists.push(list)\n    }\n  }\n  position = Math.min(newUserLists.length, position)\n\n  newUserLists.splice(position, 0, ...updateLists)\n  newUserLists.forEach((list, index) => {\n    list.position = index\n  })\n  insertUserLists(newUserLists, true)\n  userLists = newUserLists\n}\n\n/**\n * 根据列表ID获取列表内歌曲\n * @param listId 列表ID\n * @returns 列表内歌曲\n */\nexport const getListMusics = (listId: string): LX.Music.MusicInfo[] => {\n  let targetList: LX.Music.MusicInfo[] | undefined = musicLists.get(listId)\n  if (targetList == null) {\n    targetList = queryMusicInfoByListId(listId).map(info => {\n      return {\n        id: info.id,\n        name: info.name,\n        singer: info.singer,\n        source: info.source,\n        interval: info.interval,\n        meta: JSON.parse(info.meta),\n      }\n    })\n    musicLists.set(listId, targetList)\n  }\n\n  return targetList\n}\n\n/**\n * 覆盖列表内的歌曲\n * @param listId 列表id\n * @param musicInfos 歌曲列表\n */\nexport const musicOverwrite = (listId: string, musicInfos: LX.Music.MusicInfo[]) => {\n  let targetList = getListMusics(listId)\n  overwriteMusicInfo(listId, toDBMusicInfo(musicInfos, listId))\n  if (targetList) {\n    targetList.splice(0, targetList.length)\n    arrPush(targetList, musicInfos)\n  }\n}\n\n/**\n * 批量添加歌曲\n * @param listId 列表id\n * @param musicInfos 添加的歌曲信息\n * @param addMusicLocationType 添加在到列表的位置\n */\nexport const musicsAdd = (listId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n  let targetList = getListMusics(listId)\n\n  const set = new Set<string>()\n  for (const item of targetList) set.add(item.id)\n  musicInfos = musicInfos.filter(item => {\n    if (set.has(item.id)) return false\n    set.add(item.id)\n    return true\n  })\n\n  switch (addMusicLocationType) {\n    case 'top':\n      insertMusicInfoListAndRefreshOrder(toDBMusicInfo(musicInfos, listId), listId, toDBMusicInfo(targetList, listId, musicInfos.length))\n      arrUnshift(targetList, musicInfos)\n      break\n    case 'bottom':\n    default:\n      insertMusicInfoList(toDBMusicInfo(musicInfos, listId, targetList.length))\n      arrPush(targetList, musicInfos)\n      break\n  }\n}\n\n/**\n * 批量删除歌曲\n * @param listId 列表Id\n * @param ids 要删除歌曲的id\n */\nexport const musicsRemove = (listId: string, ids: string[]) => {\n  let targetList = getListMusics(listId)\n  if (!targetList.length) return\n  removeMusicInfos(listId, ids)\n  const idsSet = new Set<string>(ids)\n  musicLists.set(listId, targetList.filter(mInfo => !idsSet.has(mInfo.id)))\n}\n\n/**\n * 批量移动歌曲\n * @param fromId 源列表id\n * @param toId 目标列表id\n * @param musicInfos 添加的歌曲信息\n * @param addMusicLocationType 添加在到列表的位置\n */\nexport const musicsMove = (fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n  let fromList = getListMusics(fromId)\n  let toList = getListMusics(toId)\n\n  const ids = musicInfos.map(musicInfo => musicInfo.id)\n\n  let listSet = new Set<string>()\n  for (const item of toList) listSet.add(item.id)\n  musicInfos = musicInfos.filter(item => {\n    if (listSet.has(item.id)) return false\n    listSet.add(item.id)\n    return true\n  })\n\n  switch (addMusicLocationType) {\n    case 'top':\n      moveMusicInfoAndRefreshOrder(fromId, ids, toId, toDBMusicInfo(musicInfos, toId), toDBMusicInfo(toList, toId, musicInfos.length))\n      arrUnshift(toList, musicInfos)\n      break\n    case 'bottom':\n    default:\n      moveMusicInfo(fromId, ids, toDBMusicInfo(musicInfos, toId, toList.length))\n      arrPush(toList, musicInfos)\n      break\n  }\n\n  listSet = new Set<string>(ids)\n  musicLists.set(fromId, fromList.filter(mInfo => !listSet.has(mInfo.id)))\n}\n\n/**\n * 批量更新歌曲信息\n * @param musicInfos 歌曲&列表信息\n */\nexport const musicsUpdate = (musicInfos: LX.List.ListActionMusicUpdate) => {\n  updateMusicInfos(musicInfos.map(({ id, musicInfo }) => {\n    return {\n      ...musicInfo,\n      listId: id,\n      meta: JSON.stringify(musicInfo.meta),\n      order: 0,\n    }\n  }))\n  for (const { id, musicInfo } of musicInfos) {\n    const targetList = musicLists.get(id)\n    if (targetList == null) continue\n    const targetMusic = targetList.find(item => item.id == musicInfo.id)\n    if (!targetMusic) continue\n    targetMusic.name = musicInfo.name\n    targetMusic.singer = musicInfo.singer\n    targetMusic.source = musicInfo.source\n    targetMusic.interval = musicInfo.interval\n    targetMusic.meta = musicInfo.meta\n  }\n}\n\n/**\n * 清空列表内的歌曲\n * @param listId 列表Id\n */\nexport const musicsClear = (ids: string[]) => {\n  removeMusicInfoByListId(ids)\n  for (const id of ids) {\n    const targetList = musicLists.get(id)\n    if (!targetList) continue\n    targetList.splice(0, targetList.length)\n  }\n}\n\n/**\n * 批量更新歌曲位置\n * @param listId 列表id\n * @param position 新位置\n * @param ids 要更新位置的歌曲id\n */\nexport const musicsPositionUpdate = (listId: string, position: number, ids: string[]) => {\n  let targetList = getListMusics(listId)\n  if (!targetList.length) return\n\n  let newTargetList = [...targetList]\n\n  const infos: LX.Music.MusicInfo[] = []\n  const map = new Map<string, LX.Music.MusicInfo>()\n  for (const item of newTargetList) map.set(item.id, item)\n  for (const id of ids) {\n    infos.push(map.get(id)!)\n    map.delete(id)\n  }\n  newTargetList = newTargetList.filter(mInfo => map.has(mInfo.id))\n  arrPushByPosition(newTargetList, infos, Math.min(position, newTargetList.length))\n\n  updateMusicInfoOrder(listId, newTargetList.map((info, index) => {\n    return {\n      listId,\n      musicInfoId: info.id,\n      order: index,\n    }\n  }))\n  musicLists.set(listId, newTargetList)\n}\n\n/**\n * 覆盖所有列表数据\n * @param myListData 完整列表数据\n */\nexport const listDataOverwrite = (myListData: MakeOptional<LX.List.ListDataFull, 'tempList'>) => {\n  const dbLists: LX.DBService.UserListInfo[] = []\n  const listData: LX.List.ListDataFull = {\n    ...myListData,\n    tempList: myListData.tempList ?? getListMusics(LIST_IDS.TEMP),\n  }\n\n  const dbMusicInfos: LX.DBService.MusicInfo[] = [\n    ...toDBMusicInfo(listData.defaultList, LIST_IDS.DEFAULT),\n    ...toDBMusicInfo(listData.loveList, LIST_IDS.LOVE),\n    ...toDBMusicInfo(listData.tempList, LIST_IDS.TEMP),\n  ]\n  listData.userList.forEach(({ list, ...listInfo }, index) => {\n    dbLists.push({ ...listInfo, position: index })\n    arrPush(dbMusicInfos, toDBMusicInfo(list, listInfo.id))\n  })\n  overwriteListData(dbLists, dbMusicInfos)\n\n  if (userLists) userLists.splice(0, userLists.length, ...dbLists)\n  else userLists = dbLists\n\n  musicLists.clear()\n  musicLists.set(LIST_IDS.DEFAULT, listData.defaultList)\n  musicLists.set(LIST_IDS.LOVE, listData.loveList)\n  musicLists.set(LIST_IDS.TEMP, listData.tempList)\n  for (const list of listData.userList) musicLists.set(list.id, list.list)\n}\n\n/**\n * 检查音乐是否存在列表中\n * @param listId 列表id\n * @param musicInfoId 音乐id\n * @returns\n */\nexport const checkListExistMusic = (listId: string, musicInfoId: string): boolean => {\n  const musicInfo = queryMusicInfoByListIdAndMusicInfoId(listId, musicInfoId)\n  return musicInfo != null\n}\n\n/**\n * 获取所有存在该音乐的列表id\n * @param musicInfoId 音乐id\n * @returns\n */\nexport const getMusicExistListIds = (musicInfoId: string): string[] => {\n  const musicInfos = queryMusicInfoByMusicInfoId(musicInfoId)\n  return musicInfos.map(m => m.listId)\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/list/statements.ts",
    "content": "import { getDB } from '../../db'\n\n\n/**\n * 创建列表查询语句\n * @returns 查询语句\n */\nexport const createListQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    SELECT \"id\", \"name\", \"source\", \"sourceListId\", \"position\", \"locationUpdateTime\"\n    FROM \"main\".\"my_list\"\n    `)\n}\n\n/**\n * 创建列表插入语句\n * @returns 插入语句\n */\nexport const createListInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.UserListInfo]>(`\n    INSERT INTO \"main\".\"my_list\" (\"id\", \"name\", \"source\", \"sourceListId\", \"position\", \"locationUpdateTime\")\n    VALUES (@id, @name, @source, @sourceListId, @position, @locationUpdateTime)`)\n}\n\n/**\n * 创建列表清空语句\n * @returns 清空语句\n */\nexport const createListClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>('DELETE FROM \"main\".\"my_list\"')\n}\n\n/**\n * 创建列表删除语句\n * @returns 删除语句\n */\nexport const createListDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>('DELETE FROM \"main\".\"my_list\" WHERE \"id\"=?')\n}\n\n/**\n * 创建列表更新语句\n * @returns 更新语句\n */\nexport const createListUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.UserListInfo]>(`\n    UPDATE \"main\".\"my_list\"\n    SET \"name\"=@name, \"source\"=@source, \"sourceListId\"=@sourceListId, \"locationUpdateTime\"=@locationUpdateTime\n    WHERE \"id\"=@id`)\n}\n\n/**\n * 创建音乐信息查询语句\n * @returns 查询语句\n */\nexport const createMusicInfoQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfoQuery]>(`\n    SELECT mInfo.\"id\", mInfo.\"name\", mInfo.\"singer\", mInfo.\"source\", mInfo.\"interval\", mInfo.\"meta\"\n    FROM my_list_music_info mInfo\n    LEFT JOIN my_list_music_info_order O\n    ON mInfo.id=O.musicInfoId AND O.listId=@listId\n    WHERE mInfo.listId=@listId\n    ORDER BY O.\"order\" ASC\n    `)\n}\n\n/**\n * 创建音乐信息插入语句\n * @returns 插入语句\n */\nexport const createMusicInfoInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfo]>(`\n    INSERT INTO \"main\".\"my_list_music_info\" (\"id\", \"listId\", \"name\", \"singer\", \"source\", \"interval\", \"meta\")\n    VALUES (@id, @listId, @name, @singer, @source, @interval, @meta)`)\n}\n\n/**\n * 创建音乐信息更新语句\n * @returns 更新语句\n */\nexport const createMusicInfoUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfo]>(`\n    UPDATE \"main\".\"my_list_music_info\"\n    SET \"name\"=@name, \"singer\"=@singer, \"source\"=@source, \"interval\"=@interval, \"meta\"=@meta\n    WHERE \"id\"=@id AND \"listId\"=@listId`)\n}\n\n\n/**\n * 创建清空音乐信息语句\n * @returns 删除语句\n */\nexport const createMusicInfoClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>('DELETE FROM \"main\".\"my_list_music_info\"')\n}\n\n/**\n * 创建根据列表id批量删除音乐信息语句\n * @returns 删除语句\n */\nexport const createMusicInfoDeleteByListIdStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>('DELETE FROM \"main\".\"my_list_music_info\" WHERE \"listId\"=?')\n}\n\n/**\n * 创建根据列表Id与音乐id批量删除音乐信息语句\n * @returns 删除语句\n */\nexport const createMusicInfoDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfoRemove]>('DELETE FROM \"main\".\"my_list_music_info\" WHERE \"id\"=@id AND \"listId\"=@listId')\n}\n\n/**\n * 创建根据列表Id与音乐id查询音乐信息语句\n * @returns 删除语句\n */\nexport const createMusicInfoByListAndMusicInfoIdQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.ListMusicInfoQuery]>(`SELECT \"id\", \"name\", \"singer\", \"source\", \"interval\", \"meta\"\n    FROM \"main\".\"my_list_music_info\"\n    WHERE \"id\"=@musicInfoId\n    AND \"listId\"=@listId`)\n}\n\n/**\n * 创建根据音乐id查询音乐信息语句\n * @returns 删除语句\n */\nexport const createMusicInfoByMusicInfoIdQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`SELECT \"id\", \"name\", \"singer\", \"source\", \"interval\", \"meta\", \"listId\"\n    FROM \"main\".\"my_list_music_info\"\n    WHERE \"id\"=?`)\n}\n\n\n/**\n * 创建音乐信息排序插入语句\n * @returns 插入语句\n */\nexport const createMusicInfoOrderInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfoOrder]>(`\n    INSERT INTO \"main\".\"my_list_music_info_order\" (\"listId\", \"musicInfoId\", \"order\")\n    VALUES (@listId, @musicInfoId, @order)`)\n}\n\n/**\n * 创建清空音乐排序语句\n * @returns 删除语句\n */\nexport const createMusicInfoOrderClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>('DELETE FROM \"main\".\"my_list_music_info_order\"')\n}\n\n/**\n * 创建根据列表id删除音乐排序语句\n * @returns 删除语句\n */\nexport const createMusicInfoOrderDeleteByListIdStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>('DELETE FROM \"main\".\"my_list_music_info_order\" WHERE \"listId\"=?')\n}\n\n/**\n * 创建根据列表Id与音乐id删除音乐排序语句\n * @returns 删除语句\n */\nexport const createMusicInfoOrderDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfoRemove]>('DELETE FROM \"main\".\"my_list_music_info_order\" WHERE \"musicInfoId\"=@id AND \"listId\"=@listId')\n}\n\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/lyric/dbHelper.ts",
    "content": "import { getDB } from '../../db'\nimport {\n  createLyricQueryStatement,\n  createRawLyricQueryStatement,\n  createRawLyricInsertStatement,\n  createRawLyricDeleteStatement,\n  createRawLyricUpdateStatement,\n  createRawLyricClearStatement,\n  createEditedLyricQueryStatement,\n  createEditedLyricInsertStatement,\n  createEditedLyricDeleteStatement,\n  createEditedLyricUpdateStatement,\n  createEditedLyricClearStatement,\n  createEditedLyricCountStatement,\n  createRawLyricCountStatement,\n} from './statements'\n\n/**\n * 查询原始歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const queryLyric = (id: string) => {\n  const lyricQueryStatement = createLyricQueryStatement()\n  return lyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]\n}\n\n/**\n * 查询原始歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const queryRawLyric = (id: string) => {\n  const rawLyricQueryStatement = createRawLyricQueryStatement()\n  return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]\n}\n\n/**\n * 批量插入原始歌词\n * @param lyrics 列表\n */\nexport const insertRawLyric = (lyrics: LX.DBService.Lyricnfo[]) => {\n  const db = getDB()\n  const rawLyricInsertStatement = createRawLyricInsertStatement()\n  db.transaction((lyrics: LX.DBService.Lyricnfo[]) => {\n    for (const lyric of lyrics) rawLyricInsertStatement.run(lyric)\n  })(lyrics)\n}\n\n/**\n * 批量删除原始歌词\n * @param ids 列表\n */\nexport const deleteRawLyric = (ids: string[]) => {\n  const db = getDB()\n  const rawLyricDeleteStatement = createRawLyricDeleteStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) rawLyricDeleteStatement.run(id)\n  })(ids)\n}\n\n/**\n * 批量更新原始歌词\n * @param lyrics 列表\n */\nexport const updateRawLyric = (lyrics: LX.DBService.Lyricnfo[]) => {\n  const db = getDB()\n  const rawLyricUpdateStatement = createRawLyricUpdateStatement()\n  db.transaction((lyrics: LX.DBService.Lyricnfo[]) => {\n    for (const lyric of lyrics) rawLyricUpdateStatement.run(lyric)\n  })(lyrics)\n}\n\n/**\n * 清空原始歌词\n */\nexport const clearRawLyric = () => {\n  const rawLyricClearStatement = createRawLyricClearStatement()\n  rawLyricClearStatement.run()\n}\n\n/**\n * 统计已编辑歌词数量\n */\nexport const countRawLyric = () => {\n  const countStatement = createRawLyricCountStatement()\n  return (countStatement.get() as { count: number }).count\n}\n\n\n/**\n * 查询已编辑歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const queryEditedLyric = (id: string) => {\n  const rawLyricQueryStatement = createEditedLyricQueryStatement()\n  return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]\n}\n\n/**\n * 批量插入已编辑歌词\n * @param lyrics 列表\n */\nexport const insertEditedLyric = (lyrics: LX.DBService.Lyricnfo[]) => {\n  const db = getDB()\n  const rawLyricInsertStatement = createEditedLyricInsertStatement()\n  db.transaction((lyrics: LX.DBService.Lyricnfo[]) => {\n    for (const lyric of lyrics) rawLyricInsertStatement.run(lyric)\n  })(lyrics)\n}\n\n/**\n * 批量删除已编辑歌词\n * @param ids 列表\n */\nexport const deleteEditedLyric = (ids: string[]) => {\n  const db = getDB()\n  const rawLyricDeleteStatement = createEditedLyricDeleteStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) rawLyricDeleteStatement.run(id)\n  })(ids)\n}\n\n/**\n * 批量更新已编辑歌词\n * @param lyrics 列表\n */\nexport const updateEditedLyric = (lyrics: LX.DBService.Lyricnfo[]) => {\n  const db = getDB()\n  const rawLyricUpdateStatement = createEditedLyricUpdateStatement()\n  db.transaction((lyrics: LX.DBService.Lyricnfo[]) => {\n    for (const lyric of lyrics) rawLyricUpdateStatement.run(lyric)\n  })(lyrics)\n}\n\n/**\n * 清空已编辑歌词\n */\nexport const clearEditedLyric = () => {\n  const rawLyricClearStatement = createEditedLyricClearStatement()\n  rawLyricClearStatement.run()\n}\n\n\n/**\n * 统计已编辑歌词数量\n */\nexport const countEditedLyric = () => {\n  const countStatement = createEditedLyricCountStatement()\n  return (countStatement.get() as { count: number }).count\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/lyric/index.ts",
    "content": "import {\n  queryLyric,\n  queryRawLyric,\n  insertRawLyric,\n  deleteRawLyric,\n  updateRawLyric,\n  clearRawLyric,\n  queryEditedLyric,\n  insertEditedLyric,\n  deleteEditedLyric,\n  updateEditedLyric,\n  clearEditedLyric,\n  countEditedLyric,\n  countRawLyric,\n} from './dbHelper'\n\nconst keys = ['lyric', 'tlyric', 'rlyric', 'lxlyric'] as const\n\nconst toDBLyric = (id: string, source: LX.DBService.Lyricnfo['source'], lyricInfo: LX.Music.LyricInfo): LX.DBService.Lyricnfo[] => {\n  return (keys.map(k => [k, lyricInfo[k]])\n    .filter(([k, t]) => t != null) as Array<[LX.DBService.Lyricnfo['type'], string]>)\n    .map(([k, t]) => {\n      return {\n        id,\n        type: k,\n        text: Buffer.from(t).toString('base64'),\n        source,\n      }\n    })\n}\n\n/**\n * 获取歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const getPlayerLyric = (id: string): LX.Player.LyricInfo => {\n  const lyrics = queryLyric(id)\n\n  let lyricInfo: LX.Music.LyricInfo = {\n    lyric: '',\n  }\n  let rawLyricInfo: LX.Music.LyricInfo = {\n    lyric: '',\n  }\n  for (const lyric of lyrics) {\n    switch (lyric.source) {\n      case 'edited':\n        if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString()\n        else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString()\n        break\n      default:\n        if (lyric.type == 'lyric') rawLyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString()\n        else if (lyric.text != null) rawLyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString()\n        break\n    }\n  }\n\n  return lyricInfo.lyric ? {\n    ...lyricInfo,\n    rawlrcInfo: rawLyricInfo,\n  } : {\n    ...rawLyricInfo,\n    rawlrcInfo: rawLyricInfo,\n  }\n}\n\n/**\n * 获取原始歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const getRawLyric = (id: string): LX.Music.LyricInfo => {\n  const lyrics = queryRawLyric(id)\n\n  let lyricInfo: LX.Music.LyricInfo = {\n    lyric: '',\n  }\n  for (const lyric of lyrics) {\n    if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString()\n    else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString()\n  }\n\n  return lyricInfo\n}\n\n/**\n * 保存原始歌词信息\n * @param id 歌曲id\n * @param lyricInfo 歌词信息\n */\nexport const rawLyricAdd = (id: string, lyricInfo: LX.Music.LyricInfo) => {\n  insertRawLyric(toDBLyric(id, 'raw', lyricInfo))\n}\n\n/**\n * 删除原始歌词信息\n * @param ids 歌曲id\n */\nexport const rawLyricRemove = (ids: string[]) => {\n  deleteRawLyric(ids)\n}\n\n/**\n * 更新原始歌词信息\n * @param id 歌曲id\n * @param lyricInfo 歌词信息\n */\nexport const rawLyricUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => {\n  updateRawLyric(toDBLyric(id, 'raw', lyricInfo))\n}\n\n/**\n * 清空原始歌词信息\n */\nexport const rawLyricClear = () => {\n  clearRawLyric()\n}\n\n/**\n * 统计原始歌词数量\n */\nexport const rawLyricCount = () => {\n  return countRawLyric()\n}\n\n\n/**\n * 获取已编辑歌词\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const getEditedLyric = (id: string): LX.Music.LyricInfo => {\n  const lyrics = queryEditedLyric(id)\n\n  let lyricInfo: LX.Music.LyricInfo = {\n    lyric: '',\n  }\n  for (const lyric of lyrics) {\n    if (lyric.type == 'lyric') lyricInfo.lyric = Buffer.from(lyric.text, 'base64').toString()\n    else if (lyric.text != null) lyricInfo[lyric.type] = Buffer.from(lyric.text, 'base64').toString()\n  }\n\n  return lyricInfo\n}\n\n/**\n * 保存已编辑歌词信息\n * @param id 歌曲id\n * @param lyricInfo 歌词信息\n */\nexport const editedLyricAdd = (id: string, lyricInfo: LX.Music.LyricInfo) => {\n  insertEditedLyric(toDBLyric(id, 'edited', lyricInfo))\n}\n\n/**\n * 删除已编辑歌词信息\n * @param ids 歌曲id\n */\nexport const editedLyricRemove = (ids: string[]) => {\n  deleteEditedLyric(ids)\n}\n\n/**\n * 更新已编辑歌词信息\n * @param id 歌曲id\n * @param lyricInfo 歌词信息\n */\nexport const editedLyricUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => {\n  updateEditedLyric(toDBLyric(id, 'edited', lyricInfo))\n}\n\n/**\n * 清空已编辑歌词信息\n */\nexport const editedLyricClear = () => {\n  clearEditedLyric()\n}\n\n/**\n * 新增或更新已编辑歌词信息\n * @param id 歌曲id\n * @param lyricInfo 歌词信息\n */\nexport const editedLyricUpdateAddAndUpdate = (id: string, lyricInfo: LX.Music.LyricInfo) => {\n  const lyrics = queryEditedLyric(id)\n  if (lyrics.length) updateEditedLyric(toDBLyric(id, 'edited', lyricInfo))\n  else insertEditedLyric(toDBLyric(id, 'edited', lyricInfo))\n}\n\n/**\n * 统计已编辑歌词数量\n */\nexport const editedLyricCount = () => {\n  return countEditedLyric()\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/lyric/statements.ts",
    "content": "import { getDB } from '../../db'\n\nconst RAW_LYRIC = 'raw'\nconst EDITED_LYRIC = 'edited'\n\n/**\n * 创建歌词查询语句\n * @returns 查询语句\n */\nexport const createLyricQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    SELECT \"type\", \"text\", \"source\"\n    FROM \"main\".\"lyric\"\n    WHERE \"id\"=?\n    `)\n}\n\n/**\n * 创建原始歌词查询语句\n * @returns 查询语句\n */\nexport const createRawLyricQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    SELECT \"type\", \"text\"\n    FROM \"main\".\"lyric\"\n    WHERE \"id\"=? AND \"source\"='${RAW_LYRIC}'\n    `)\n}\n\n/**\n * 创建原始歌词插入语句\n * @returns 插入语句\n */\nexport const createRawLyricInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.Lyricnfo]>(`\n    INSERT INTO \"main\".\"lyric\" (\"id\", \"type\", \"text\", \"source\")\n    VALUES (@id, @type, @text, '${RAW_LYRIC}')`)\n}\n\n/**\n * 创建原始歌词清空语句\n * @returns 清空语句\n */\nexport const createRawLyricClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"lyric\"\n    WHERE \"source\"='${RAW_LYRIC}'\n  `)\n}\n\n/**\n * 创建原始歌词删除语句\n * @returns 删除语句\n */\nexport const createRawLyricDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    DELETE FROM \"main\".\"lyric\"\n    WHERE \"id\"=? AND \"source\"='${RAW_LYRIC}'\n  `)\n}\n\n/**\n * 创建原始歌词更新语句\n * @returns 更新语句\n */\nexport const createRawLyricUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.Lyricnfo]>(`\n    UPDATE \"main\".\"lyric\"\n    SET \"text\"=@text\n    WHERE \"id\"=@id AND \"source\"='${RAW_LYRIC}' AND \"type\"=@type`)\n}\n\n\n/**\n * 创建原始歌词数量统计语句\n * @returns 统计语句\n */\nexport const createRawLyricCountStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`SELECT COUNT(*) as count FROM \"main\".\"lyric\" WHERE \"source\"='${RAW_LYRIC}'`)\n}\n\n\n/**\n * 创建已编辑歌词查询语句\n * @returns 查询语句\n */\nexport const createEditedLyricQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    SELECT \"type\", \"text\"\n    FROM \"main\".\"lyric\"\n    WHERE \"id\"=? AND \"source\"='${EDITED_LYRIC}'\n    `)\n}\n\n/**\n * 创建已编辑歌词插入语句\n * @returns 插入语句\n */\nexport const createEditedLyricInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.Lyricnfo]>(`\n    INSERT INTO \"main\".\"lyric\" (\"id\", \"type\", \"text\", \"source\")\n    VALUES (@id, @type, @text, '${EDITED_LYRIC}')`)\n}\n\n/**\n * 创建已编辑歌词清空语句\n * @returns 清空语句\n */\nexport const createEditedLyricClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"lyric\"\n    WHERE \"source\"='${EDITED_LYRIC}'\n  `)\n}\n\n/**\n * 创建已编辑歌词删除语句\n * @returns 删除语句\n */\nexport const createEditedLyricDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    DELETE FROM \"main\".\"lyric\"\n    WHERE \"id\"=? AND \"source\"='${EDITED_LYRIC}'\n  `)\n}\n\n/**\n * 创建已编辑歌词更新语句\n * @returns 更新语句\n */\nexport const createEditedLyricUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.Lyricnfo]>(`\n    UPDATE \"main\".\"lyric\"\n    SET \"text\"=@text\n    WHERE \"id\"=@id AND \"source\"='${EDITED_LYRIC}' AND \"type\"=@type`)\n}\n\n/**\n * 创建已编辑歌词数量统计语句\n * @returns 统计语句\n */\nexport const createEditedLyricCountStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`SELECT COUNT(*) as count FROM \"main\".\"lyric\" WHERE \"source\"='${EDITED_LYRIC}'`)\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_other_source/dbHelper.ts",
    "content": "import { getDB } from '../../db'\nimport {\n  createMusicInfoQueryStatement,\n  createMusicInfoInsertStatement,\n  createMusicInfoDeleteStatement,\n  createMusicInfoClearStatement,\n  createCountStatement,\n} from './statements'\n\n\n/**\n * 查询歌曲信息\n * @param id 歌曲id\n * @returns 歌曲信息\n */\nexport const queryMusicInfo = (id: string) => {\n  const musicInfoQueryStatement = createMusicInfoQueryStatement()\n  return musicInfoQueryStatement.all(id) as LX.DBService.MusicInfoOtherSource[]\n}\n\n/**\n * 批量插入歌曲信息\n * @param musicInfos 列表\n */\nexport const insertMusicInfo = (musicInfos: LX.DBService.MusicInfoOtherSource[]) => {\n  const db = getDB()\n  const musicInfoInsertStatement = createMusicInfoInsertStatement()\n  db.transaction((musicInfos: LX.DBService.MusicInfoOtherSource[]) => {\n    for (const info of musicInfos) musicInfoInsertStatement.run(info)\n  })(musicInfos)\n}\n\n/**\n * 批量删除歌曲信息\n * @param ids 列表\n */\nexport const deleteMusicInfo = (ids: string[]) => {\n  const db = getDB()\n  const musicInfoDeleteStatement = createMusicInfoDeleteStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) musicInfoDeleteStatement.run(id)\n  })(ids)\n}\n\n/**\n * 清空歌曲信息\n */\nexport const clearMusicInfo = () => {\n  const musicInfoClearStatement = createMusicInfoClearStatement()\n  musicInfoClearStatement.run()\n}\n\n/**\n * 统计歌曲信息数量\n */\nexport const countMusicInfo = () => {\n  const countStatement = createCountStatement()\n  return (countStatement.get() as { count: number }).count\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_other_source/index.ts",
    "content": "import {\n  queryMusicInfo,\n  insertMusicInfo,\n  deleteMusicInfo,\n  clearMusicInfo,\n  countMusicInfo,\n} from './dbHelper'\n\n\nconst toDBMusicInfo = (id: string, musicInfos: LX.Music.MusicInfo[]): LX.DBService.MusicInfoOtherSource[] => {\n  return musicInfos.map((info, index) => {\n    return {\n      ...info,\n      meta: JSON.stringify(info.meta),\n      source_id: id,\n      order: index,\n    }\n  })\n}\n\n/**\n * 获取歌曲信息\n * @param id 歌曲id\n * @returns 歌词信息\n */\nexport const getMusicInfoOtherSource = (id: string): LX.Music.MusicInfoOnline[] => {\n  const list = queryMusicInfo(id).sort((a, b) => a.order - b.order).map(info => {\n    return {\n      id: info.id,\n      name: info.name,\n      singer: info.singer,\n      source: info.source,\n      interval: info.interval,\n      meta: JSON.parse(info.meta),\n    }\n  })\n\n  return list\n}\n\n/**\n * 保存歌曲信息信息\n * @param id 歌曲id\n * @param musicInfos 歌词信息\n */\nexport const musicInfoOtherSourceAdd = (id: string, musicInfos: LX.Music.MusicInfoOnline[]) => {\n  insertMusicInfo(toDBMusicInfo(id, musicInfos))\n}\n\n/**\n * 删除歌曲信息信息\n * @param ids 歌曲id\n */\nexport const musicInfoOtherSourceRemove = (ids: string[]) => {\n  deleteMusicInfo(ids)\n}\n\n/**\n * 清空歌曲信息信息\n */\nexport const musicInfoOtherSourceClear = () => {\n  clearMusicInfo()\n}\n\n\n/**\n * 统计歌曲信息信息数量\n */\nexport const musicInfoOtherSourceCount = () => {\n  return countMusicInfo()\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_other_source/statements.ts",
    "content": "import { getDB } from '../../db'\n\n\n/**\n * 创建歌曲信息查询语句\n * @returns 查询语句\n */\nexport const createMusicInfoQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    SELECT \"id\", \"name\", \"singer\", \"source\", \"meta\"\n    FROM \"main\".\"music_info_other_source\"\n    WHERE source_id=?\n    ORDER BY \"order\" ASC\n  `)\n}\n\n/**\n * 创建歌曲信息插入语句\n * @returns 插入语句\n */\nexport const createMusicInfoInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicInfoOtherSource]>(`\n    INSERT INTO \"main\".\"music_info_other_source\" (\"id\", \"name\", \"singer\", \"source\", \"meta\", \"source_id\", \"order\")\n    VALUES (@id, @name, @singer, @source, @meta, @source_id, @order)\n  `)\n}\n\n/**\n * 创建歌曲信息清空语句\n * @returns 清空语句\n */\nexport const createMusicInfoClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"music_info_other_source\"\n  `)\n}\n\n/**\n * 创建歌曲信息删除语句\n * @returns 删除语句\n */\nexport const createMusicInfoDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    DELETE FROM \"main\".\"music_info_other_source\"\n    WHERE \"source_id\"=?\n  `)\n}\n\n/**\n * 创建数量统计语句\n * @returns 统计语句\n */\nexport const createCountStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>('SELECT COUNT(*) as count FROM \"main\".\"music_info_other_source\"')\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_url/dbHelper.ts",
    "content": "import { getDB } from '../../db'\nimport {\n  createQueryStatement,\n  createInsertStatement,\n  createDeleteStatement,\n  // createUpdateStatement,\n  createClearStatement,\n  createCountStatement,\n} from './statements'\n\n/**\n * 查询歌曲url\n * @param id 歌曲id\n * @returns url\n */\nexport const queryMusicUrl = (id: string) => {\n  const queryStatement = createQueryStatement()\n  return (queryStatement.get(id) as { url: string } | null)?.url ?? null\n}\n\n/**\n * 批量插入歌曲url\n * @param urlInfo 列表\n */\nexport const insertMusicUrl = (urlInfo: LX.DBService.MusicUrlInfo[]) => {\n  const db = getDB()\n  const insertStatement = createInsertStatement()\n  const deleteStatement = createDeleteStatement()\n  db.transaction((urlInfo: LX.DBService.MusicUrlInfo[]) => {\n    for (const info of urlInfo) {\n      deleteStatement.run(info.id)\n      insertStatement.run(info)\n    }\n  })(urlInfo)\n}\n\n/**\n * 批量删除歌曲url\n * @param ids 列表\n */\nexport const deleteMusicUrl = (ids: string[]) => {\n  const db = getDB()\n  const deleteStatement = createDeleteStatement()\n  db.transaction((ids: string[]) => {\n    for (const id of ids) deleteStatement.run(id)\n  })(ids)\n}\n\n/**\n * 批量更新歌曲url\n * @param urlInfo 列表\n */\n// export const updateMusicUrl = (urlInfo: LX.DBService.MusicUrlInfo[]) => {\n//   const db = getDB()\n//   const updateStatement = createUpdateStatement()\n//   db.transaction((urlInfo: LX.DBService.MusicUrlInfo[]) => {\n//     for (const info of urlInfo) updateStatement.run(info)\n//   })(urlInfo)\n// }\n\n/**\n * 清空歌曲url\n */\nexport const clearMusicUrl = () => {\n  const clearStatement = createClearStatement()\n  clearStatement.run()\n}\n\n/**\n * 统计歌曲信息数量\n */\nexport const countMusicUrl = () => {\n  const countStatement = createCountStatement()\n  return (countStatement.get() as { count: number }).count\n}\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_url/index.ts",
    "content": "import {\n  queryMusicUrl,\n  insertMusicUrl,\n  deleteMusicUrl,\n  clearMusicUrl,\n  countMusicUrl,\n} from './dbHelper'\n\n\n/**\n * 获取歌曲url\n * @param id 歌曲id\n * @returns 歌曲url\n */\nexport const getMusicUrl = (id: string): string | null => {\n  const url = queryMusicUrl(id)\n  return url\n}\n\n/**\n * 保存歌曲url\n * @param urlInfos url信息\n */\nexport const musicUrlSave = (urlInfos: LX.Music.MusicUrlInfo[]) => {\n  insertMusicUrl(urlInfos)\n}\n\n/**\n * 删除歌曲url\n * @param ids 歌曲id\n */\nexport const musicUrlRemove = (ids: string[]) => {\n  deleteMusicUrl(ids)\n}\n\n/**\n * 清空歌曲url\n */\nexport const musicUrlClear = () => {\n  clearMusicUrl()\n}\n\n/**\n * 统计歌曲url数量\n */\nexport const musicUrlCount = () => {\n  return countMusicUrl()\n}\n\n"
  },
  {
    "path": "src/main/worker/dbService/modules/music_url/statements.ts",
    "content": "import { getDB } from '../../db'\n\n/**\n * 创建歌曲url查询语句\n * @returns 查询语句\n */\nexport const createQueryStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    SELECT \"url\"\n    FROM \"main\".\"music_url\"\n    WHERE \"id\"=?\n    `)\n}\n\n/**\n * 创建歌曲url插入语句\n * @returns 插入语句\n */\nexport const createInsertStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicUrlInfo]>(`\n    INSERT INTO \"main\".\"music_url\" (\"id\", \"url\")\n    VALUES (@id, @url)`)\n}\n\n/**\n * 创建歌曲url清空语句\n * @returns 清空语句\n */\nexport const createClearStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>(`\n    DELETE FROM \"main\".\"music_url\"\n  `)\n}\n\n/**\n * 创建歌曲url删除语句\n * @returns 删除语句\n */\nexport const createDeleteStatement = () => {\n  const db = getDB()\n  return db.prepare<[string]>(`\n    DELETE FROM \"main\".\"music_url\"\n    WHERE \"id\"=?\n  `)\n}\n\n/**\n * 创建歌曲url更新语句\n * @returns 更新语句\n */\nexport const createUpdateStatement = () => {\n  const db = getDB()\n  return db.prepare<[LX.DBService.MusicUrlInfo]>(`\n    UPDATE \"main\".\"music_url\"\n    SET \"url\"=@url\n    WHERE \"id\"=@id`)\n}\n\n/**\n * 创建数量统计语句\n * @returns 统计语句\n */\nexport const createCountStatement = () => {\n  const db = getDB()\n  return db.prepare<[]>('SELECT COUNT(*) as count FROM \"main\".\"music_url\"')\n}\n"
  },
  {
    "path": "src/main/worker/dbService/tables.ts",
    "content": "// export const sql = `\n//   CREATE TABLE \"db_info\" (\n//     \"id\" INTEGER NOT NULL UNIQUE,\n//     \"field_name\" TEXT,\n//     \"field_value\" TEXT,\n//     PRIMARY KEY(\"id\" AUTOINCREMENT)\n//   );\n\n//   CREATE TABLE \"my_list\" (\n//     \"id\" TEXT NOT NULL,\n//     \"name\" TEXT NOT NULL,\n//     \"source\" TEXT,\n//     \"sourceListId\" TEXT,\n//     \"position\" INTEGER NOT NULL,\n//     \"locationUpdateTime\" INTEGER,\n//     PRIMARY KEY(\"id\")\n//   );\n\n//   CREATE TABLE \"my_list_music_info\" (\n//     \"id\" TEXT NOT NULL,\n//     \"listId\" TEXT NOT NULL,\n//     \"name\" TEXT NOT NULL,\n//     \"singer\" TEXT NOT NULL,\n//     \"source\" TEXT NOT NULL,\n//     \"interval\" TEXT,\n//     \"meta\" TEXT NOT NULL,\n//     UNIQUE(\"id\",\"listId\")\n//   );\n//   CREATE INDEX \"index_my_list_music_info\" ON \"my_list_music_info\" (\n//     \"id\",\n//     \"listId\"\n//   );\n\n//   CREATE TABLE \"my_list_music_info_order\" (\n//     \"listId\" TEXT NOT NULL,\n//     \"musicInfoId\" TEXT NOT NULL,\n//     \"order\" INTEGER NOT NULL\n//   );\n//   CREATE INDEX \"index_my_list_music_info_order\" ON \"my_list_music_info_order\" (\n//     \"listId\",\n//     \"musicInfoId\"\n//   );\n\n//   CREATE TABLE \"music_info_other_source\" (\n//     \"source_id\" TEXT NOT NULL,\n//     \"id\" TEXT NOT NULL,\n//     \"source\" TEXT NOT NULL,\n//     \"name\" TEXT NOT NULL,\n//     \"singer\" TEXT NOT NULL,\n//     \"meta\" TEXT NOT NULL,\n//     \"order\" INTEGER NOT NULL,\n//     UNIQUE(\"source_id\",\"id\")\n//   );\n//   CREATE INDEX \"index_music_info_other_source\" ON \"music_info_other_source\" (\n//     \"source_id\",\n//     \"id\"\n//   );\n\n//   -- TODO  \"meta\" TEXT NOT NULL,\n//   CREATE TABLE \"lyric\" (\n//     \"id\" TEXT NOT NULL,\n//     \"source\" TEXT NOT NULL,\n//     \"type\" TEXT NOT NULL,\n//     \"text\" TEXT NOT NULL\n//   );\n\n//   CREATE TABLE \"music_url\" (\n//     \"id\" TEXT NOT NULL,\n//     \"url\" TEXT NOT NULL\n//   );\n\n//   CREATE TABLE \"download_list\" (\n//     \"id\" TEXT NOT NULL,\n//     \"isComplate\" INTEGER NOT NULL,\n//     \"status\" TEXT NOT NULL,\n//     \"statusText\" TEXT NOT NULL,\n//     \"progress_downloaded\" INTEGER NOT NULL,\n//     \"progress_total\" INTEGER NOT NULL,\n//     \"url\" TEXT,\n//     \"quality\" TEXT NOT NULL,\n//     \"ext\" TEXT NOT NULL,\n//     \"fileName\" TEXT NOT NULL,\n//     \"filePath\" TEXT NOT NULL,\n//     \"musicInfo\" TEXT NOT NULL,\n//     \"position\" INTEGER NOT NULL,\n//     PRIMARY KEY(\"id\")\n//   );\n// `\n\n// export const tables = [\n//   'table_db_info',\n//   'table_my_list',\n//   'table_my_list_music_info',\n//   'index_index_my_list_music_info',\n//   'table_my_list_music_info_order',\n//   'index_index_my_list_music_info_order',\n//   'table_music_info_other_source',\n//   'index_index_music_info_other_source',\n//   'table_lyric',\n//   'table_music_url',\n//   'table_download_list',\n// ]\n\ntype Tables = 'db_info'\n| 'my_list'\n| 'my_list_music_info'\n| 'index_my_list_music_info'\n| 'my_list_music_info_order'\n| 'index_my_list_music_info_order'\n| 'music_info_other_source'\n| 'index_music_info_other_source'\n| 'lyric'\n| 'music_url'\n| 'download_list'\n| 'dislike_list'\n\nconst tables = new Map<Tables, string>()\n\n\ntables.set('db_info', `\n  CREATE TABLE \"db_info\" (\n    \"id\" INTEGER NOT NULL UNIQUE,\n    \"field_name\" TEXT,\n    \"field_value\" TEXT,\n    PRIMARY KEY(\"id\" AUTOINCREMENT)\n  );\n`)\ntables.set('my_list', `\n  CREATE TABLE \"my_list\" (\n    \"id\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"source\" TEXT,\n    \"sourceListId\" TEXT,\n    \"position\" INTEGER NOT NULL,\n    \"locationUpdateTime\" INTEGER,\n    PRIMARY KEY(\"id\")\n  );\n`)\ntables.set('my_list_music_info', `\n  CREATE TABLE \"my_list_music_info\" (\n    \"id\" TEXT NOT NULL,\n    \"listId\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"singer\" TEXT NOT NULL,\n    \"source\" TEXT NOT NULL,\n    \"interval\" TEXT,\n    \"meta\" TEXT NOT NULL,\n    UNIQUE(\"id\",\"listId\")\n  );\n`)\ntables.set('index_my_list_music_info', `\n  CREATE INDEX \"index_my_list_music_info\" ON \"my_list_music_info\" (\n    \"id\",\n    \"listId\"\n  );\n`)\ntables.set('my_list_music_info_order', `\n  CREATE TABLE \"my_list_music_info_order\" (\n    \"listId\" TEXT NOT NULL,\n    \"musicInfoId\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL\n  );\n`)\ntables.set('index_my_list_music_info_order', `\n  CREATE INDEX \"index_my_list_music_info_order\" ON \"my_list_music_info_order\" (\n    \"listId\",\n    \"musicInfoId\"\n  );\n`)\ntables.set('music_info_other_source', `\n  CREATE TABLE \"music_info_other_source\" (\n    \"source_id\" TEXT NOT NULL,\n    \"id\" TEXT NOT NULL,\n    \"source\" TEXT NOT NULL,\n    \"name\" TEXT NOT NULL,\n    \"singer\" TEXT NOT NULL,\n    \"meta\" TEXT NOT NULL,\n    \"order\" INTEGER NOT NULL,\n    UNIQUE(\"source_id\",\"id\")\n  );\n`)\ntables.set('index_music_info_other_source', `\n  CREATE INDEX \"index_music_info_other_source\" ON \"music_info_other_source\" (\n    \"source_id\",\n    \"id\"\n  );\n`)\ntables.set('lyric', `\n  -- TODO  \"meta\" TEXT NOT NULL,\n  CREATE TABLE \"lyric\" (\n    \"id\" TEXT NOT NULL,\n    \"source\" TEXT NOT NULL,\n    \"type\" TEXT NOT NULL,\n    \"text\" TEXT NOT NULL\n  );\n`)\ntables.set('music_url', `\n  CREATE TABLE \"music_url\" (\n    \"id\" TEXT NOT NULL,\n    \"url\" TEXT NOT NULL\n  );\n`)\ntables.set('download_list', `\n  CREATE TABLE \"download_list\" (\n    \"id\" TEXT NOT NULL,\n    \"isComplate\" INTEGER NOT NULL,\n    \"status\" TEXT NOT NULL,\n    \"statusText\" TEXT NOT NULL,\n    \"progress_downloaded\" INTEGER NOT NULL,\n    \"progress_total\" INTEGER NOT NULL,\n    \"url\" TEXT,\n    \"quality\" TEXT NOT NULL,\n    \"ext\" TEXT NOT NULL,\n    \"fileName\" TEXT NOT NULL,\n    \"filePath\" TEXT NOT NULL,\n    \"musicInfo\" TEXT NOT NULL,\n    \"position\" INTEGER NOT NULL,\n    PRIMARY KEY(\"id\")\n  );\n`)\ntables.set('dislike_list', `\n  CREATE TABLE \"dislike_list\" (\n    \"type\" TEXT NOT NULL,\n    \"content\" TEXT NOT NULL,\n    \"meta\" TEXT\n  );\n`)\n\nexport default tables\n\nexport const DB_VERSION = '2'\n"
  },
  {
    "path": "src/main/worker/dbService/verifyDB.ts",
    "content": "import type Database from 'better-sqlite3'\nimport tables from './tables'\n\nconst rxp = /\\n|\\s|;|--.+/g\nexport default (db: Database.Database) => {\n  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 }>\n  const dbTableMap = new Map<string, string>()\n  for (const info of result) dbTableMap.set(info.name, info.sql.replace(rxp, ''))\n  return Array.from(tables.entries()).every(([name, sql]) => {\n    const dbSql = dbTableMap.get(name)\n    // if (!(dbSql && dbSql == sql.replace(rxp, ''))) {\n    //   console.log('dbSql：', dbSql, '\\nsql：', sql.replace(rxp, ''))\n    // }\n    // return true\n    return dbSql && dbSql == sql.replace(rxp, '')\n  })\n  // console.log(dbTableMap)\n  // for (const [name, sql] of tables.entries()) {\n  //   const dbSql = dbTableMap.get(name)\n  //   if (dbSql) {\n  //     if (dbSql == sql.replace(rxp, '')) continue\n  //     console.log(dbSql)\n  //     console.log(sql.replace(rxp, ''))\n  //   } else {\n  //     console.log(name)\n  //   }\n  // }\n  // if (result.every((info) => { tables.includes() }))\n}\n"
  },
  {
    "path": "src/main/worker/index.ts",
    "content": "import { createDBServiceWorker } from './utils'\n\n\nexport default () => {\n  return {\n    dbService: createDBServiceWorker(),\n  }\n}\n\n"
  },
  {
    "path": "src/main/worker/utils/index.ts",
    "content": "import { Worker } from 'node:worker_threads'\nimport * as Comlink from 'comlink'\nimport nodeEndpoint from 'comlink/dist/esm/node-adapter'\n\nexport type DBSeriveTypes = Comlink.Remote<LX.WorkerDBSeriveListTypes>\n\nexport const createDBServiceWorker = () => {\n  const worker: Worker = new Worker(new URL(\n    /* webpackChunkName: 'dbService.worker' */\n    '../dbService',\n    import.meta.url,\n  ))\n  return Comlink.wrap<LX.WorkerDBSeriveListTypes>(nodeEndpoint(worker))\n}\n\n"
  },
  {
    "path": "src/main/worker/utils/worker.ts",
    "content": "import worker from 'node:worker_threads'\nimport * as Comlink from 'comlink'\nimport nodeEndpoint from 'comlink/dist/esm/node-adapter'\n\n\nexport const exposeWorker = (obj: any) => {\n  if (worker.parentPort == null) return\n  Comlink.expose(obj, nodeEndpoint(worker.parentPort))\n}\n"
  },
  {
    "path": "src/renderer/.eslintrc.cjs",
    "content": "/* eslint-env node */\nconst { base, html, typescript, vue } = require('../../.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    html,\n    vue,\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n  ignorePatterns: [\n    'vendors',\n  ],\n}\n"
  },
  {
    "path": "src/renderer/App.vue",
    "content": "<template>\n  <div id=\"container\" class=\"view-container\">\n    <layout-aside id=\"left\" />\n    <div id=\"right\">\n      <layout-toolbar id=\"toolbar\" />\n      <layout-view id=\"view\" />\n      <layout-play-bar id=\"player\" />\n    </div>\n    <layout-icons />\n    <layout-change-log-modal />\n    <layout-update-modal />\n    <layout-pact-modal />\n    <layout-sync-mode-modal />\n    <layout-sync-auth-code-modal />\n    <layout-play-detail />\n  </div>\n</template>\n\n<script setup>\nimport { onMounted } from '@common/utils/vueTools'\n// import BubbleCursor from '@common/utils/effects/cursor-effects/bubbleCursor'\n// import '@common/utils/effects/snow.min'\nimport useApp from '@renderer/core/useApp'\n\nuseApp()\n\nonMounted(() => {\n  document.getElementById('root').style.display = 'block'\n\n  // const styles = getComputedStyle(document.documentElement)\n  // window.lxData.bubbleCursor = new BubbleCursor({\n  //   fillStyle: styles.getPropertyValue('--color-primary-alpha-900'),\n  //   strokeStyle: styles.getPropertyValue('--color-primary-alpha-700'),\n  // })\n})\n\n// onBeforeUnmount(() => {\n//   window.lxData.bubbleCursor?.destroy()\n// })\n\n</script>\n\n\n<style lang=\"less\">\n@import './assets/styles/index.less';\n@import './assets/styles/layout.less';\n\nhtml {\n  height: 100vh;\n}\nhtml, body {\n  // overflow: hidden;\n  box-sizing: border-box;\n}\n\nbody {\n  user-select: none;\n  height: 100%;\n}\n#root {\n  height: 100%;\n  position: relative;\n  overflow: hidden;\n  color: var(--color-font);\n  background: var(--background-image) var(--background-image-position) no-repeat;\n  background-size: var(--background-image-size);\n  transition: background-color @transition-normal;\n  background-color: var(--color-content-background);\n  box-sizing: border-box;\n}\n\n.disableAnimation * {\n  transition: none !important;\n  animation: none !important;\n}\n\n.transparent {\n  background: transparent;\n  padding: @shadow-app;\n  // #waiting-mask {\n  //   border-radius: @radius-border;\n  //   left: @shadow-app;\n  //   right: @shadow-app;\n  //   top: @shadow-app;\n  //   bottom: @shadow-app;\n  // }\n  #body {\n    border-radius: @radius-border;\n  }\n  #root {\n    box-shadow: 0 0 @shadow-app rgba(0, 0, 0, 0.5);\n    border-radius: @radius-border;\n  }\n  // #container {\n    // border-radius: @radius-border;\n    // background-color: transparent;\n  // }\n}\n.disableTransparent {\n  background-color: var(--color-content-background);\n\n  #body {\n    border: 1Px solid var(--color-primary-light-500);\n  }\n\n  #right {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n  }\n\n  // #view { // 偏移5px距离解决非透明模式下右侧滚动条无法拖动的问题\n  //   margin-right: 5Px;\n  // }\n}\n.fullscreen {\n  background-color: var(--color-content-background);\n\n  #right {\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n  }\n}\n\n#container {\n  position: relative;\n  display: flex;\n  height: 100%;\n  background-color: var(--color-app-background);\n}\n\n#left {\n  flex: none;\n  width: @width-app-left;\n}\n#right {\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  transition: background-color @transition-normal;\n  background-color: var(--color-main-background);\n\n  border-top-left-radius: @radius-border;\n  border-bottom-left-radius: @radius-border;\n  overflow: hidden;\n  box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);\n}\n#toolbar, #player {\n  flex: none;\n}\n#view {\n  position: relative;\n  flex: auto;\n  // display: flex;\n  min-height: 0;\n}\n\n.view-container {\n  transition: opacity @transition-normal;\n}\n#root.show-modal > .view-container {\n  opacity: .9;\n}\n#view.show-modal > .view-container {\n  opacity: .2;\n}\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/assets/styles/animate.less",
    "content": "// https://daneden.github.io/animate.css/\n\n@keyframes bounce {\n  from,\n  20%,\n  53%,\n  80%,\n  to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  40%,\n  43% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -30px, 0);\n    transform: translate3d(0, -30px, 0);\n  }\n\n  70% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -15px, 0);\n    transform: translate3d(0, -15px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -4px, 0);\n    transform: translate3d(0, -4px, 0);\n  }\n}\n@keyframes flash {\n  from,\n  50%,\n  to {\n    opacity: 1;\n  }\n\n  25%,\n  75% {\n    opacity: 0;\n  }\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.05, 1.05, 1.05);\n    transform: scale3d(1.05, 1.05, 1.05);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes rubberBand {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  30% {\n    -webkit-transform: scale3d(1.25, 0.75, 1);\n    transform: scale3d(1.25, 0.75, 1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.75, 1.25, 1);\n    transform: scale3d(0.75, 1.25, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.15, 0.85, 1);\n    transform: scale3d(1.15, 0.85, 1);\n  }\n\n  65% {\n    -webkit-transform: scale3d(0.95, 1.05, 1);\n    transform: scale3d(0.95, 1.05, 1);\n  }\n\n  75% {\n    -webkit-transform: scale3d(1.05, 0.95, 1);\n    transform: scale3d(1.05, 0.95, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes shake {\n  from,\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  10%,\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  20%,\n  40%,\n  60%,\n  80% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n}\n@keyframes headShake {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n\n  6.5% {\n    -webkit-transform: translateX(-6px) rotateY(-9deg);\n    transform: translateX(-6px) rotateY(-9deg);\n  }\n\n  18.5% {\n    -webkit-transform: translateX(5px) rotateY(7deg);\n    transform: translateX(5px) rotateY(7deg);\n  }\n\n  31.5% {\n    -webkit-transform: translateX(-3px) rotateY(-5deg);\n    transform: translateX(-3px) rotateY(-5deg);\n  }\n\n  43.5% {\n    -webkit-transform: translateX(2px) rotateY(3deg);\n    transform: translateX(2px) rotateY(3deg);\n  }\n\n  50% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes swing {\n  20% {\n    -webkit-transform: rotate3d(0, 0, 1, 15deg);\n    transform: rotate3d(0, 0, 1, 15deg);\n  }\n\n  40% {\n    -webkit-transform: rotate3d(0, 0, 1, -10deg);\n    transform: rotate3d(0, 0, 1, -10deg);\n  }\n\n  60% {\n    -webkit-transform: rotate3d(0, 0, 1, 5deg);\n    transform: rotate3d(0, 0, 1, 5deg);\n  }\n\n  80% {\n    -webkit-transform: rotate3d(0, 0, 1, -5deg);\n    transform: rotate3d(0, 0, 1, -5deg);\n  }\n\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg);\n    transform: rotate3d(0, 0, 1, 0deg);\n  }\n}\n@keyframes tada {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  10%,\n  20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n  }\n\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n  }\n\n  40%,\n  60%,\n  80% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@keyframes wobble {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  15% {\n    -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n    transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n  }\n\n  30% {\n    -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n    transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n  }\n\n  45% {\n    -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n    transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n  }\n\n  60% {\n    -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n    transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n    transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes jello {\n  from,\n  11.1%,\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  22.2% {\n    -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);\n    transform: skewX(-12.5deg) skewY(-12.5deg);\n  }\n\n  33.3% {\n    -webkit-transform: skewX(6.25deg) skewY(6.25deg);\n    transform: skewX(6.25deg) skewY(6.25deg);\n  }\n\n  44.4% {\n    -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);\n    transform: skewX(-3.125deg) skewY(-3.125deg);\n  }\n\n  55.5% {\n    -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);\n    transform: skewX(1.5625deg) skewY(1.5625deg);\n  }\n\n  66.6% {\n    -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);\n    transform: skewX(-0.78125deg) skewY(-0.78125deg);\n  }\n\n  77.7% {\n    -webkit-transform: skewX(0.390625deg) skewY(0.390625deg);\n    transform: skewX(0.390625deg) skewY(0.390625deg);\n  }\n\n  88.8% {\n    -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg);\n    transform: skewX(-0.1953125deg) skewY(-0.1953125deg);\n  }\n}\n@keyframes heartBeat {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n\n  14% {\n    -webkit-transform: scale(1.3);\n    transform: scale(1.3);\n  }\n\n  28% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n\n  42% {\n    -webkit-transform: scale(1.3);\n    transform: scale(1.3);\n  }\n\n  70% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n}\n\n@keyframes flipInX {\n  from {\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n\n  to {\n    transform: perspective(400px);\n  }\n}\n@keyframes flipOutX {\n  from {\n    transform: perspective(400px);\n  }\n\n  30% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    opacity: 1;\n  }\n\n  to {\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes flipInY {\n  from {\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n  }\n\n  to {\n    transform: perspective(400px);\n  }\n}\n@keyframes flipOutY {\n  from {\n    transform: perspective(400px);\n  }\n\n  30% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    opacity: 1;\n  }\n\n  to {\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n@keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n@keyframes bounceIn {\n  from,\n  20%,\n  40%,\n  60%,\n  80%,\n  to {\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  20% {\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  40% {\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(1.03, 1.03, 1.03);\n  }\n\n  80% {\n    transform: scale3d(0.97, 0.97, 0.97);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes bounceOut {\n  20% {\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  50%,\n  55% {\n    opacity: 1;\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n}\n@keyframes lightSpeedIn {\n  from {\n    transform: translate3d(100%, 0, 0) skewX(-30deg);\n    opacity: 0;\n  }\n\n  60% {\n    transform: skewX(20deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: skewX(-5deg);\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes lightSpeedOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    transform: translate3d(100%, 0, 0) skewX(30deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateIn {\n  from {\n    transform-origin: center;\n    transform: rotate3d(0, 0, 1, -200deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: center;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownLeft {\n  from {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownRight {\n  from {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpLeft {\n  from {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpRight {\n  from {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, -90deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateOut {\n  from {\n    transform-origin: center;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: center;\n    transform: rotate3d(0, 0, 1, 200deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownLeft {\n  from {\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownRight {\n  from {\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpLeft {\n  from {\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpRight {\n  from {\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes rollIn {\n  from {\n    opacity: 0;\n    transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n  }\n\n  to {\n    opacity: 1;\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes rollOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n  }\n}\n@keyframes zoomIn {\n  from {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  50% {\n    opacity: 1;\n  }\n}\n@keyframes zoomInDown {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInLeft {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInRight {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInUp {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomOut {\n  from {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n@keyframes zoomOutDown {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    transform-origin: center bottom;\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomOutLeft {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale(0.1) translate3d(-2000px, 0, 0);\n    transform-origin: left center;\n  }\n}\n@keyframes zoomOutRight {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale(0.1) translate3d(2000px, 0, 0);\n    transform-origin: right center;\n  }\n}\n@keyframes zoomOutUp {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    transform-origin: center bottom;\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes slideInDown {\n  from {\n    transform: translate3d(0, -100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInLeft {\n  from {\n    transform: translate3d(-100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInRight {\n  from {\n    transform: translate3d(100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInUp {\n  from {\n    transform: translate3d(0, 100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideOutDown {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(0, 100%, 0);\n  }\n}\n@keyframes slideOutLeft {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n@keyframes slideOutRight {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(100%, 0, 0);\n  }\n}\n@keyframes slideOutUp {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(0, -100%, 0);\n  }\n}\n@keyframes jackInTheBox {\n  from {\n    opacity: 0;\n    transform: scale(0.1) rotate(30deg);\n    transform-origin: center bottom;\n  }\n\n  50% {\n    transform: rotate(-10deg);\n  }\n\n  70% {\n    transform: rotate(3deg);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n@keyframes hinge {\n  0% {\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  20%,\n  60% {\n    -webkit-transform: rotate3d(0, 0, 1, 80deg);\n    transform: rotate3d(0, 0, 1, 80deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n  }\n\n  40%,\n  80% {\n    -webkit-transform: rotate3d(0, 0, 1, 60deg);\n    transform: rotate3d(0, 0, 1, 60deg);\n    -webkit-transform-origin: top left;\n    transform-origin: top left;\n    -webkit-animation-timing-function: ease-in-out;\n    animation-timing-function: ease-in-out;\n    opacity: 1;\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 700px, 0);\n    transform: translate3d(0, 700px, 0);\n    opacity: 0;\n  }\n}\n\n.flipInX {\n  backface-visibility: visible !important;\n  animation-name: flipInX;\n}\n.flipInY {\n  backface-visibility: visible !important;\n  animation-name: flipInY;\n}\n.fadeIn {\n  animation-name: fadeIn;\n}\n.bounceIn {\n  animation-duration: 0.75s;\n  animation-name: bounceIn;\n}\n.lightSpeedIn {\n  animation-name: lightSpeedIn;\n  animation-timing-function: ease-out;\n}\n.rotateIn {\n  animation-name: rotateIn;\n}\n.rotateInDownLeft {\n  animation-name: rotateInDownLeft;\n}\n.rotateInDownRight {\n  animation-name: rotateInDownRight;\n}\n.rotateInUpLeft {\n  animation-name: rotateInUpLeft;\n}\n.rotateInUpRight {\n  animation-name: rotateInUpRight;\n}\n.rollIn {\n  animation-name: rollIn;\n}\n.zoomIn {\n  animation-name: zoomIn;\n}\n.zoomInDown {\n  animation-name: zoomInDown;\n}\n.zoomInLeft {\n  animation-name: zoomInLeft;\n}\n.zoomInRight {\n  animation-name: zoomInRight;\n}\n.zoomInUp {\n  animation-name: zoomInUp;\n}\n.slideInDown {\n  animation-name: slideInDown;\n}\n.slideInLeft {\n  animation-name: slideInLeft;\n}\n.slideInRight {\n  animation-name: slideInRight;\n}\n.slideInUp {\n  animation-name: slideInUp;\n}\n.jackInTheBox {\n  -webkit-animation-name: jackInTheBox;\n  animation-name: jackInTheBox;\n}\n\n\n\n.flipOutX {\n  animation-duration: 0.75s;\n  animation-name: flipOutX;\n  backface-visibility: visible !important;\n}\n.flipOutY {\n  animation-duration: 0.75s;\n  backface-visibility: visible !important;\n  animation-name: flipOutY;\n}\n.fadeOut {\n  animation-name: fadeOut;\n}\n.bounceOut {\n  animation-duration: 0.75s;\n  animation-name: bounceOut;\n}\n.lightSpeedOut {\n  animation-name: lightSpeedOut;\n  animation-timing-function: ease-in;\n}\n.rotateOut {\n  animation-name: rotateOut;\n}\n.rotateOutDownLeft {\n  animation-name: rotateOutDownLeft;\n}\n.rotateOutDownRight {\n  animation-name: rotateOutDownRight;\n}\n.rotateOutUpLeft {\n  animation-name: rotateOutUpLeft;\n}\n.rotateOutUpRight {\n  animation-name: rotateOutUpRight;\n}\n.hinge {\n  animation-duration: 2s;\n  animation-name: hinge;\n}\n.rollOut {\n  animation-name: rollOut;\n}\n.zoomOut {\n  animation-name: zoomOut;\n}\n.zoomOutDown {\n  animation-name: zoomOutDown;\n}\n.zoomOutLeft {\n  animation-name: zoomOutLeft;\n}\n.zoomOutRight {\n  animation-name: zoomOutRight;\n}\n.zoomOutUp {\n  animation-name: zoomOutUp;\n}\n.slideOutDown {\n  animation-name: slideOutDown;\n}\n.slideOutLeft {\n  animation-name: slideOutLeft;\n}\n.slideOutRight {\n  animation-name: slideOutRight;\n}\n.slideOutUp {\n  animation-name: slideOutUp;\n}\n\n\n.bounce {\n  animation-name: bounce;\n  transform-origin: center bottom;\n}\n.flash {\n  animation-name: flash;\n}\n.pulse {\n  animation-name: pulse;\n}\n.rubberBand {\n  animation-name: rubberBand;\n}\n.shake {\n  animation-name: shake;\n}\n.headShake {\n  animation-timing-function: ease-in-out;\n  animation-name: headShake;\n}\n.swing {\n  transform-origin: top center;\n  animation-name: swing;\n}\n.tada {\n  animation-name: tada;\n}\n.wobble {\n  animation-name: wobble;\n}\n.jello {\n  animation-name: jello;\n  transform-origin: center;\n}\n.heartBeat {\n  animation-name: heartBeat;\n  animation-duration: 1.3s;\n  animation-timing-function: ease-in-out;\n}\n\n.animated {\n  animation-duration: 0.5s;\n  animation-fill-mode: both;\n}\n\n.animated-slow {\n  animation-duration: 0.8s;\n  animation-fill-mode: both;\n}\n\n.animated-fast {\n  animation-duration: 0.3s;\n  animation-fill-mode: both;\n}\n"
  },
  {
    "path": "src/renderer/assets/styles/colors.less",
    "content": "@red-50: #ffebee;\n@red-100: #ffcdd2;\n@red-200: #ef9a9a;\n@red-300: #e57373;\n@red-400: #ef5350;\n@red-500: #f44336;\n@red-600: #e53935;\n@red-700: #d32f2f;\n@red-800: #c62828;\n@red-900: #b71c1c;\n@red-A100: #ff8a80;\n@red-A200: #ff5252;\n@red-A400: #ff1744;\n@red-A700: #d50000;\n@red: @red-500;\n\n\n@pink-50: #fce4ec;\n@pink-100: #f8bbd0;\n@pink-200: #f48fb1;\n@pink-300: #f06292;\n@pink-400: #ec407a;\n@pink-500: #e91e63;\n@pink-600: #d81b60;\n@pink-700: #c2185b;\n@pink-800: #ad1457;\n@pink-900: #880e4f;\n@pink-A100: #ff80ab;\n@pink-A200: #ff4081;\n@pink-A400: #f50057;\n@pink-A700: #c51162;\n@pink: @pink-500;\n\n\n@purple-50: #f3e5f5;\n@purple-100: #e1bee7;\n@purple-200: #ce93d8;\n@purple-300: #ba68c8;\n@purple-400: #ab47bc;\n@purple-500: #9c27b0;\n@purple-600: #8e24aa;\n@purple-700: #7b1fa2;\n@purple-800: #6a1b9a;\n@purple-900: #4a148c;\n@purple-A100: #ea80fc;\n@purple-A200: #e040fb;\n@purple-A400: #d500f9;\n@purple-A700: #aa00ff;\n@purple: @purple-500;\n\n\n@deep-purple-50: #ede7f6;\n@deep-purple-100: #d1c4e9;\n@deep-purple-200: #b39ddb;\n@deep-purple-300: #9575cd;\n@deep-purple-400: #7e57c2;\n@deep-purple-500: #673ab7;\n@deep-purple-600: #5e35b1;\n@deep-purple-700: #512da8;\n@deep-purple-800: #4527a0;\n@deep-purple-900: #311b92;\n@deep-purple-A100: #b388ff;\n@deep-purple-A200: #7c4dff;\n@deep-purple-A400: #651fff;\n@deep-purple-A700: #6200ea;\n@deep-purple: @deep-purple-500;\n\n\n@indigo-50: #e8eaf6;\n@indigo-100: #c5cae9;\n@indigo-200: #9fa8da;\n@indigo-300: #7986cb;\n@indigo-400: #5c6bc0;\n@indigo-500: #3f51b5;\n@indigo-600: #3949ab;\n@indigo-700: #303f9f;\n@indigo-800: #283593;\n@indigo-900: #1a237e;\n@indigo-A100: #8c9eff;\n@indigo-A200: #536dfe;\n@indigo-A400: #3d5afe;\n@indigo-A700: #304ffe;\n@indigo: @indigo-500;\n\n\n@blue-50: #e3f2fd;\n@blue-100: #bbdefb;\n@blue-200: #90caf9;\n@blue-300: #64b5f6;\n@blue-400: #42a5f5;\n@blue-500: #2196f3;\n@blue-600: #1e88e5;\n@blue-700: #1976d2;\n@blue-800: #1565c0;\n@blue-900: #0d47a1;\n@blue-A100: #82b1ff;\n@blue-A200: #448aff;\n@blue-A400: #2979ff;\n@blue-A700: #2962ff;\n@blue: @blue-500;\n\n\n@light-blue-50: #e1f5fe;\n@light-blue-100: #b3e5fc;\n@light-blue-200: #81d4fa;\n@light-blue-300: #4fc3f7;\n@light-blue-400: #29b6f6;\n@light-blue-500: #03a9f4;\n@light-blue-600: #039be5;\n@light-blue-700: #0288d1;\n@light-blue-800: #0277bd;\n@light-blue-900: #01579b;\n@light-blue-A100: #80d8ff;\n@light-blue-A200: #40c4ff;\n@light-blue-A400: #00b0ff;\n@light-blue-A700: #0091ea;\n@light-blue: @light-blue-500;\n\n\n@cyan-50: #e0f7fa;\n@cyan-100: #b2ebf2;\n@cyan-200: #80deea;\n@cyan-300: #4dd0e1;\n@cyan-400: #26c6da;\n@cyan-500: #00bcd4;\n@cyan-600: #00acc1;\n@cyan-700: #0097a7;\n@cyan-800: #00838f;\n@cyan-900: #006064;\n@cyan-A100: #84ffff;\n@cyan-A200: #18ffff;\n@cyan-A400: #00e5ff;\n@cyan-A700: #00b8d4;\n@cyan: @cyan-500;\n\n\n@teal-50: #e0f2f1;\n@teal-100: #b2dfdb;\n@teal-200: #80cbc4;\n@teal-300: #4db6ac;\n@teal-400: #26a69a;\n@teal-500: #009688;\n@teal-600: #00897b;\n@teal-700: #00796b;\n@teal-800: #00695c;\n@teal-900: #004d40;\n@teal-A100: #a7ffeb;\n@teal-A200: #64ffda;\n@teal-A400: #1de9b6;\n@teal-A700: #00bfa5;\n@teal: @teal-500;\n\n\n@green-50: #e8f5e9;\n@green-100: #c8e6c9;\n@green-200: #a5d6a7;\n@green-300: #81c784;\n@green-400: #66bb6a;\n@green-500: #4caf50;\n@green-600: #43a047;\n@green-700: #388e3c;\n@green-800: #2e7d32;\n@green-900: #1b5e20;\n@green-A100: #b9f6ca;\n@green-A200: #69f0ae;\n@green-A400: #00e676;\n@green-A700: #00c853;\n@green: @green-500;\n\n\n@light-green-50: #f1f8e9;\n@light-green-100: #dcedc8;\n@light-green-200: #c5e1a5;\n@light-green-300: #aed581;\n@light-green-400: #9ccc65;\n@light-green-500: #8bc34a;\n@light-green-600: #7cb342;\n@light-green-700: #689f38;\n@light-green-800: #558b2f;\n@light-green-900: #33691e;\n@light-green-A100: #ccff90;\n@light-green-A200: #b2ff59;\n@light-green-A400: #76ff03;\n@light-green-A700: #64dd17;\n@light-green: @light-green-500;\n\n\n@lime-50: #f9fbe7;\n@lime-100: #f0f4c3;\n@lime-200: #e6ee9c;\n@lime-300: #dce775;\n@lime-400: #d4e157;\n@lime-500: #cddc39;\n@lime-600: #c0ca33;\n@lime-700: #afb42b;\n@lime-800: #9e9d24;\n@lime-900: #827717;\n@lime-A100: #f4ff81;\n@lime-A200: #eeff41;\n@lime-A400: #c6ff00;\n@lime-A700: #aeea00;\n@lime: @lime-500;\n\n\n@yellow-50: #fffde7;\n@yellow-100: #fff9c4;\n@yellow-200: #fff59d;\n@yellow-300: #fff176;\n@yellow-400: #ffee58;\n@yellow-500: #fec60a;\n@yellow-600: #fdd835;\n@yellow-700: #fbc02d;\n@yellow-800: #f9a825;\n@yellow-900: #f57f17;\n@yellow-A100: #ffff8d;\n@yellow-A200: #ffff00;\n@yellow-A400: #ffea00;\n@yellow-A700: #ffd600;\n@yellow: @yellow-700;\n\n\n@amber-50: #fff8e1;\n@amber-100: #ffecb3;\n@amber-200: #ffe082;\n@amber-300: #ffd54f;\n@amber-400: #ffca28;\n@amber-500: #ffc107;\n@amber-600: #ffb300;\n@amber-700: #ffa000;\n@amber-800: #ff8f00;\n@amber-900: #ff6f00;\n@amber-A100: #ffe57f;\n@amber-A200: #ffd740;\n@amber-A400: #ffc400;\n@amber-A700: #ffab00;\n@amber: @amber-500;\n\n\n@orange-50: #fff3e0;\n@orange-100: #ffe0b2;\n@orange-200: #ffcc80;\n@orange-300: #ffb74d;\n@orange-400: #ffa726;\n@orange-500: #ff9800;\n@orange-600: #fb8c00;\n@orange-700: #f57c00;\n@orange-800: #ef6c00;\n@orange-900: #e65100;\n@orange-A100: #ffd180;\n@orange-A200: #ffab40;\n@orange-A400: #ff9100;\n@orange-A700: #ff6d00;\n@orange: @orange-500;\n\n\n@deep-orange-50: #fbe9e7;\n@deep-orange-100: #ffccbc;\n@deep-orange-200: #ffab91;\n@deep-orange-300: #ff8a65;\n@deep-orange-400: #ff7043;\n@deep-orange-500: #ff5722;\n@deep-orange-600: #f4511e;\n@deep-orange-700: #e64a19;\n@deep-orange-800: #d84315;\n@deep-orange-900: #bf360c;\n@deep-orange-A100: #ff9e80;\n@deep-orange-A200: #ff6e40;\n@deep-orange-A400: #ff3d00;\n@deep-orange-A700: #dd2c00;\n@deep-orange: @deep-orange-500;\n\n\n@brown-50: #efebe9;\n@brown-100: #d7ccc8;\n@brown-200: #bcaaa4;\n@brown-300: #a1887f;\n@brown-400: #8d6e63;\n@brown-500: #795548;\n@brown-600: #6d4c41;\n@brown-700: #5d4037;\n@brown-800: #4e342e;\n@brown-900: #3e2723;\n@brown-A100: #d7ccc8;\n@brown-A200: #bcaaa4;\n@brown-A400: #8d6e63;\n@brown-A700: #5d4037;\n@brown: @brown-500;\n\n\n@grey-50: #fafafa;\n@grey-100: #f5f5f5;\n@grey-200: #eeeeee;\n@grey-300: #e0e0e0;\n@grey-400: #bdbdbd;\n@grey-500: #9e9e9e;  @rgb-grey-500: \"158, 158, 158\";\n@grey-600: #757575;\n@grey-700: #616161;\n@grey-800: #424242;\n@grey-900: #212121;\n@grey-A100: #f5f5f5;\n@grey-A200: #eeeeee;\n@grey-A400: #bdbdbd;\n@grey-A700: #616161;\n@grey: @grey-500;\n\n\n@blue-grey-50: #eceff1;\n@blue-grey-100: #cfd8dc;\n@blue-grey-200: #b0bec5;\n@blue-grey-300: #90a4ae;\n@blue-grey-400: #78909c;\n@blue-grey-500: #607d8b;\n@blue-grey-600: #546e7a;\n@blue-grey-700: #455a64;\n@blue-grey-800: #37474f;\n@blue-grey-900: #263238;\n@blue-grey-A100: #cfd8dc;\n@blue-grey-A200: #b0bec5;\n@blue-grey-A400: #78909c;\n@blue-grey-A700: #455a64;\n@blue-grey: @blue-grey-500;\n\n\n@black: #000000; @rgb-black: \"0,0,0\";\n@white: #ffffff; @rgb-white: \"255,255,255\";\n"
  },
  {
    "path": "src/renderer/assets/styles/index.less",
    "content": "@import './reset.less';\n@import './animate.less';\n@import './layout.less';\n\n*, *::after, *::before {\n\t-webkit-user-drag: none;\n}\n\n:root {\n  --color-primary: rgb(77, 175, 124);\n  --color-primary-alpha-100: rgba(77, 175, 124, 0.90);\n  --color-primary-alpha-200: rgba(77, 175, 124, 0.80);\n  --color-primary-alpha-300: rgba(77, 175, 124, 0.70);\n  --color-primary-alpha-400: rgba(77, 175, 124, 0.60);\n  --color-primary-alpha-500: rgba(77, 175, 124, 0.50);\n  --color-primary-alpha-600: rgba(77, 175, 124, 0.40);\n  --color-primary-alpha-700: rgba(77, 175, 124, 0.30);\n  --color-primary-alpha-800: rgba(77, 175, 124, 0.20);\n  --color-primary-alpha-900: rgba(77, 175, 124, 0.10);\n  --color-primary-dark-100: rgb(69,158,112);\n  --color-primary-dark-100-alpha-100: rgba(69, 158, 112, 0.90);\n  --color-primary-dark-100-alpha-200: rgba(69, 158, 112, 0.80);\n  --color-primary-dark-100-alpha-300: rgba(69, 158, 112, 0.70);\n  --color-primary-dark-100-alpha-400: rgba(69, 158, 112, 0.60);\n  --color-primary-dark-100-alpha-500: rgba(69, 158, 112, 0.50);\n  --color-primary-dark-100-alpha-600: rgba(69, 158, 112, 0.40);\n  --color-primary-dark-100-alpha-700: rgba(69, 158, 112, 0.30);\n  --color-primary-dark-100-alpha-800: rgba(69, 158, 112, 0.20);\n  --color-primary-dark-100-alpha-900: rgba(69, 158, 112, 0.10);\n  --color-primary-dark-200: rgb(62,142,101);\n  --color-primary-dark-200-alpha-100: rgba(62, 142, 101, 0.90);\n  --color-primary-dark-200-alpha-200: rgba(62, 142, 101, 0.80);\n  --color-primary-dark-200-alpha-300: rgba(62, 142, 101, 0.70);\n  --color-primary-dark-200-alpha-400: rgba(62, 142, 101, 0.60);\n  --color-primary-dark-200-alpha-500: rgba(62, 142, 101, 0.50);\n  --color-primary-dark-200-alpha-600: rgba(62, 142, 101, 0.40);\n  --color-primary-dark-200-alpha-700: rgba(62, 142, 101, 0.30);\n  --color-primary-dark-200-alpha-800: rgba(62, 142, 101, 0.20);\n  --color-primary-dark-200-alpha-900: rgba(62, 142, 101, 0.10);\n  --color-primary-dark-300: rgb(56,128,91);\n  --color-primary-dark-300-alpha-100: rgba(56, 128, 91, 0.90);\n  --color-primary-dark-300-alpha-200: rgba(56, 128, 91, 0.80);\n  --color-primary-dark-300-alpha-300: rgba(56, 128, 91, 0.70);\n  --color-primary-dark-300-alpha-400: rgba(56, 128, 91, 0.60);\n  --color-primary-dark-300-alpha-500: rgba(56, 128, 91, 0.50);\n  --color-primary-dark-300-alpha-600: rgba(56, 128, 91, 0.40);\n  --color-primary-dark-300-alpha-700: rgba(56, 128, 91, 0.30);\n  --color-primary-dark-300-alpha-800: rgba(56, 128, 91, 0.20);\n  --color-primary-dark-300-alpha-900: rgba(56, 128, 91, 0.10);\n  --color-primary-dark-400: rgb(50,115,82);\n  --color-primary-dark-400-alpha-100: rgba(50, 115, 82, 0.90);\n  --color-primary-dark-400-alpha-200: rgba(50, 115, 82, 0.80);\n  --color-primary-dark-400-alpha-300: rgba(50, 115, 82, 0.70);\n  --color-primary-dark-400-alpha-400: rgba(50, 115, 82, 0.60);\n  --color-primary-dark-400-alpha-500: rgba(50, 115, 82, 0.50);\n  --color-primary-dark-400-alpha-600: rgba(50, 115, 82, 0.40);\n  --color-primary-dark-400-alpha-700: rgba(50, 115, 82, 0.30);\n  --color-primary-dark-400-alpha-800: rgba(50, 115, 82, 0.20);\n  --color-primary-dark-400-alpha-900: rgba(50, 115, 82, 0.10);\n  --color-primary-dark-500: rgb(45,104,74);\n  --color-primary-dark-500-alpha-100: rgba(45, 104, 74, 0.90);\n  --color-primary-dark-500-alpha-200: rgba(45, 104, 74, 0.80);\n  --color-primary-dark-500-alpha-300: rgba(45, 104, 74, 0.70);\n  --color-primary-dark-500-alpha-400: rgba(45, 104, 74, 0.60);\n  --color-primary-dark-500-alpha-500: rgba(45, 104, 74, 0.50);\n  --color-primary-dark-500-alpha-600: rgba(45, 104, 74, 0.40);\n  --color-primary-dark-500-alpha-700: rgba(45, 104, 74, 0.30);\n  --color-primary-dark-500-alpha-800: rgba(45, 104, 74, 0.20);\n  --color-primary-dark-500-alpha-900: rgba(45, 104, 74, 0.10);\n  --color-primary-dark-600: rgb(41,94,67);\n  --color-primary-dark-600-alpha-100: rgba(41, 94, 67, 0.90);\n  --color-primary-dark-600-alpha-200: rgba(41, 94, 67, 0.80);\n  --color-primary-dark-600-alpha-300: rgba(41, 94, 67, 0.70);\n  --color-primary-dark-600-alpha-400: rgba(41, 94, 67, 0.60);\n  --color-primary-dark-600-alpha-500: rgba(41, 94, 67, 0.50);\n  --color-primary-dark-600-alpha-600: rgba(41, 94, 67, 0.40);\n  --color-primary-dark-600-alpha-700: rgba(41, 94, 67, 0.30);\n  --color-primary-dark-600-alpha-800: rgba(41, 94, 67, 0.20);\n  --color-primary-dark-600-alpha-900: rgba(41, 94, 67, 0.10);\n  --color-primary-dark-700: rgb(37,85,60);\n  --color-primary-dark-700-alpha-100: rgba(37, 85, 60, 0.90);\n  --color-primary-dark-700-alpha-200: rgba(37, 85, 60, 0.80);\n  --color-primary-dark-700-alpha-300: rgba(37, 85, 60, 0.70);\n  --color-primary-dark-700-alpha-400: rgba(37, 85, 60, 0.60);\n  --color-primary-dark-700-alpha-500: rgba(37, 85, 60, 0.50);\n  --color-primary-dark-700-alpha-600: rgba(37, 85, 60, 0.40);\n  --color-primary-dark-700-alpha-700: rgba(37, 85, 60, 0.30);\n  --color-primary-dark-700-alpha-800: rgba(37, 85, 60, 0.20);\n  --color-primary-dark-700-alpha-900: rgba(37, 85, 60, 0.10);\n  --color-primary-dark-800: rgb(33,77,54);\n  --color-primary-dark-800-alpha-100: rgba(33, 77, 54, 0.90);\n  --color-primary-dark-800-alpha-200: rgba(33, 77, 54, 0.80);\n  --color-primary-dark-800-alpha-300: rgba(33, 77, 54, 0.70);\n  --color-primary-dark-800-alpha-400: rgba(33, 77, 54, 0.60);\n  --color-primary-dark-800-alpha-500: rgba(33, 77, 54, 0.50);\n  --color-primary-dark-800-alpha-600: rgba(33, 77, 54, 0.40);\n  --color-primary-dark-800-alpha-700: rgba(33, 77, 54, 0.30);\n  --color-primary-dark-800-alpha-800: rgba(33, 77, 54, 0.20);\n  --color-primary-dark-800-alpha-900: rgba(33, 77, 54, 0.10);\n  --color-primary-dark-900: rgb(30,69,49);\n  --color-primary-dark-900-alpha-100: rgba(30, 69, 49, 0.90);\n  --color-primary-dark-900-alpha-200: rgba(30, 69, 49, 0.80);\n  --color-primary-dark-900-alpha-300: rgba(30, 69, 49, 0.70);\n  --color-primary-dark-900-alpha-400: rgba(30, 69, 49, 0.60);\n  --color-primary-dark-900-alpha-500: rgba(30, 69, 49, 0.50);\n  --color-primary-dark-900-alpha-600: rgba(30, 69, 49, 0.40);\n  --color-primary-dark-900-alpha-700: rgba(30, 69, 49, 0.30);\n  --color-primary-dark-900-alpha-800: rgba(30, 69, 49, 0.20);\n  --color-primary-dark-900-alpha-900: rgba(30, 69, 49, 0.10);\n  --color-primary-dark-1000: rgb(27,62,44);\n  --color-primary-dark-1000-alpha-100: rgba(27, 62, 44, 0.90);\n  --color-primary-dark-1000-alpha-200: rgba(27, 62, 44, 0.80);\n  --color-primary-dark-1000-alpha-300: rgba(27, 62, 44, 0.70);\n  --color-primary-dark-1000-alpha-400: rgba(27, 62, 44, 0.60);\n  --color-primary-dark-1000-alpha-500: rgba(27, 62, 44, 0.50);\n  --color-primary-dark-1000-alpha-600: rgba(27, 62, 44, 0.40);\n  --color-primary-dark-1000-alpha-700: rgba(27, 62, 44, 0.30);\n  --color-primary-dark-1000-alpha-800: rgba(27, 62, 44, 0.20);\n  --color-primary-dark-1000-alpha-900: rgba(27, 62, 44, 0.10);\n  --color-primary-light-100: rgb(113,191,150);\n  --color-primary-light-100-alpha-100: rgba(113, 191, 150, 0.90);\n  --color-primary-light-100-alpha-200: rgba(113, 191, 150, 0.80);\n  --color-primary-light-100-alpha-300: rgba(113, 191, 150, 0.70);\n  --color-primary-light-100-alpha-400: rgba(113, 191, 150, 0.60);\n  --color-primary-light-100-alpha-500: rgba(113, 191, 150, 0.50);\n  --color-primary-light-100-alpha-600: rgba(113, 191, 150, 0.40);\n  --color-primary-light-100-alpha-700: rgba(113, 191, 150, 0.30);\n  --color-primary-light-100-alpha-800: rgba(113, 191, 150, 0.20);\n  --color-primary-light-100-alpha-900: rgba(113, 191, 150, 0.10);\n  --color-primary-light-200: rgb(141,204,171);\n  --color-primary-light-200-alpha-100: rgba(141, 204, 171, 0.90);\n  --color-primary-light-200-alpha-200: rgba(141, 204, 171, 0.80);\n  --color-primary-light-200-alpha-300: rgba(141, 204, 171, 0.70);\n  --color-primary-light-200-alpha-400: rgba(141, 204, 171, 0.60);\n  --color-primary-light-200-alpha-500: rgba(141, 204, 171, 0.50);\n  --color-primary-light-200-alpha-600: rgba(141, 204, 171, 0.40);\n  --color-primary-light-200-alpha-700: rgba(141, 204, 171, 0.30);\n  --color-primary-light-200-alpha-800: rgba(141, 204, 171, 0.20);\n  --color-primary-light-200-alpha-900: rgba(141, 204, 171, 0.10);\n  --color-primary-light-300: rgb(164,214,188);\n  --color-primary-light-300-alpha-100: rgba(164, 214, 188, 0.90);\n  --color-primary-light-300-alpha-200: rgba(164, 214, 188, 0.80);\n  --color-primary-light-300-alpha-300: rgba(164, 214, 188, 0.70);\n  --color-primary-light-300-alpha-400: rgba(164, 214, 188, 0.60);\n  --color-primary-light-300-alpha-500: rgba(164, 214, 188, 0.50);\n  --color-primary-light-300-alpha-600: rgba(164, 214, 188, 0.40);\n  --color-primary-light-300-alpha-700: rgba(164, 214, 188, 0.30);\n  --color-primary-light-300-alpha-800: rgba(164, 214, 188, 0.20);\n  --color-primary-light-300-alpha-900: rgba(164, 214, 188, 0.10);\n  --color-primary-light-400: rgb(182,222,201);\n  --color-primary-light-400-alpha-100: rgba(182, 222, 201, 0.90);\n  --color-primary-light-400-alpha-200: rgba(182, 222, 201, 0.80);\n  --color-primary-light-400-alpha-300: rgba(182, 222, 201, 0.70);\n  --color-primary-light-400-alpha-400: rgba(182, 222, 201, 0.60);\n  --color-primary-light-400-alpha-500: rgba(182, 222, 201, 0.50);\n  --color-primary-light-400-alpha-600: rgba(182, 222, 201, 0.40);\n  --color-primary-light-400-alpha-700: rgba(182, 222, 201, 0.30);\n  --color-primary-light-400-alpha-800: rgba(182, 222, 201, 0.20);\n  --color-primary-light-400-alpha-900: rgba(182, 222, 201, 0.10);\n  --color-primary-light-500: rgb(197,229,212);\n  --color-primary-light-500-alpha-100: rgba(197, 229, 212, 0.90);\n  --color-primary-light-500-alpha-200: rgba(197, 229, 212, 0.80);\n  --color-primary-light-500-alpha-300: rgba(197, 229, 212, 0.70);\n  --color-primary-light-500-alpha-400: rgba(197, 229, 212, 0.60);\n  --color-primary-light-500-alpha-500: rgba(197, 229, 212, 0.50);\n  --color-primary-light-500-alpha-600: rgba(197, 229, 212, 0.40);\n  --color-primary-light-500-alpha-700: rgba(197, 229, 212, 0.30);\n  --color-primary-light-500-alpha-800: rgba(197, 229, 212, 0.20);\n  --color-primary-light-500-alpha-900: rgba(197, 229, 212, 0.10);\n  --color-primary-light-600: rgb(209,234,221);\n  --color-primary-light-600-alpha-100: rgba(209, 234, 221, 0.90);\n  --color-primary-light-600-alpha-200: rgba(209, 234, 221, 0.80);\n  --color-primary-light-600-alpha-300: rgba(209, 234, 221, 0.70);\n  --color-primary-light-600-alpha-400: rgba(209, 234, 221, 0.60);\n  --color-primary-light-600-alpha-500: rgba(209, 234, 221, 0.50);\n  --color-primary-light-600-alpha-600: rgba(209, 234, 221, 0.40);\n  --color-primary-light-600-alpha-700: rgba(209, 234, 221, 0.30);\n  --color-primary-light-600-alpha-800: rgba(209, 234, 221, 0.20);\n  --color-primary-light-600-alpha-900: rgba(209, 234, 221, 0.10);\n  --color-primary-light-700: rgb(218,238,228);\n  --color-primary-light-700-alpha-100: rgba(218, 238, 228, 0.90);\n  --color-primary-light-700-alpha-200: rgba(218, 238, 228, 0.80);\n  --color-primary-light-700-alpha-300: rgba(218, 238, 228, 0.70);\n  --color-primary-light-700-alpha-400: rgba(218, 238, 228, 0.60);\n  --color-primary-light-700-alpha-500: rgba(218, 238, 228, 0.50);\n  --color-primary-light-700-alpha-600: rgba(218, 238, 228, 0.40);\n  --color-primary-light-700-alpha-700: rgba(218, 238, 228, 0.30);\n  --color-primary-light-700-alpha-800: rgba(218, 238, 228, 0.20);\n  --color-primary-light-700-alpha-900: rgba(218, 238, 228, 0.10);\n  --color-primary-light-800: rgb(225,241,233);\n  --color-primary-light-800-alpha-100: rgba(225, 241, 233, 0.90);\n  --color-primary-light-800-alpha-200: rgba(225, 241, 233, 0.80);\n  --color-primary-light-800-alpha-300: rgba(225, 241, 233, 0.70);\n  --color-primary-light-800-alpha-400: rgba(225, 241, 233, 0.60);\n  --color-primary-light-800-alpha-500: rgba(225, 241, 233, 0.50);\n  --color-primary-light-800-alpha-600: rgba(225, 241, 233, 0.40);\n  --color-primary-light-800-alpha-700: rgba(225, 241, 233, 0.30);\n  --color-primary-light-800-alpha-800: rgba(225, 241, 233, 0.20);\n  --color-primary-light-800-alpha-900: rgba(225, 241, 233, 0.10);\n  --color-primary-light-900: rgb(231,244,237);\n  --color-primary-light-900-alpha-100: rgba(231, 244, 237, 0.90);\n  --color-primary-light-900-alpha-200: rgba(231, 244, 237, 0.80);\n  --color-primary-light-900-alpha-300: rgba(231, 244, 237, 0.70);\n  --color-primary-light-900-alpha-400: rgba(231, 244, 237, 0.60);\n  --color-primary-light-900-alpha-500: rgba(231, 244, 237, 0.50);\n  --color-primary-light-900-alpha-600: rgba(231, 244, 237, 0.40);\n  --color-primary-light-900-alpha-700: rgba(231, 244, 237, 0.30);\n  --color-primary-light-900-alpha-800: rgba(231, 244, 237, 0.20);\n  --color-primary-light-900-alpha-900: rgba(231, 244, 237, 0.10);\n  --color-primary-light-1000: rgb(255,255,255);\n  --color-primary-light-1000-alpha-100: rgba(255, 255, 255, 0.90);\n  --color-primary-light-1000-alpha-200: rgba(255, 255, 255, 0.80);\n  --color-primary-light-1000-alpha-300: rgba(255, 255, 255, 0.70);\n  --color-primary-light-1000-alpha-400: rgba(255, 255, 255, 0.60);\n  --color-primary-light-1000-alpha-500: rgba(255, 255, 255, 0.50);\n  --color-primary-light-1000-alpha-600: rgba(255, 255, 255, 0.40);\n  --color-primary-light-1000-alpha-700: rgba(255, 255, 255, 0.30);\n  --color-primary-light-1000-alpha-800: rgba(255, 255, 255, 0.20);\n  --color-primary-light-1000-alpha-900: rgba(255, 255, 255, 0.10);\n  --color-theme: rgb(77, 175, 124);\n  // --color-scrollbar-track:\n\n  // --color-900: #fff;\n  // --color-800: #fafafa;\n  // --color-700: #f5f5f5;\n  // --color-600: #eeeeee;\n  // --color-500: #e0e0e0;\n  // --color-400: #bdbdbd;\n  // --color-300: #9e9e9e;\n  // --color-200: #757575;\n  // --color-100: #616161;\n  // --color-050: #424242;\n  // --color-000: #212121;\n  // --color-000: #fff;\n  // --color-050: #fafafa;\n  // --color-100: #f5f5f5;\n  // --color-200: #eeeeee;\n  // --color-300: #e0e0e0;\n  // --color-400: #bdbdbd;\n  // --color-500: #9e9e9e;\n  // --color-600: #757575;\n  // --color-700: #616161;\n  // --color-800: #424242;\n  // --color-900: #212121;\n\n  --color-000: rgb(255,255,255);\n  --color-050: rgb(244,244,244);\n  --color-100: rgb(233,233,233);\n  --color-150: rgb(222,222,222);\n  --color-200: rgb(211,211,211);\n  --color-250: rgb(200,200,200);\n  --color-300: rgb(188,188,188);\n  --color-350: rgb(177,177,177);\n  --color-400: rgb(166,166,166);\n  --color-450: rgb(155,155,155);\n  --color-500: rgb(144,144,144);\n  --color-550: rgb(133,133,133);\n  --color-600: rgb(122,122,122);\n  --color-650: rgb(111,111,111);\n  --color-700: rgb(100,100,100);\n  --color-750: rgb(89,89,89);\n  --color-800: rgb(77,77,77);\n  --color-850: rgb(66,66,66);\n  --color-900: rgb(55,55,55);\n  --color-950: rgb(44,44,44);\n  --color-1000: rgb(33, 33, 33);\n\n  --color-app-background: var(--color-primary-light-600-alpha-600);\n  --color-main-background: rgba(255, 255, 255, 0.9);\n  --color-nav-font: var(--color-primary);\n  // --color-app-background: rgba(0, 0, 0, .5);\n  // --color-main-background: rgba(0, 0, 0, 0.26);\n  --color-btn-hide: #3bc2b2;\n  --color-btn-min: #85c43b;\n  // --color-btn-max: #e7aa36;\n  --color-btn-close: #fab4a0;\n\n  --color-badge-primary: var(--color-primary);\n  --color-badge-secondary: #4baed5;\n  --color-badge-tertiary: #e7aa36;\n\n  --color-font: var(--color-850);\n  --color-font-label: var(--color-450);\n  --color-primary-font: var(--color-primary);\n  --color-primary-font-hover: var(--color-primary-alpha-300);\n  --color-primary-font-active: var(--color-primary-dark-100-alpha-200);\n  --color-primary-background: var(--color-primary-light-400-alpha-700);\n  --color-primary-background-hover: var(--color-primary-light-300-alpha-800);\n  --color-primary-background-active: var(--color-primary-light-100-alpha-800);\n  --color-button-font: var(--color-primary-alpha-100);\n  --color-button-font-selected: var(--color-primary-dark-100-alpha-100);\n  --color-button-background: var(--color-primary-light-400-alpha-700);\n  --color-button-background-selected: var(--color-primary-alpha-600);\n  --color-button-background-hover: var(--color-primary-light-300-alpha-600);\n  --color-button-background-active: var(--color-primary-light-100-alpha-600);\n  --color-list-header-border-bottom: 1px solid var(--color-primary-alpha-900);\n  --color-content-background: var(--color-primary-light-1000);\n\n\n  --background-image: none;\n  --background-image-position: center;\n  --background-image-size: cover;\n}\n\nhtml {\n  font-size: 16px;\n}\n\n.nobreak {\n  white-space: nowrap;\n}\n\n.auto-hidden {\n  .mixin-ellipsis-1();\n}\n\n.center {\n  text-align: center;\n}\n\n.break {\n  word-break: break-all;\n}\n\n.select {\n  user-select: text;\n}\n.no-select {\n  user-select: none;\n}\n\n.badge {\n  display: inline-block;\n  padding: 0.25em 0.4em;\n  font-size: .7em;\n  // font-weight: 700;\n  line-height: 1.2;\n  text-align: center;\n  white-space: nowrap;\n  // vertical-align: baseline;\n  vertical-align: text-top;\n  // border-radius: 2px;\n\n  // &.badge-light {\n  //   background-color: #f8f9fa;\n  // }\n  // &.badge-secondary {\n  //   color: #fff;\n  //   background-color: #6c757d;\n  // }\n  // &.badge-info {\n  //   color: #fff;\n  //   background-color: #4baed5;\n  // }\n  // &.badge-warning {\n  //   color: #fff;\n  //   background-color: #ffa45a;\n  // }\n  // &.badge-danger {\n  //   color: #fff;\n  //   background-color: #ff705a;\n  // }\n  // &.badge-success {\n  //   color: #fff;\n  //   background-color: #32bc63;\n  // }\n  &.badge-theme-primary {\n    color: var(--color-badge-primary);\n  }\n  &.badge-theme-secondary {\n    color: var(--color-badge-secondary);\n  }\n  &.badge-theme-tertiary {\n    color: var(--color-badge-tertiary);\n  }\n}\n\nsmall {\n  font-size: .8em;\n}\n.small {\n  font-size: .9em;\n}\n.tip {\n  color: var(--color-label);\n}\nstrong {\n  font-weight: bold;\n}\n\n.underline {\n  text-decoration: underline;\n}\n\nsvg {\n  transition: @transition-normal;\n  transition-property: fill;\n}\n\nbutton, input, textarea, a {\n  color: var(--color-font);\n}\n\ninput, textarea {\n  &::placeholder {\n    color: var(--color-font-label);\n  }\n}\n::selection {\n  color: var(--color-primary-dark-500-alpha-200);\n  background: var(--color-primary-light-100-alpha-500);\n}\n\n.hover, a {\n  cursor: pointer;\n  transition: color .2s ease;\n  &:hover {\n    color: var(--color-primary-font-hover);\n  }\n  &:active {\n    color: var(--color-primary-font-active);\n  }\n}\n\n.scroll {\n  overflow: auto;\n  &::-webkit-scrollbar {\n    width: 6px;\n    height: 6px;\n    background-color: rgba(0, 0, 0, 0);\n  }\n  &::-webkit-scrollbar-track {\n    background-color: var(--color-primary-light-100-alpha-800);\n    border-radius: 3px;\n    // background-color: rgba(0, 0, 0, 0.1);\n  }\n  &::-webkit-scrollbar-thumb {\n    border-radius: 3px;\n    background-color: var(--color-primary-alpha-600);\n    // background-color: rgba(0, 0, 0, 0.2);\n    transition: background-color 0.4s ease;\n  }\n  &::-webkit-scrollbar-thumb:hover {\n    border-radius: 3px;\n    background-color: var(--color-primary-alpha-400);\n    // background-color: rgba(0, 0, 0, 0.4);\n    transition: background-color 0.4s ease;\n  }\n}\n\n\n.thead {\n  border-bottom: var(--color-list-header-border-bottom);\n  padding-right: 6px;\n  flex: none;\n\n  .num {\n    .nobreak();\n    .center();\n    color: var(--color-font-label);\n  }\n  // box-shadow: 0 0 2px var(--color-primary-dark-500-alpha-800);\n  // position: relative;\n  // z-index: 2;\n}\ntable {\n  width: 100%;\n  border-spacing: 0;\n  border-collapse: collapse;\n  overflow: hidden;\n  color: var(--color-font);\n  th {\n    font-size: 12px;\n    text-align: left;\n    line-height: 38px;\n    padding: 0 6px;\n  }\n}\n\n\n.list {\n  width: 100%;\n  overflow: hidden;\n  color: var(--color-font);\n  flex: auto;\n\n\n  .list-item {\n    height: 100%;\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n    // border-top: 1px solid rgba(0, 0, 0, 0.12);\n    transition: 0.2s ease;\n    transition-property: background-color, color;\n    // border-bottom: 1px solid @color-theme_2-line;\n    box-sizing: border-box;\n    font-size: 12px;\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n\n      // .list-item-cell-action {\n      //   display: block;\n      // }\n    }\n    &.active {\n      background-color: var(--color-primary-background-active);\n    }\n    &.selected {\n      background-color: var(--color-primary-background-hover);\n    }\n    &.disabled {\n      opacity: .5;\n    }\n    .list-item-cell {\n      flex: none;\n      padding: 0 6px;\n      position: relative;\n      // transition:  0.3s cubic-bezier(0.4, 0, 0.2, 1);\n      line-height: 16px;\n      vertical-align: middle;\n      box-sizing: border-box;\n      .mixin-ellipsis-1();\n\n      &.auto {\n        flex: auto;\n      }\n\n      &.num, .num {\n        .nobreak();\n        .center();\n        padding-left: 3px;\n        padding-right: 3px;\n        font-size: 11px;\n        color: var(--color-font-label);\n      }\n\n      &.name {\n        display: flex;\n        flex-flow: row nowrap;\n        overflow: hidden;\n        white-space: initial;\n        text-overflow: initial;\n        align-items: center;\n\n        >.name {\n          .mixin-ellipsis-1();\n        }\n      }\n      .badge {\n        margin-left: 3px;\n        opacity: .85;\n      }\n    }\n\n    // .list-item-cell-action {\n    //   white-space: nowrap;\n    //   display: none;\n    //   flex: auto;\n    //   text-align: right;\n    //   // position: absolute;\n    //   // right: 5px;\n    //   // top: -2px;\n    //   // opacity: 0;\n    //   // transition: opacity .1s ease;\n    // }\n  }\n}\n\n.copying {\n  .no-select {\n    display: none !important;\n  }\n}\n\n.gap-left {\n  + .gap-left {\n    margin-left: 20px;\n  }\n}\n.gap-top {\n  &.top {\n    margin-top: 25px;\n  }\n\n  + .gap-top {\n    margin-top: 10px;\n  }\n}\n\n.color-picker {\n  border-radius: @radius-border !important;\n}\n\n.list-active-enter-active,\n.list-active-leave-active {\n  transition: .13s ease;\n  transition-property: width, opacity;\n}\n\n.list-active-enter-from,\n.list-active-leave-to {\n  width: 0.25em !important;\n  opacity: 0;\n}\n\n.play-active-enter-active,\n.play-active-leave-active {\n  transition: .13s ease;\n  transition-property: transform, opacity;\n}\n\n.play-active-enter-from,\n.play-active-leave-to {\n  transform: scale(0.3);\n  opacity: 0;\n}\n"
  },
  {
    "path": "src/renderer/assets/styles/layout.less",
    "content": "@import './variables.less';\n\n\n/*自动隐藏文字*/\n.mixin-ellipsis-1() {\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n.mixin-ellipsis(@n: 1) {\n\tdisplay: -webkit-box;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tword-wrap: break-word;\n\tword-break: break-all;\n\twhite-space: normal !important;\n\t-webkit-line-clamp: @n;\n\t-webkit-box-orient: vertical;\n}\n.mixin-ellipsis-2() {\n\tdisplay: -webkit-box;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tword-wrap: break-word;\n\tword-break: break-all;\n\twhite-space: normal !important;\n\t-webkit-line-clamp: 2;\n\t-webkit-box-orient: vertical;\n}\n\n.mixin-after() {\n  display: block;\n  position: absolute;\n  content: '';\n}\n"
  },
  {
    "path": "src/renderer/assets/styles/reset.less",
    "content": "// https://github.com/microsoft/vscode/blob/2dd0bca3954d4c03c427d6b447205b68817bd000/src/vs/workbench/browser/media/style.css\n/* Font Families (with CJK support) */\n\n.windows { font-family: \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(zh-Hans) { font-family:\"Microsoft YaHei\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(zh-Hant) { font-family:\"Microsoft Jhenghei\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(ja) { font-family:\"Yu Gothic UI\", \"Meiryo UI\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(ko) { font-family:\"Malgun Gothic\", \"Dotom\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n\n.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }\n.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Hiragino Sans GB\", sans-serif; }\n.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, \"PingFang TC\", sans-serif; }\n.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, \"Hiragino Kaku Gothic Pro\", sans-serif; }\n.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Nanum Gothic\", \"AppleGothic\", sans-serif; }\n\n/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */\n.linux { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", sans-serif; }\n.linux:lang(zh-Hans) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Source Han Sans\", sans-serif; }\n.linux:lang(zh-Hant) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans TC\", \"Source Han Sans TW\", \"Source Han Sans\", sans-serif; }\n.linux:lang(ja) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans J\", \"Source Han Sans JP\", \"Source Han Sans\", sans-serif; }\n.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; }\n\n\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header, hgroup,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n  font-size: inherit;\n\tvertical-align: baseline;\n}\ninput, button, textarea {\n\tfont-family: inherit;\n}\n// html {\n//   font-family:\n//     // windows\n//     Segoe WPC,Segoe UI,\n//     Microsoft YaHei,\n//     Microsoft Jhenghei,\n//     Yu Gothic UI,Meiryo UI,\n//     Malgun Gothic,Dotom,\n\n//     // mac\n//     -apple-system,BlinkMacSystemFont,\n//     PingFang SC,\n//     PingFang TC,\n//     Hiragino Kaku Gothic Pro,\n//     Apple SD Gothic Neo,\n//     Hiragino Sans GB,Nanum Gothic,AppleGothic,\n\n//     // linux\n//     system-ui,Ubuntu,Droid Sans,\n//     Source Han Sans SC,Source Han Sans CN,\n//     Source Han Sans TC,Source Han Sans TW,\n//     Source Han Sans J,Source Han Sans JP,\n//     Source Han Sans K,Source Han Sans JR,\n//     Source Han Sans,UnDotum,FBaekmuk Gulim,\n\n//     sans-serif;\n// }\n\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, hgroup, menu, nav, section {\n  display: block;\n}\n// html {\n// }\nbody {\n\tline-height: 1.2;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n"
  },
  {
    "path": "src/renderer/assets/styles/variables.less",
    "content": "@import './colors.less';\n\n\n// Width\n@width-app-left: 6.6%;\n\n// Height\n@height-toolbar: 54px;\n@height-player: 66px;\n\n\n// Shadow\n@shadow-app: 8px;\n\n\n// Radius\n@radius-progress-border: 5px;\n@radius-border: 4px;\n\n@transition-slow: .6s ease;\n@transition-normal: .4s ease;\n@transition-fast: .3s ease;\n\n@form-radius: 3px;\n"
  },
  {
    "path": "src/renderer/components/base/Btn.vue",
    "content": "<template>\n  <button\n    :class=\"[$style.btn, {[$style.min]: min}, {[$style.outline]: outline}]\"\n    tabindex=\"0\"\n    :disabled=\"disabled\"\n  >\n    <slot />\n  </button>\n</template>\n\n<script>\nexport default {\n  props: {\n    min: {\n      type: Boolean,\n    },\n    outline: {\n      type: Boolean,\n      default: false,\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.btn {\n  display: inline-block;\n  border: none;\n  border-radius: @form-radius;\n  cursor: pointer;\n  padding: 8px 15px;\n  color: var(--color-button-font);\n  outline: none;\n  transition: background-color 0.2s ease;\n  background-color: var(--color-button-background);\n  font-size: 14px;\n  &[disabled] {\n    opacity: .4;\n    cursor: default;\n  }\n\n  &.outline {\n    background-color: transparent;\n  }\n\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n\n.min {\n  padding: 3px 8px;\n  font-size: 12px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/Checkbox.vue",
    "content": "<template>\n  <div :class=\"$style.checkbox\">\n    <input\n      :id=\"id\" ref=\"dom_input\" :type=\"need ? 'radio' : 'checkbox'\" :aria-hidden=\"true\" :checked=\"checked\"\n      :class=\"$style.input\" :disabled=\"disabled\" :value=\"value\" :name=\"name\" @input=\"handleInput($event.target.checked)\"\n    >\n    <label :for=\"id\" :class=\"$style.content\">\n      <div :class=\"$style.container\" :role=\"need ? 'radio' : 'checkbox'\" tabindex=\"0\" :aria-label=\"ariaLabel || label\" :aria-checked=\"checked\" :aria-disabled=\"disabled\" @keydown.enter.space.stop.prevent=\"handleToggle\">\n        <svg version=\"1.1\" :class=\"$style.icon\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" width=\"100%\" viewBox=\"0 32 448 448\" space=\"preserve\">\n          <use xlink:href=\"#icon-check-true\" />\n        </svg>\n      </div>\n      <slot v-if=\"label == null\" />\n      <span v-else :class=\"$style.label\">\n        {{ label }}\n      </span>\n    </label>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    modelValue: {\n      type: [String, Boolean, Number, Array],\n      required: true,\n    },\n    value: {\n      type: [String, Boolean, Number, Array],\n      default: undefined,\n    },\n    id: {\n      type: String,\n      required: true,\n    },\n    name: {\n      type: String,\n      default: undefined,\n    },\n    need: {\n      type: Boolean,\n      default: false,\n    },\n    ariaLabel: {\n      type: String,\n      default: undefined,\n    },\n    label: {\n      type: String,\n      default: undefined,\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  data() {\n    return {\n      checked: false,\n    }\n  },\n  watch: {\n    modelValue(n) {\n      this.setValue(n)\n    },\n  },\n  mounted() {\n    this.setValue(this.modelValue)\n  },\n  methods: {\n    handleInput(checked) {\n      let modelValue\n      if (Array.isArray(this.modelValue)) {\n        modelValue = [...this.modelValue]\n        if (checked) modelValue.push(this.value)\n        else {\n          const index = modelValue.indexOf(this.value)\n          if (index > -1) modelValue.splice(index, 1)\n        }\n      } else {\n        if (typeof this.modelValue == 'boolean') {\n          modelValue = checked\n        } else modelValue = checked ? this.value : ''\n      }\n      this.$emit('update:modelValue', modelValue)\n      this.$emit('change', modelValue)\n    },\n    setValue(value) {\n      let checked\n      if (Array.isArray(value)) {\n        checked = value.includes(this.value)\n      } else {\n        if (typeof this.modelValue == 'boolean') {\n          checked = this.modelValue\n        } else if (value == null) checked = this.modelValue != ''\n        else checked = this.modelValue == this.value\n      }\n      // console.log(this.need, this.value, checked)\n      // this.checked = this.need ? checked && this.value : checked\n      if (this.checked == checked) return\n      this.checked = checked\n    },\n    handleToggle(event) {\n      event.lx_handled = true\n      if (this.need) {\n        if (this.$refs.dom_input.checked) return\n        this.$refs.dom_input.checked = true\n        this.handleInput(true)\n      } else {\n        this.$refs.dom_input.checked = !this.$refs.dom_input.checked\n        this.handleInput(this.$refs.dom_input.checked)\n      }\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.checkbox {\n  display: inline-block;\n  // font-size: 56px;\n}\n.input {\n  display: none;\n  &[disabled] {\n    + .content {\n      opacity: .5;\n      .container, .label {\n        cursor: default;\n      }\n    }\n  }\n  &:checked {\n    + .content {\n      .container {\n        &:after {\n          border-color: var(--color-primary-font);\n        }\n      }\n      .icon {\n        transform: scale(1);\n        // opacity: 1;\n      }\n    }\n  }\n}\n.content {\n  display: flex;\n  align-items: center;\n}\n.container {\n  flex: none;\n  position: relative;\n  width: 1em;\n  height: 1em;\n  cursor: pointer;\n  display: flex;\n  color: var(--color-primary);\n  // border: 1px solid #ccc;\n  &:after {\n    position: absolute;\n    content: ' ';\n    top: 0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    border: 1px solid var(--color-font-label);\n    transition: border-color 0.2s ease;\n    border-radius: 2px;\n  }\n}\n.icon {\n  transition: 0.3s ease;\n  transition-property: transform;\n  transform: scale(0);\n  border-radius: 2px;\n  // opacity: 0;\n}\n\n.label {\n  flex: auto;\n  margin-left: 5px;\n  line-height: 1.5;\n  cursor: pointer;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/Input.vue",
    "content": "<template>\n  <input\n    ref=\"dom_input\"\n    :class=\"$style.input\"\n    :type=\"type\"\n    :placeholder=\"placeholder\"\n    :value=\"modelValue\"\n    :disabled=\"disabled\"\n    tabindex=\"0\"\n    @input=\"handleInput\"\n    @change=\"$emit('change', $event.target.value.trim())\"\n    @keyup.enter=\"$emit('submit', $event.target.value.trim())\"\n    @contextmenu=\"handleContextMenu\"\n  >\n</template>\n\n<script>\nimport { clipboardReadText } from '@common/utils/electron'\n\nexport default {\n  props: {\n    placeholder: {\n      type: String,\n      default: '',\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n    modelValue: {\n      type: [String, Number],\n      default: '',\n    },\n    type: {\n      type: String,\n      default: 'text',\n    },\n    trim: {\n      type: Boolean,\n      default: true,\n    },\n    stopContentEventPropagation: {\n      type: Boolean,\n      default: true,\n    },\n    autoPaste: {\n      type: Boolean,\n      default: true,\n    },\n    // bindValue: {\n    //   type: Boolean,\n    //   default: true,\n    // },\n  },\n  emits: ['update:modelValue', 'submit', 'change'],\n  methods: {\n    handleInput(event) {\n      let value = event.target.value\n      if (this.trim) {\n        value = value.trim()\n        event.target.value = value\n      }\n      this.$emit('update:modelValue', value)\n    },\n    focus() {\n      this.$refs.dom_input.focus()\n    },\n    handleContextMenu(event) {\n      if (this.stopContentEventPropagation) event.stopPropagation()\n      if (!this.autoPaste) return\n      let dom_input = this.$refs.dom_input\n      if (dom_input.selectionStart === null) return\n      let str = clipboardReadText()\n      str = str.trim()\n      str = str.replace(/\\t|\\r\\n|\\n|\\r/g, ' ')\n      str = str.replace(/\\s+/g, ' ')\n      const text = dom_input.value\n      // if (dom_input.selectionStart == dom_input.selectionEnd) {\n      const value = text.substring(0, dom_input.selectionStart) + str + text.substring(dom_input.selectionEnd, text.length)\n      event.target.value = value\n      this.$emit('update:modelValue', value)\n      // } else {\n      //   clipboardWriteText(text.substring(dom_input.selectionStart, dom_input.selectionEnd))\n      // }\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.input {\n  display: inline-block;\n  border: none;\n  border-radius: @form-radius;\n  padding: 7px 8px;\n  color: var(--color-button-font);\n  outline: none;\n  transition: background-color 0.2s ease;\n  background-color: var(--color-primary-background);\n  font-size: 13.3px;\n\n  &::-webkit-outer-spin-button,\n  &::-webkit-inner-spin-button {\n    -webkit-appearance: none;\n    margin: 0;\n  }\n\n  &[disabled] {\n    opacity: .4;\n  }\n\n  &:hover, &:focus {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-background-active);\n  }\n}\n\n.min {\n  padding: 3px 8px;\n  font-size: 12px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/Menu.vue",
    "content": "<template>\n  <teleport to=\"#root\">\n    <ul ref=\"dom_menu\" :class=\"$style.list\" :style=\"menuStyles\" role=\"toolbar\" :aria-hidden=\"!modelValue\">\n      <li\n        v-for=\"item in menus\"\n        v-show=\"!item.hide && (item.action == 'download' ? appSetting['download.enable'] : true)\"\n        :key=\"item.action\"\n        :class=\"$style.listItem\"\n        role=\"tab\"\n        tabindex=\"0\"\n        :aria-label=\"item[itemName]\"\n        ignore-tip\n        :disabled=\"item.disabled ? true : null\"\n        @click=\"menuClick(item)\"\n      >\n        {{ item[itemName] }}\n      </li>\n    </ul>\n  </teleport>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport useMenuLocation from '@renderer/utils/compositions/useMenuLocation'\n\nimport { appSetting } from '@renderer/store/setting'\n\n\nexport default {\n  name: 'MenuToolBar',\n  props: {\n    modelValue: {\n      type: Boolean,\n      required: true,\n    },\n    xy: {\n      type: Object,\n      required: true,\n    },\n    menus: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    itemName: {\n      type: String,\n      default: 'name',\n    },\n  },\n  emits: ['update:modelValue', 'menu-click'],\n  setup(props, { emit }) {\n    const visible = computed(() => props.modelValue)\n    const location = computed(() => props.xy)\n\n    const onHide = () => {\n      emit('update:modelValue', false)\n      menuClick(null)\n    }\n\n    const { dom_menu, menuStyles } = useMenuLocation({\n      visible,\n      location,\n      onHide,\n    })\n\n    const menuClick = (item) => {\n      if (item?.disabled) return\n      emit('menu-click', item)\n    }\n\n    return {\n      dom_menu,\n      menuStyles,\n      menuClick,\n      appSetting,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.list {\n  font-size: 12px;\n  position: absolute;\n  opacity: 0;\n  transform: scale(0);\n  transform-origin: 0 0 0;\n  transition: .14s ease;\n  transition-property: transform, opacity;\n  border-radius: @radius-border;\n  background-color: var(--color-content-background);\n  box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);\n  z-index: 10;\n  overflow: hidden;\n  // will-change: transform;\n}\n.listItem {\n  cursor: pointer;\n  min-width: 96px;\n  line-height: 34px;\n  // color: var(--color-button-font);\n  padding: 0 10px;\n  text-align: center;\n  outline: none;\n  transition: @transition-normal;\n  transition-property: background-color, opacity;\n  box-sizing: border-box;\n  .mixin-ellipsis-1();\n  // background-color: var(--color-primary-light-600-alpha-800);\n\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-background-active);\n  }\n\n  &[disabled] {\n    cursor: default;\n    opacity: .4;\n    &:hover {\n      background: none !important;\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/MusicList.vue",
    "content": "<template>\n  <component :is=\"containerEl\" ref=\"dom_scrollContainer\" :class=\"containerClass\" tabindex=\"0\" style=\"outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;\">\n    <component :is=\"contentEl\" ref=\"dom_list\" :class=\"contentClass\" :style=\"contentStyle\">\n      <!-- <div v-for=\"item in views\" :key=\"item.key\" :style=\"item.style\">\n        <slot name=\"default\" v-bind=\"{ item: item.item, index: item.index }\" />\n      </div> -->\n    </component>\n    <slot name=\"footer\" />\n  </component>\n</template>\n\n<script>\nimport {\n  computed,\n  ref,\n  nextTick,\n  watch,\n  onMounted,\n  onBeforeUnmount,\n  render,\n  // h,\n} from 'vue'\n\nconst easeInOutQuad = (t, b, c, d) => {\n  t /= d / 2\n  if (t < 1) return (c / 2) * t * t + b\n  t--\n  return (-c / 2) * (t * (t - 2) - 1) + b\n}\nconst handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {\n  if (!element) { callback(); return }\n  const start = element.scrollTop || element.scrollY || 0\n  let cancel = false\n  if (to > start) {\n    let maxScrollTop = element.scrollHeight - element.clientHeight\n    if (to > maxScrollTop) to = maxScrollTop\n  } else if (to < start) {\n    if (to < 0) to = 0\n  } else { callback(); return }\n  const change = to - start\n  const increment = 10\n  if (!change) { callback(); return }\n\n  let currentTime = 0\n  let val\n  let cancelCallback\n\n  const animateScroll = () => {\n    currentTime += increment\n    val = parseInt(easeInOutQuad(currentTime, start, change, duration))\n    if (element.scrollTo) {\n      element.scrollTo(0, val)\n    } else {\n      element.scrollTop = val\n    }\n    if (currentTime < duration) {\n      if (cancel) {\n        cancelCallback()\n        onCancel()\n        return\n      }\n      setTimeout(animateScroll, increment)\n    } else {\n      callback()\n    }\n  }\n  animateScroll()\n  return (callback) => {\n    cancelCallback = callback\n    cancel = true\n  }\n}\n\nexport default {\n  name: 'VirtualizedList',\n  props: {\n    containerEl: {\n      type: String,\n      default: 'div',\n    },\n    containerClass: {\n      type: String,\n      default: 'virtualized-list',\n    },\n    contentEl: {\n      type: String,\n      default: 'div',\n    },\n    contentClass: {\n      type: String,\n      default: 'virtualized-list-content',\n    },\n    itemHeight: {\n      type: Number,\n      required: true,\n    },\n    keyName: {\n      type: String,\n      required: true,\n    },\n    list: {\n      type: Array,\n      required: true,\n    },\n  },\n  emits: ['scroll'],\n  setup(props, { emit, slots }) {\n    const views = ref([])\n    const dom_scrollContainer = ref(null)\n    const dom_list = ref(null)\n    let startIndex = -1\n    let endIndex = -1\n    let scrollTop = -1\n    let cachedList = []\n    let cancelScroll = null\n    let isScrolling = false\n    let scrollToValue = 0\n\n    const createList = (startIndex, endIndex) => {\n      if (startIndex == endIndex) return []\n      console.log(startIndex, endIndex)\n      const cache = cachedList.slice(startIndex, endIndex)\n      const list = props.list.slice(startIndex, endIndex).map((item, i) => {\n        if (cache[i]) return cache[i]\n        const top = (startIndex + i) * props.itemHeight\n        const index = startIndex + i\n        return cachedList[index] = {\n          item,\n          top,\n          style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: props.itemHeight + 'px' },\n          index,\n          key: item[props.keyName],\n        }\n      })\n      return list\n    }\n\n    const renderListItem = (list, type) => {\n      if (!list.length) return\n      console.log(list)\n      // const dom = document.createDocumentFragment()\n      switch (type) {\n        case 'up':\n          break\n        case 'down':\n          break\n        default:\n          render(null, slots.default(list[0])[0])\n          break\n      }\n    }\n\n    const updateView = (force = false, currentScrollTop = dom_scrollContainer.value.scrollTop) => {\n      // const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop\n      const itemHeight = props.itemHeight\n      const currentStartIndex = Math.floor(currentScrollTop / itemHeight)\n      const scrollContainerHeight = dom_scrollContainer.value.clientHeight\n      const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)\n      const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex\n      const currentStartRenderIndex = Math.max(currentStartIndex, 0)\n      const currentEndRenderIndex = currentEndIndex + 1\n      // console.log(continuous, currentStartIndex, endIndex, currentEndIndex, startIndex)\n      // debugger\n      if (!force && continuous) {\n        // if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return\n        // console.log('update')\n        if (currentScrollTop > scrollTop) { // scroll down\n          console.log('scroll down')\n          renderListItem(createList(endIndex + 1, currentEndRenderIndex))\n        //   // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))\n        //   // // if (this.views.length > 100) {\n        //   // nextTick(() => {\n        //   //   views.value.splice(0, views.value.indexOf(list[0]))\n        //   // })\n        //   // }\n        } else if (currentScrollTop < scrollTop) { // scroll up\n          console.log('scroll up')\n          renderListItem(createList(currentStartRenderIndex, startIndex))\n          // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n        } else return\n        // if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return\n        // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n      } else {\n        renderListItem(createList(currentStartRenderIndex, currentEndRenderIndex))\n      }\n      startIndex = currentStartIndex\n      endIndex = currentEndIndex\n      scrollTop = currentScrollTop\n    }\n\n    const onScroll = event => {\n      const currentScrollTop = dom_scrollContainer.value.scrollTop\n      if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {\n        updateView(false, currentScrollTop)\n      }\n      emit('scroll', event)\n    }\n\n    const scrollTo = async(scrollTop, animate = false) => {\n      return new Promise(resolve => {\n        if (cancelScroll) {\n          cancelScroll(resolve)\n        } else {\n          resolve()\n        }\n      }).then(async() => {\n        return new Promise((resolve, reject) => {\n          if (animate) {\n            isScrolling = true\n            scrollToValue = scrollTop\n            cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {\n              cancelScroll = null\n              isScrolling = false\n              resolve()\n            }, () => {\n              cancelScroll = null\n              isScrolling = false\n              reject(new Error('canceled'))\n            })\n          } else {\n            dom_scrollContainer.value.scrollTop = scrollTop\n          }\n        })\n      })\n    }\n\n    const scrollToIndex = async(index, offset = 0, animate = false) => {\n      return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)\n    }\n\n    const getScrollTop = () => {\n      return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop\n    }\n\n    const handleResize = () => {\n      setTimeout(updateView)\n    }\n\n    const contentStyle = computed(() => ({\n      display: 'block',\n      height: props.list.length * props.itemHeight + 'px',\n    }))\n\n    const handleReset = list => {\n      cachedList = Array(list.length)\n      startIndex = -1\n      endIndex = -1\n      void nextTick(() => {\n        updateView(true)\n      })\n    }\n    watch(() => props.itemHeight, () => {\n      handleReset(props.list)\n    })\n    watch(() => props.list, (list) => {\n      handleReset(list)\n    }, {\n      deep: true,\n    })\n\n    onMounted(() => {\n      dom_scrollContainer.value.addEventListener('scroll', onScroll, {\n        capture: false,\n        passive: true,\n      })\n      cachedList = Array(props.list.length)\n      startIndex = -1\n      endIndex = -1\n      updateView(true)\n      window.addEventListener('resize', handleResize)\n    })\n    onBeforeUnmount(() => {\n      dom_scrollContainer.value.removeEventListener('scroll', onScroll)\n      window.removeEventListener('resize', handleResize)\n      if (cancelScroll) cancelScroll()\n    })\n\n    return {\n      views,\n      dom_scrollContainer,\n      dom_list,\n      contentStyle,\n      scrollTo,\n      scrollToIndex,\n      getScrollTop,\n    }\n  },\n  // render() {\n  //   console.log('render')\n  //   // const list = this.views.map((item) => {\n  //   //   return h('div', { style: item.style, key: item.key }, this.$slots.default(item))\n  //   // })\n  //   return h(this.containerEl, {\n  //     ref: 'dom_scrollContainer',\n  //     class: this.containerClass,\n  //     style: 'outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;',\n  //   }, [\n  //     h(this.contentEl, { style: this.contentStyle, class: this.contentClass }, this.views.map((item) => {\n  //       return h('div', { style: item.style, key: item.key }, 'this.$slots.default(item)')\n  //     })),\n  //   ])\n  // },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/components/base/Popup.vue",
    "content": "<template>\n  <component :is=\"Teleport\" to=\"#root\">\n    <div\n      :class=\"[$style.popup, {[$style.top]: isShowTop}, {[$style.active]: props.visible}]\"\n      :style=\"popupStyle\"\n      :aria-hidden=\"!props.visible\"\n      @click.stop\n      @mouseenter=\"emit('mouseenter', $event)\"\n      @mouseleave=\"emit('mouseleave', $event)\"\n      @transitionend=\"emit('transitionend', $event)\"\n    >\n      <div ref=\"dom_content\" class=\"scroll\" :class=\"$style.list\">\n        <slot />\n      </div>\n    </div>\n  </component>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watch, onMounted, onBeforeUnmount, reactive } from '@common/utils/vueTools'\n\n// https://github.com/vuejs/core/issues/2855#issuecomment-768388962\nimport {\n  Teleport as teleport_,\n  type TeleportProps,\n  type VNodeProps,\n} from 'vue'\nconst Teleport = teleport_ as new () => {\n  $props: VNodeProps & TeleportProps\n}\n\nconst props = defineProps<{\n  visible: boolean\n  btnEl: HTMLElement | null\n}>()\n\ninterface Emitter {\n  (event: 'update:visible', visible: boolean): void\n  (event: 'mouseenter', visible: MouseEvent): void\n  (event: 'mouseleave', visible: MouseEvent): void\n  (event: 'transitionend', visible: TransitionEvent): void\n}\nconst emit = defineEmits<Emitter>()\n\nconst dom_content = ref<HTMLElement | null>(null)\nconst isShowTop = ref(false)\n\nconst popupStyle = reactive({\n  maxHeight: 'none',\n  top: '0px',\n  left: '0px',\n  '--arrow-left': '0px',\n})\n\nconst arrowHeight = 9\nconst arrowWidth = 8\nconst sidePadding = 50\n\nwatch(() => props.visible, (visible) => {\n  if (!visible || !dom_content.value || !props.btnEl) return\n  const rect = props.btnEl.getBoundingClientRect()\n  const maxHeight = document.body.clientHeight\n  const elTop = rect.top - window.lx.rootOffset\n  const bottomTopVal = elTop + rect.height\n  const contentHeight = dom_content.value.scrollHeight + arrowHeight + sidePadding\n  if (bottomTopVal + contentHeight < maxHeight || (contentHeight > elTop && elTop <= maxHeight - bottomTopVal)) {\n    isShowTop.value = false\n    popupStyle.top = bottomTopVal + arrowHeight + 'px'\n    popupStyle.maxHeight = maxHeight - bottomTopVal - arrowHeight - sidePadding + 'px'\n  } else {\n    isShowTop.value = true\n    let maxContentHeight = elTop - arrowHeight - sidePadding\n    popupStyle.top = (elTop - (elTop < contentHeight ? elTop : contentHeight) + sidePadding) + 'px'\n    popupStyle.maxHeight = maxContentHeight + 'px'\n  }\n\n  const maxWidth = document.body.clientWidth - 20\n  let center = dom_content.value.clientWidth / 2\n  let left = rect.left + rect.width / 2 - window.lx.rootOffset - center\n  if (left < sidePadding) {\n    center -= sidePadding - left\n    left = sidePadding\n  } else if (left + dom_content.value.clientWidth > maxWidth) {\n    let newLeft = maxWidth - dom_content.value.clientWidth\n    center = center + left - newLeft\n    left = newLeft\n  }\n  popupStyle.left = left + 'px'\n  popupStyle['--arrow-left'] = center - arrowWidth + 'px'\n})\n\nconst handleHide = (evt?: MouseEvent) => {\n  // if (evt && (evt.target as HTMLElement)?.parentNode != dom_content.value && props.visible) return emit('update:visible', false)\n  // console.log(this.$refs)\n  // if (evt && (evt.target == dom_btn.value || dom_btn.value?.contains(evt.target as HTMLElement))) return\n  // setTimeout(() => {\n  //   popupVisible.value = false\n  emit('update:visible', false)\n  // }, 50)\n}\n\n\nonMounted(() => {\n  document.addEventListener('click', handleHide)\n})\n\nonBeforeUnmount(() => {\n  document.removeEventListener('click', handleHide)\n})\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.popup {\n  position: absolute;\n  // top: -100%;\n  // width: 645px;\n  // left: 8px;\n  // margin-top: 12px;\n  max-width: 98%;\n  border-radius: 4px;\n  background-color: var(--color-content-background);\n  opacity: 0;\n  transform: scale(.8);\n  transform-origin: 50% 0 0;\n  transition: .16s ease;\n  transition-property: transform, opacity;\n  max-height: 250px;\n  z-index: 10;\n  pointer-events: none;\n  filter: drop-shadow(0px 0px 3px rgba(0, 0, 0, .12));\n  display: flex;\n\n  &:before {\n    content: \" \";\n    position: absolute;\n    top: -6px;\n    left: var(--arrow-left);\n    width: 0;\n    height: 0;\n    border-left: 8px solid transparent;\n    border-right: 8px solid transparent;\n    border-bottom: 8px solid var(--color-content-background);\n  }\n\n  &.active {\n    opacity: 1;\n    transform: scale(1);\n    pointer-events: initial;\n  }\n\n  &.top {\n    filter: drop-shadow(0px 1px 3px rgba(0, 0, 0, .12));\n    transform-origin: 50% 100% 0;\n\n    &:before {\n      top: 100%;\n      border-bottom: none;\n      border-top: 8px solid var(--color-content-background);\n    }\n  }\n}\n.list {\n  padding: 10px;\n  box-sizing: border-box;\n  // box-shadow: 0 0 4px rgba(0, 0, 0, .2);\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/Selection.vue",
    "content": "<template>\n  <div class=\"content\" :class=\"[$style.select, show ? $style.active : '']\">\n    <div ref=\"dom_btn\" class=\"label-content\" :class=\"$style.label\" @click=\"handleShow\">\n      <span class=\"label\">{{ label }}</span>\n      <div class=\"icon\" :class=\"$style.icon\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.847 451.847\" space=\"preserve\">\n          <use xlink:href=\"#icon-down\" />\n        </svg>\n      </div>\n    </div>\n    <ul v-if=\"show\" ref=\"dom_list\" class=\"selection-list scroll\" :class=\"$style.list\" :style=\"listStyles\">\n      <li\n        v-for=\"(item, index) in list\" :key=\"index\" :class=\"[$style.listItem, (itemKey ? item[itemKey] : item) == modelValue ? $style.active : null]\"\n        :aria-label=\"itemName ? item[itemName] : item\" @click=\"handleClick(item)\"\n      >\n        {{ itemName ? item[itemName] : item }}\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\n\nexport default {\n  props: {\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    modelValue: {\n      type: [String, Number],\n      required: true,\n    },\n    itemName: {\n      type: String,\n      default: '',\n    },\n    itemKey: {\n      type: String,\n      default: '',\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  data() {\n    return {\n      show: false,\n      listStyles: {\n        transform: 'scaleY(0) translateY(0)',\n      },\n    }\n  },\n  computed: {\n    activeIndex() {\n      if (this.modelValue == null) return -1\n      if (!this.itemName) return this.list.indexOf(this.modelValue)\n      return this.list.findIndex(l => l[this.itemKey] == this.modelValue)\n    },\n    label() {\n      if (this.modelValue == null) return ''\n      if (this.itemName == null) return this.modelValue\n      const item = this.list[this.activeIndex]\n      if (!item) return ''\n      return item[this.itemName]\n    },\n  },\n  mounted() {\n    document.addEventListener('click', this.handleHide, true)\n  },\n  beforeUnmount() {\n    document.removeEventListener('click', this.handleHide, true)\n  },\n  methods: {\n    handleHide(e) {\n      if (!this.show) return\n      // if (e && e.target.parentNode != this.$refs.dom_list && this.show) return this.show = false\n      if (e && (e.target == this.$refs.dom_btn || this.$refs.dom_btn.contains(e.target))) return\n      this.listStyles.transform = 'scaleY(0) translateY(0)'\n      setTimeout(() => {\n        this.show = false\n      }, 50)\n    },\n    handleClick(item) {\n      // console.log(this.modelValue)\n      if (item === this.modelValue) return\n      this.$emit('update:modelValue', this.itemKey ? item[this.itemKey] : item)\n      this.$emit('change', item)\n    },\n    handleShow() {\n      this.show = true\n      this.$nextTick(() => {\n        this.listStyles.transform = `scaleY(1) translateY(${this.handleGetOffset()}px)`\n\n        const activeItem = this.$refs.dom_list.children[this.activeIndex]\n        if (activeItem) this.$refs.dom_list.scrollTop = activeItem.offsetTop - this.$refs.dom_list.clientHeight * 0.38\n      })\n    },\n    handleGetOffset() {\n      const listHeight = this.$refs.dom_list.clientHeight\n      const dom_select = this.$refs.dom_list.offsetParent\n      const dom_container = dom_select.offsetParent\n      const containerHeight = dom_container.clientHeight\n      if (containerHeight < listHeight) return 0\n      const offsetHeight = (dom_container.scrollTop + containerHeight) - (dom_select.offsetTop + listHeight)\n      if (offsetHeight > 0) return 0\n      return offsetHeight - 5\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@selection-height: 28px;\n\n.select {\n  display: inline-block;\n  font-size: 12px;\n  position: relative;\n  width: var(--selection-width, 300px);\n\n  &.active {\n    .label {\n      background-color: var(--color-button-background);\n    }\n    .list {\n      opacity: 1;\n    }\n    .icon {\n      svg{\n        transform: rotate(180deg);\n      }\n    }\n  }\n}\n\n.label {\n  background-color: var(--color-button-background);\n  padding: 0 10px;\n  transition: background-color @transition-normal;\n  height: @selection-height;\n  // line-height: 27px;\n  line-height: 1.5;\n  box-sizing: border-box;\n  color: var(--color-button-font);\n  border-radius: @form-radius;\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n\n  span {\n    flex: auto;\n    .mixin-ellipsis-1();\n  }\n  .icon {\n    flex: none;\n    margin-left: 7px;\n    line-height: 0;\n    svg {\n      width: 1em;\n      transition: transform .2s ease;\n      transform: rotate(0);\n    }\n  }\n\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n\n.list {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  background-color: var(--color-content-background);\n  opacity: 0;\n  transform: scaleY(0) translateY(0);\n  transform-origin: 0 (@selection-height / 2) 0;\n  transition: .25s ease;\n  transition-property: transform, opacity;\n  z-index: 10;\n  border-radius: @form-radius;\n  box-shadow: 0 0 4px rgba(0, 0, 0, .15);\n  overflow: auto;\n  max-height: 200px;\n}\n.listItem {\n  cursor: pointer;\n  padding: 0 10px;\n  line-height: @selection-height;\n  // color: var(--color-button-font);\n  outline: none;\n  transition: background-color @transition-normal;\n  background-color: transparent;\n  box-sizing: border-box;\n  .mixin-ellipsis-1();\n\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n  &.active {\n    color: var(--color-button-font);\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/SliderBar.vue",
    "content": "<template>\n  <div :class=\"[$style.sliderContent, { [$style.disabled]: disabled }, className]\">\n    <div :class=\"[$style.slider]\">\n      <div ref=\"dom_sliderBar\" :class=\"$style.sliderBar\" :style=\"{ transform: `scaleX(${(value - min) / (max - min) || 0})` }\" />\n    </div>\n    <div :class=\"$style.sliderMask\" @mousedown=\"handleSliderMsDown\" />\n  </div>\n</template>\n\n<script>\nimport { ref, onBeforeUnmount } from '@common/utils/vueTools'\n// import { player as eventPlayerNames } from '@renderer/event/names'\n\nexport default {\n  props: {\n    className: {\n      type: String,\n      default: '',\n    },\n    value: {\n      type: Number,\n      required: true,\n    },\n    min: {\n      type: Number,\n      required: true,\n    },\n    max: {\n      type: Number,\n      required: true,\n    },\n    step: {\n      type: Number,\n      default: 1,\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['change'],\n  setup(props, { emit }) {\n    const sliderEvent = {\n      isMsDown: false,\n      msDownX: 0,\n      msDownRatio: 0,\n    }\n    const dom_sliderBar = ref(null)\n\n    const clampValue = val => {\n      if (val < props.min) return props.min\n      if (val > props.max) return props.max\n      return val\n    }\n    const getSteppedValue = val => {\n      const step = props.step > 0 ? props.step : 1\n      const stepped = Math.round((val - props.min) / step) * step + props.min\n      return clampValue(Number(stepped.toFixed(10)))\n    }\n    const getSliderWidth = () => dom_sliderBar.value?.clientWidth || 0\n    const getRange = () => props.max - props.min\n    const emitSteppedValue = rawValue => {\n      const value = getSteppedValue(rawValue)\n      emit('change', value)\n      return value\n    }\n\n    const handleSliderMsDown = event => {\n      if (props.disabled) return\n      const width = getSliderWidth()\n      if (!width) return\n\n      sliderEvent.isMsDown = true\n      sliderEvent.msDownX = event.clientX\n\n      const rawValue = (event.offsetX / width) * getRange() + props.min\n      const value = emitSteppedValue(rawValue)\n      sliderEvent.msDownRatio = getRange() === 0 ? 0 : (value - props.min) / getRange()\n    }\n    const handleSliderMsUp = () => {\n      sliderEvent.isMsDown = false\n    }\n    const handleSliderMsMove = event => {\n      if (!sliderEvent.isMsDown || props.disabled) return\n      const width = getSliderWidth()\n      if (!width) return\n\n      const ratio = sliderEvent.msDownRatio + (event.clientX - sliderEvent.msDownX) / width\n      const rawValue = ratio * getRange() + props.min\n      emitSteppedValue(rawValue)\n    }\n\n    document.addEventListener('mousemove', handleSliderMsMove)\n    document.addEventListener('mouseup', handleSliderMsUp)\n    onBeforeUnmount(() => {\n      document.removeEventListener('mousemove', handleSliderMsMove)\n      document.removeEventListener('mouseup', handleSliderMsUp)\n    })\n\n    return {\n      handleSliderMsDown,\n      dom_sliderBar,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.sliderContent {\n  flex: none;\n  position: relative;\n  width: 100px;\n  padding: 5px 0;\n  // margin-right: 10px;\n  display: flex;\n  align-items: center;\n  opacity: .5;\n  transition: opacity @transition-normal;\n  &:hover {\n    opacity: 1;\n  }\n  &.disabled {\n    opacity: .3;\n    .sliderMask {\n      cursor: default;\n    }\n  }\n}\n\n.slider {\n  // cursor: pointer;\n  width: 100%;\n  height: 5px;\n  border-radius: 20px;\n  overflow: hidden;\n  transition: @transition-normal;\n  transition-property: background-color, opacity;\n  background-color: var(--color-primary-alpha-700);\n  // background-color: #f5f5f5;\n  position: relative;\n  // border-radius: @radius-progress-border;\n}\n\n// .muted {\n//   opacity: .5;\n// }\n\n.sliderBar {\n  position: absolute;\n  left: 0;\n  top: 0;\n  transform: scaleX(0);\n  transform-origin: 0;\n  transition-property: transform;\n  transition-timing-function: ease;\n  width: 100%;\n  height: 100%;\n  // border-radius: @radius-progress-border;\n  transition-duration: 0.2s;\n  background-color: var(--color-button-font);\n  box-shadow: 0 0 2px rgba(0, 0, 0, 0.2);\n}\n\n.sliderMask {\n  position: absolute;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  cursor: pointer;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/Tab.vue",
    "content": "<template>\n  <ul :class=\"[$style.list, $style[align]]\" role=\"tablist\">\n    <li\n      v-for=\"item in list\"\n      :key=\"item[itemKey]\" :class=\"[$style.listItem, {[$style.active]: modelValue == item[itemKey]}]\" tabindex=\"-1\" role=\"tab\"\n      :aria-label=\"item[itemLabel]\" ignore-tip :aria-selected=\"modelValue == item[itemKey]\" @click=\"handleToggle(item[itemKey])\"\n    >\n      <span :class=\"$style.label\">{{ item[itemLabel] }}</span>\n    </li>\n  </ul>\n</template>\n\n<script>\n\nexport default {\n  props: {\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    align: {\n      type: String,\n      default: 'left',\n    },\n    itemKey: {\n      type: String,\n      default: 'id',\n    },\n    itemLabel: {\n      type: String,\n      default: 'label',\n    },\n    modelValue: {\n      type: [String, Number],\n      default: '',\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  setup(props, { emit }) {\n    const handleToggle = id => {\n      if (id == props.modelValue) return\n      emit('update:modelValue', id)\n      emit('change', id)\n    }\n\n    return {\n      handleToggle,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.list {\n  display: flex;\n  flex-flow: row nowrap;\n  font-size: 12px;\n  gap: 25px;\n  padding: 0 15px;\n\n  &.left {\n    justify-content: flex-start;\n  }\n  &.center {\n    justify-content: center;\n  }\n  &.right {\n    justify-content: flex-end;\n  }\n}\n.listItem {\n  display: block;\n  // padding: 5px 15px;\n  cursor: pointer;\n  transition: color @transition-normal;\n\n\n  &:hover {\n    color: var(--color-primary);\n  }\n\n\n  &.active {\n    color: var(--color-primary);\n    cursor: default;\n\n    >.label {\n      &:after {\n        // background-color: var(--color-primary);\n        opacity: 1;\n        transform: translateY(0);\n      }\n    }\n  }\n}\n\n.label {\n  display: block;\n  position: relative;\n  padding: 8px 0;\n  &:after {\n    .mixin-after();\n    left: 0;\n    bottom: 0;\n    width: 100%;\n    height: 2px;\n    border-radius: 20px;\n    background-color: transparent;\n    transform: translateY(-4px);\n    opacity: 0;\n    background-color: var(--color-primary-alpha-300);\n    transition: @transition-fast;\n    transition-property: transform, opacity;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/base/VirtualizedList.vue",
    "content": "<template>\n  <component\n    :is=\"containerEl\"\n    ref=\"dom_scrollContainer\"\n    :class=\"containerClass\"\n    tabindex=\"0\"\n    style=\"outline: none; height: 100%; overflow-y: auto; position: relative; display: block; contain: strict;\"\n  >\n    <component :is=\"contentEl\" :class=\"contentClass\" :style=\"contentStyle\">\n      <div v-for=\"item in views\" :key=\"item.key\" :style=\"item.style\">\n        <slot name=\"default\" v-bind=\"{ item: item.item, index: item.index }\" />\n      </div>\n    </component>\n    <slot name=\"footer\" />\n  </component>\n</template>\n\n<script>\nimport {\n  computed,\n  ref,\n  nextTick,\n  watch,\n  onMounted,\n  onBeforeUnmount,\n} from 'vue'\n\n/**\n * 生成防抖函数\n * @param {*} fn\n * @param {*} delay\n */\nexport const debounce = (fn, delay = 100) => {\n  let timer = null\n  let _args = null\n  return function(...args) {\n    _args = args\n    if (timer) clearTimeout(timer)\n    timer = setTimeout(() => {\n      timer = null\n      fn.apply(this, _args)\n    }, delay)\n  }\n}\n\nconst easeInOutQuad = (t, b, c, d) => {\n  t /= d / 2\n  if (t < 1) return (c / 2) * t * t + b\n  t--\n  return (-c / 2) * (t * (t - 2) - 1) + b\n}\nconst handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {\n  if (!element) { callback(); return }\n  const start = element.scrollTop || element.scrollY || 0\n  let cancel = false\n  if (to > start) {\n    let maxScrollTop = element.scrollHeight - element.clientHeight\n    if (to > maxScrollTop) to = maxScrollTop\n  } else if (to < start) {\n    if (to < 0) to = 0\n  } else { callback(); return }\n  const change = to - start\n  const increment = 10\n  if (!change) { callback(); return }\n\n  let currentTime = 0\n  let val\n  let cancelCallback\n\n  const animateScroll = () => {\n    currentTime += increment\n    val = parseInt(easeInOutQuad(currentTime, start, change, duration))\n    if (element.scrollTo) {\n      element.scrollTo(0, val)\n    } else {\n      element.scrollTop = val\n    }\n    if (currentTime < duration) {\n      if (cancel) {\n        cancelCallback()\n        onCancel()\n        return\n      }\n      window.setTimeout(animateScroll, increment)\n    } else {\n      callback()\n    }\n  }\n  animateScroll()\n  return (callback) => {\n    cancelCallback = callback\n    cancel = true\n  }\n}\n\nexport default {\n  name: 'VirtualizedList',\n  props: {\n    containerEl: {\n      type: String,\n      default: 'div',\n    },\n    containerClass: {\n      type: String,\n      default: 'virtualized-list',\n    },\n    contentEl: {\n      type: String,\n      default: 'div',\n    },\n    contentClass: {\n      type: String,\n      default: 'virtualized-list-content',\n    },\n    itemHeight: {\n      type: Number,\n      required: true,\n    },\n    keyName: {\n      type: String,\n      required: true,\n    },\n    list: {\n      type: Array,\n      required: true,\n    },\n  },\n  emits: ['scroll'],\n  setup(props, { emit }) {\n    const views = ref([])\n    const dom_scrollContainer = ref(null)\n    let isListScrolling = false\n    const isListScrollingRef = ref(false)\n    let startIndex = -1\n    let endIndex = -1\n    let scrollTop = -1\n    let cachedList = []\n    let cancelScroll = null\n    let isAutoScrolling = false\n    let scrollToValue = 0\n\n    const createList = (startIndex, endIndex) => {\n      const cache = cachedList.slice(startIndex, endIndex)\n      const list = props.list.slice(startIndex, endIndex).map((item, i) => {\n        if (cache[i]) return cache[i]\n        const top = (startIndex + i) * props.itemHeight\n        const index = startIndex + i\n        return cachedList[index] = {\n          item,\n          top,\n          style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: props.itemHeight + 'px' },\n          index,\n          key: item[props.keyName],\n        }\n      })\n      return list\n    }\n\n    const updateView = (currentScrollTop = dom_scrollContainer.value.scrollTop) => {\n      // const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop\n      const itemHeight = props.itemHeight\n      const currentStartIndex = Math.floor(currentScrollTop / itemHeight)\n      const scrollContainerHeight = dom_scrollContainer.value.clientHeight\n      const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)\n      const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex\n      const currentStartRenderIndex = Math.max(currentStartIndex, 0)\n      const currentEndRenderIndex = currentEndIndex + 1\n      // console.log(continuous)\n      // debugger\n      if (continuous) {\n        // if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return\n        // console.log('update')\n        // if (currentScrollTop > scrollTop) { // scroll down\n        //   // console.log('scroll down')\n        //   views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n        //   // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))\n        //   // // if (this.views.length > 100) {\n        //   // nextTick(() => {\n        //   //   views.value.splice(0, views.value.indexOf(list[0]))\n        //   // })\n        //   // }\n        // } else if (currentScrollTop < scrollTop) { // scroll up\n        //   // console.log('scroll up')\n        //   views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n        // } else return\n        if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return\n        requestAnimationFrame(() => {\n          views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n        })\n      } else {\n        requestAnimationFrame(() => {\n          views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n        })\n      }\n      startIndex = currentStartIndex\n      endIndex = currentEndIndex\n      scrollTop = currentScrollTop\n    }\n\n    const setStopScrollStatus = debounce(() => {\n      isListScrolling = false\n      isListScrollingRef.value = false\n    }, 200)\n    const onScroll = event => {\n      if (!isListScrolling) isListScrolling = isListScrollingRef.value = true\n      setStopScrollStatus()\n\n      const currentScrollTop = dom_scrollContainer.value.scrollTop\n      if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {\n        updateView(currentScrollTop)\n      }\n      emit('scroll', event)\n    }\n\n    const scrollTo = (scrollTop, animate = false, onScrollEnd) => {\n      if (onScrollEnd) {\n        void new Promise(resolve => {\n          if (cancelScroll) {\n            cancelScroll(resolve)\n          } else {\n            resolve()\n          }\n        }).then(() => {\n          if (animate) {\n            isAutoScrolling = true\n            scrollToValue = scrollTop\n            cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {\n              cancelScroll = null\n              isAutoScrolling = false\n              onScrollEnd(true)\n            }, () => {\n              cancelScroll = null\n              isAutoScrolling = false\n              onScrollEnd('canceled')\n            })\n          } else {\n            dom_scrollContainer.value.scrollTop = scrollTop\n          }\n        })\n      } else {\n        dom_scrollContainer.value.scrollTo({\n          top: scrollTop,\n          behavior: animate ? 'smooth' : 'instant',\n        })\n      }\n    }\n\n    const scrollToIndex = (index, offset = 0, animate = false, onScrollEnd) => {\n      scrollTo(Math.max(index * props.itemHeight + offset, 0), animate, onScrollEnd)\n    }\n\n    const getScrollTop = () => {\n      return isAutoScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop\n    }\n\n    const handleResize = () => {\n      window.setTimeout(updateView)\n    }\n\n    const contentStyle = computed(() => {\n      const style = {\n        display: 'block',\n        height: props.list.length * props.itemHeight + 'px',\n      }\n      if (isListScrollingRef.value) style['pointer-events'] = 'none'\n      return style\n    })\n\n    const handleReset = list => {\n      cachedList = Array(list.length)\n      startIndex = -1\n      endIndex = -1\n      if (cachedList.length) {\n        void nextTick(() => {\n          requestAnimationFrame(() => {\n            updateView()\n          })\n        })\n      } else {\n        views.value = []\n      }\n    }\n    watch(() => props.itemHeight, () => {\n      handleReset(props.list)\n    })\n    watch(() => props.list, (list) => {\n      handleReset(list)\n    })\n\n    onMounted(() => {\n      dom_scrollContainer.value.addEventListener('scroll', onScroll, {\n        capture: false,\n        passive: true,\n      })\n      cachedList = Array(props.list.length)\n      startIndex = -1\n      endIndex = -1\n\n      if (props.list.length) {\n        void nextTick(() => {\n          requestAnimationFrame(() => {\n            console.log('updateView')\n            updateView()\n          })\n        })\n      }\n      window.addEventListener('resize', handleResize)\n    })\n    onBeforeUnmount(() => {\n      dom_scrollContainer.value.removeEventListener('scroll', onScroll)\n      window.removeEventListener('resize', handleResize)\n      if (cancelScroll) cancelScroll()\n    })\n\n    return {\n      views,\n      dom_scrollContainer,\n      contentStyle,\n      scrollTo,\n      scrollToIndex,\n      getScrollTop,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/components/base/useVirtualizedList.ts",
    "content": "// import {\n//   computed,\n//   ref,\n//   nextTick,\n//   watch,\n//   onMounted,\n//   onBeforeUnmount,\n// } from 'vue'\n// import { scrollTo } from '@common/utils/renderer'\n\n// interface ListItem {\n//   item: LX.Music.MusicInfo\n//   top: number\n//   style: {\n//     position: string\n//     left: number\n//     right: number\n//     top: string\n//     height: string\n//   }\n//   index: number\n//   key: string\n// }\n\n// export default (props: { list: LX.Music.MusicInfo[], itemHeight: number }) => {\n//   const dom_scrollContainer = ref<HTMLElement | null>(null)\n//   const dom_list = ref<HTMLElement | null>(null)\n//   let startIndex = -1\n//   let endIndex = -1\n//   let scrollTop = -1\n//   let cachedList: ListItem[] = []\n//   let cancelScroll: null | (() => void) = null\n//   let isScrolling = false\n//   let scrollToValue = 0\n\n//   const createList = (startIndex: number, endIndex: number) => {\n//     if (startIndex == endIndex) return []\n//     console.log(startIndex, endIndex)\n//     const cache = cachedList.slice(startIndex, endIndex)\n//     const list = props.list.slice(startIndex, endIndex).map((item, i) => {\n//       if (cache[i]) return cache[i]\n//       const top = (startIndex + i) * props.itemHeight\n//       const index = startIndex + i\n//       return cachedList[index] = {\n//         item,\n//         top,\n//         style: { position: 'absolute', left: 0, right: 0, top: `${top}px`, height: `${props.itemHeight}px` },\n//         index,\n//         key: item.id,\n//       }\n//     })\n//     return list\n//   }\n\n//   // div.list-item(@click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n//   // :class=\"[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]\")\n//   // div.list-item-cell.nobreak.center(:style=\"{ width: rowWidth.r1 }\" style=\"padding-left: 3px; padding-right: 3px;\" :class=\"$style.noSelect\" @click.stop) {{ index + 1 }}\n//   // 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}` : '')\")\n//   //   span.select {{ item.name }}\n//   //   span.badge.badge-theme-primary(:class=\"[$style.labelQuality, $style.noSelect]\" v-if=\"item.meta._qualitys.flac32bit\") {{ $t('tag__lossless_24bit') }}\n//   //   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') }}\n//   //   span.badge.badge-theme-secondary(:class=\"[$style.labelQuality, $style.noSelect]\" v-else-if=\"item.meta._qualitys['320k']\") {{ $t('tag__high_quality') }}\n//   //   span.badge.badge-theme-tertiary(:class=\"[$style.labelQuality, $style.noSelect]\" v-if=\"sourceTag\") {{ item.source }}\n//   // div.list-item-cell(:style=\"{ width: rowWidth.r3 }\" :aria-label=\"item.singer\")\n//   //   span.select {{ item.singer }}\n//   // div.list-item-cell(:style=\"{ width: rowWidth.r4 }\" :aria-label=\"item.albumName\")\n//   //   span.select {{ item.meta.albumName }}\n//   // div.list-item-cell(:style=\"{ width: rowWidth.r5 }\")\n//   //   span(:class=\"[$style.time, $style.noSelect]\") {{ item.meta.interval || '--/--' }}\n//   // div.list-item-cell(:style=\"{ width: rowWidth.r6 }\" style=\"padding-left: 0; padding-right: 0;\")\n//   //   material-list-buttons(:index=\"index\" :class=\"$style.btns\"\n//   //       :remove-btn=\"false\" @btn-click=\"handleListBtnClick\"\n//   //       :download-btn=\"assertApiSupport(item.source)\")\n//   const renderListItem = (list: ListItem) => {\n//     const dom_listItem = document.createElement('div')\n//     dom_listItem.className = 'list-item'\n//   }\n\n//   const renderList = (list: ListItem[], type?: 'up' | 'down') => {\n//     if (!list.length) return\n//     console.log(list)\n//     const dom = document.createDocumentFragment()\n//     for (const item of list) {\n//       dom.appendChild(renderListItem(item))\n//     }\n//     switch (type) {\n//       case 'up':\n//         break\n//       case 'down':\n//         break\n//       default:\n//         // console.log()\n//         break\n//     }\n//   }\n\n//   const updateView = (force = false, currentScrollTop = dom_scrollContainer.value.scrollTop) => {\n//     // const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop\n//     const itemHeight = props.itemHeight\n//     const currentStartIndex = Math.floor(currentScrollTop / itemHeight)\n//     const scrollContainerHeight = dom_scrollContainer.value.clientHeight\n//     const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)\n//     const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex\n//     const currentStartRenderIndex = Math.max(currentStartIndex, 0)\n//     const currentEndRenderIndex = currentEndIndex + 1\n//     // console.log(continuous, currentStartIndex, endIndex, currentEndIndex, startIndex)\n//     // debugger\n//     if (!force && continuous) {\n//       // if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * 0.6) return\n//       // console.log('update')\n//       if (currentScrollTop > scrollTop) { // scroll down\n//         console.log('scroll down')\n//         renderList(createList(endIndex + 1, currentEndRenderIndex))\n//         //   // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))\n//         //   // // if (this.views.length > 100) {\n//         //   // nextTick(() => {\n//         //   //   views.value.splice(0, views.value.indexOf(list[0]))\n//         //   // })\n//         //   // }\n//       } else if (currentScrollTop < scrollTop) { // scroll up\n//         console.log('scroll up')\n//         renderList(createList(currentStartRenderIndex, startIndex))\n//         // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n//       } else return\n//       // if (currentScrollTop == scrollTop && endIndex >= currentEndIndex) return\n//       // views.value = createList(currentStartRenderIndex, currentEndRenderIndex)\n//     } else {\n//       renderList(createList(currentStartRenderIndex, currentEndRenderIndex))\n//     }\n//     startIndex = currentStartIndex\n//     endIndex = currentEndIndex\n//     scrollTop = currentScrollTop\n//   }\n\n//   const onScroll = event => {\n//     const currentScrollTop = dom_scrollContainer.value.scrollTop\n//     if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * 0.6) {\n//       updateView(false, currentScrollTop)\n//     }\n//     emit('scroll', event)\n//   }\n\n//   const scrollTo = async(scrollTop, animate = false) => {\n//     return new Promise(resolve => {\n//       if (cancelScroll) {\n//         cancelScroll(resolve)\n//       } else {\n//         resolve()\n//       }\n//     }).then(async() => {\n//       return new Promise((resolve, reject) => {\n//         if (animate) {\n//           isScrolling = true\n//           scrollToValue = scrollTop\n//           cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {\n//             cancelScroll = null\n//             isScrolling = false\n//             resolve()\n//           }, () => {\n//             cancelScroll = null\n//             isScrolling = false\n//             reject('canceled')\n//           })\n//         } else {\n//           dom_scrollContainer.value.scrollTop = scrollTop\n//         }\n//       })\n//     })\n//   }\n\n//   const scrollToIndex = async(index, offset = 0, animate = false) => {\n//     return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)\n//   }\n\n//   const getScrollTop = () => {\n//     return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop\n//   }\n\n//   const handleResize = () => {\n//     setTimeout(updateView)\n//   }\n\n//   const contentStyle = computed(() => ({\n//     display: 'block',\n//     height: props.list.length * props.itemHeight + 'px',\n//   }))\n\n//   const handleReset = list => {\n//     cachedList = Array(list.length)\n//     startIndex = -1\n//     endIndex = -1\n//     void nextTick(() => {\n//       updateView(true)\n//     })\n//   }\n//   watch(() => props.itemHeight, () => {\n//     handleReset(props.list)\n//   })\n//   watch(() => props.list, (list) => {\n//     handleReset(list)\n//   }, {\n//     deep: true,\n//   })\n\n//   onMounted(() => {\n//     dom_scrollContainer.value!.addEventListener('scroll', onScroll, false)\n//     cachedList = Array(props.list.length)\n//     startIndex = -1\n//     endIndex = -1\n//     updateView(true)\n//     window.addEventListener('resize', handleResize)\n//   })\n//   onBeforeUnmount(() => {\n//     dom_scrollContainer.value!.removeEventListener('scroll', onScroll)\n//     window.removeEventListener('resize', handleResize)\n//     if (cancelScroll) cancelScroll()\n//   })\n\n//   return {\n//     dom_scrollContainer,\n//     dom_list,\n//     contentStyle,\n//     scrollTo,\n//     scrollToIndex,\n//     getScrollTop,\n//   }\n// }\nexport {}\n"
  },
  {
    "path": "src/renderer/components/common/AudioVisualizer.vue",
    "content": "<template>\n  <div :class=\"$style.content\">\n    <canvas ref=\"dom_canvas\" :class=\"$style.canvas\" />\n  </div>\n</template>\n\n<script>\nimport { ref, onBeforeUnmount, onMounted } from '@common/utils/vueTools'\nimport { getAnalyser } from '@renderer/plugins/player'\nimport { isPlay } from '@renderer/store/player/state'\n// import { appSetting } from '@renderer/store/setting'\n\n// const themes = {\n//   green: 'rgba(77,175,124,.16)',\n//   blue: 'rgba(52,152,219,.16)',\n//   yellow: 'rgba(233,212,96,.22)',\n//   orange: 'rgba(245,171,53,.16)',\n//   red: 'rgba(214,69,65,.12)',\n//   pink: 'rgba(241,130,141,.16)',\n//   purple: 'rgba(155,89,182,.14)',\n//   grey: 'rgba(108,122,137,.16)',\n//   ming: 'rgba(51,110,123,.14)',\n//   blue2: 'rgba(79,98,208,.14)',\n//   black: 'rgba(39,39,39,.4)',\n//   mid_autumn: 'rgba(74,55,82,.1)',\n//   naruto: 'rgba(87,144,167,.15)',\n//   happy_new_year: 'rgba(192,57,43,.1)',\n// }\n\nconst getBarWidth = canvasWidth => {\n  let barWidth = (canvasWidth / 128) * 2.5\n  const width = canvasWidth / 86\n  const diffWidth = barWidth - width\n  // console.log(barWidth - width)\n  // if (barWidth - width > 20) newBarWidth = 20\n  // barWidth = newBarWidth\n  return diffWidth > 32\n    ? canvasWidth / 128 // 4k屏、超宽屏直接显示所有频谱条\n    : diffWidth > 12 ? width : barWidth\n}\nexport default {\n  setup() {\n    const dom_canvas = ref(null)\n    const analyser = getAnalyser()\n\n    let ctx\n    let bufferLength = 0\n    let dataArray\n    let WIDTH\n    let HEIGHT\n    let MAX_HEIGHT\n    let barWidth\n    let barHeight\n    let x = 0\n    let isPlaying = false\n    let animationFrameId\n\n    let num\n    let mult\n    const maxNum = 255\n    let frequencyAvg = 0\n\n    // const theme = useRefGetter('theme')\n    // const setting = useRefGetter('setting')\n    let themeColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary-light-200-alpha-800')\n    // watch(theme, theme => {\n    //   themeColor = themes[theme || 'green']\n    // })\n\n    // https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode/smoothingTimeConstant\n    const renderFrame = () => {\n      x = 0\n\n      analyser.getByteFrequencyData(dataArray)\n\n      ctx.clearRect(0, 0, WIDTH, HEIGHT)\n      // ctx.fillRect(0, 0, WIDTH, HEIGHT)\n      ctx.fillStyle = themeColor\n\n      for (let i = 0; i < bufferLength; i++) {\n        mult = Math.floor(i / maxNum)\n        num = mult % 2 === 0 ? (i - maxNum * mult) : (maxNum - (i - maxNum * mult))\n        let spectrum = num > 90 ? 0 : dataArray[num + 20]\n        frequencyAvg += spectrum * 1.2\n      }\n      frequencyAvg /= bufferLength\n      frequencyAvg *= 1.4\n\n      frequencyAvg = frequencyAvg / maxNum\n      // ctx.scale(1, 1 + frequencyAvg)\n\n      for (let i = 0; i < bufferLength; i++) {\n        if (x > WIDTH) break\n\n        barHeight = dataArray[i]\n\n        // let r = barHeight + (25 * (i / bufferLength))\n        // let g = 250 * (i / bufferLength)\n        // let b = 50\n\n        // ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'\n        barHeight = (barHeight * frequencyAvg + barHeight * 0.42) * MAX_HEIGHT\n        ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight)\n\n        x += barWidth\n      }\n\n      animationFrameId = null\n      if (isPlaying) animationFrameId = window.requestAnimationFrame(renderFrame)\n    }\n\n    const handlePlay = () => {\n      isPlaying = true\n      // analyser.fftSize = 256\n      bufferLength = analyser.frequencyBinCount\n      // console.log(bufferLength)\n      barWidth = getBarWidth(WIDTH)\n      dataArray = new Uint8Array(bufferLength)\n      renderFrame()\n    }\n    const handlePause = () => {\n      if (animationFrameId) window.cancelAnimationFrame(animationFrameId)\n      isPlaying = false\n    }\n\n    const handleResize = () => {\n      const canvas = dom_canvas.value\n      canvas.width = canvas.clientWidth\n      canvas.height = canvas.clientHeight\n      WIDTH = canvas.width\n      HEIGHT = canvas.height\n      MAX_HEIGHT = Math.round(HEIGHT * 0.4 / 255 * 10000) / 10000\n      // console.log(MAX_HEIGHT)\n      barWidth = getBarWidth(WIDTH)\n    }\n\n    window.app_event.on('play', handlePlay)\n    window.app_event.on('pause', handlePause)\n    window.app_event.on('error', handlePause)\n    window.addEventListener('resize', handleResize)\n    onBeforeUnmount(() => {\n      handlePause()\n      window.app_event.off('play', handlePlay)\n      window.app_event.off('pause', handlePause)\n      window.app_event.off('error', handlePause)\n      window.removeEventListener('resize', handleResize)\n    })\n\n    onMounted(() => {\n      const canvas = dom_canvas.value\n      ctx = canvas.getContext('2d')\n      canvas.width = canvas.clientWidth\n      canvas.height = canvas.clientHeight\n      WIDTH = canvas.width\n      HEIGHT = canvas.height\n      MAX_HEIGHT = Math.round(HEIGHT * 0.4 / 255 * 10000) / 10000\n      // console.log(MAX_HEIGHT)\n      if (isPlay.value) handlePlay()\n    })\n\n    return {\n      dom_canvas,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n.content {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  z-index: 100;\n}\n.canvas {\n  width: 100%;\n  height: 100%;\n  // opacity: 0.1;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/DownloadModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" :bg-close=\"bgClose\" :teleport=\"teleport\" @close=\"handleClose\">\n    <main :class=\"$style.main\">\n      <h2>{{ info.name }}<br>{{ info.singer }}</h2>\n      <base-btn v-for=\"quality in qualitys\" :key=\"quality.type\" :class=\"$style.btn\" @click=\"handleClick(quality.type)\">\n        {{ getTypeName(quality.type) }}{{ quality.size && ` - ${quality.size.toUpperCase()}` }}\n      </base-btn>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { qualityList } from '@renderer/store'\nimport { createDownloadTasks } from '@renderer/store/download/action'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicInfo: {\n      type: [Object, null],\n      required: true,\n    },\n    listId: {\n      type: String,\n      default: '',\n    },\n    bgClose: {\n      type: Boolean,\n      default: true,\n    },\n    teleport: {\n      type: String,\n      default: '#root',\n    },\n  },\n  emits: ['update:show'],\n  setup() {\n    return {\n      qualityList,\n    }\n  },\n  computed: {\n    info() {\n      return this.musicInfo || {}\n    },\n    sourceQualityList() {\n      return this.qualityList[this.musicInfo.source] || []\n    },\n    qualitys() {\n      return this.info.meta?.qualitys?.filter(quality => this.checkSource(quality.type)) || []\n    },\n  },\n  methods: {\n    handleClick(quality) {\n      void createDownloadTasks([this.musicInfo], quality, this.listId)\n      this.handleClose()\n    },\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    getTypeName(quality) {\n      switch (quality) {\n        case 'flac24bit':\n          return this.$t('download__lossless') + ' FLAC Hires'\n        case 'flac':\n        case 'ape':\n        case 'wav':\n          return this.$t('download__lossless') + ' ' + quality.toUpperCase()\n        case '320k':\n          return this.$t('download__high_quality') + ' ' + quality.toUpperCase()\n        case '192k':\n        case '128k':\n          return this.$t('download__normal') + ' ' + quality.toUpperCase()\n      }\n    },\n    checkSource(quality) {\n      return this.sourceQualityList.includes(quality)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px;\n  max-width: 400px;\n  min-width: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    margin-bottom: 15px;\n  }\n}\n\n.btn {\n  display: block;\n  margin-bottom: 15px;\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/DownloadMultipleModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" :bg-close=\"bgClose\" :teleport=\"teleport\" @close=\"handleClose\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('download__multiple_tip', { len: list.length }) }}<br>{{ $t('download__multiple_tip2') }}</h2>\n      <base-btn :class=\"$style.btn\" @click=\"handleClick('128k')\">{{ $t('download__normal') }} - 128K</base-btn>\n      <base-btn :class=\"$style.btn\" @click=\"handleClick('320k')\">{{ $t('download__high_quality') }} - 320K</base-btn>\n      <base-btn :class=\"$style.btn\" @click=\"handleClick('flac')\">{{ $t('download__lossless') }} - FLAC</base-btn>\n      <base-btn :class=\"$style.btn\" @click=\"handleClick('flac24bit')\">{{ $t('download__lossless') }} - FLAC Hires</base-btn>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { createDownloadTasks } from '@renderer/store/download/action'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    bgClose: {\n      type: Boolean,\n      default: true,\n    },\n    listId: {\n      type: String,\n      default: '',\n    },\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    teleport: {\n      type: String,\n      default: '#root',\n    },\n  },\n  emits: ['update:show', 'confirm'],\n  methods: {\n    handleClick(quality) {\n      void createDownloadTasks(this.list.filter(item => item.source != 'local'), quality, this.listId)\n      this.handleClose()\n      this.$emit('confirm')\n    },\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px;\n  max-width: 400px;\n  min-width: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    margin-bottom: 15px;\n  }\n}\n\n.btn {\n  display: block;\n  margin-bottom: 15px;\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/ListAddModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" :bg-close=\"bgClose\" :teleport=\"teleport\" max-width=\"70%\" min-width=\"200px\" @close=\"handleClose\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('list_add__' + (isMove ? 'title_first_move' : 'title_first_add')) }}&nbsp;<span :class=\"$style.name\">{{ currentMusicInfo.name }}</span>&nbsp;{{ $t('list_add__title_last') }}</h2>\n      <div class=\"scroll\" :class=\"$style.btnContent\">\n        <base-btn v-for=\"(item, index) in lists\" :key=\"item.id\" :class=\"$style.btn\" :aria-label=\"$t('list_add__btn_title', { name: item.name })\" :disabled=\"item.isExist\" @click=\"handleClick(index)\">{{ item.name }}</base-btn>\n        <base-btn :class=\"[$style.btn, $style.newList, isEditing ? $style.editing : null]\" :aria-label=\"$t('lists__new_list_btn')\" @click=\"handleEditing($event)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 42 42\" space=\"preserve\">\n            <use xlink:href=\"#icon-addTo\" />\n          </svg>\n          <base-input :class=\"$style.newListInput\" :value=\"newListName\" :placeholder=\"$t('lists__new_list_input')\" @keyup.enter=\"handleSaveList($event)\" @blur=\"handleSaveList($event)\" />\n        </base-btn>\n        <span v-for=\"i in spaceNum\" :key=\"i\" :class=\"$style.btn\" />\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\n// import { mapMutations } from 'vuex'\nimport { watch, ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/state'\nimport { addListMusics, moveListMusics, createUserList, getMusicExistListIds } from '@renderer/store/list/action'\nimport useKeyDown from '@renderer/utils/compositions/useKeyDown'\nimport { useI18n } from '@root/lang'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicInfo: {\n      type: [Object, null],\n      required: true,\n    },\n    bgClose: {\n      type: Boolean,\n      default: true,\n    },\n    excludeListId: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    // listName: {\n    //   type: String,\n    //   default: '',\n    // },\n    fromListId: {\n      type: String,\n      default: null,\n    },\n    isMove: {\n      type: Boolean,\n      default: false,\n    },\n    teleport: {\n      type: String,\n      default: '#root',\n    },\n  },\n  emits: ['update:show'],\n  setup(props) {\n    const keyModDown = useKeyDown('mod')\n    const t = useI18n()\n    const lists = ref([])\n\n    const currentMusicInfo = ref({})\n\n    const checkMusicExist = (musicInfo) => {\n      const mid = musicInfo.id\n      void getMusicExistListIds(mid).then(ids => {\n        if (mid != musicInfo.id) return\n        for (const list of lists.value) {\n          if (ids.includes(list.id)) list.isExist = true\n        }\n      })\n    }\n\n    let stopWatchUserList = null\n\n    const getList = () => {\n      lists.value = [\n        { ...defaultList, name: t(defaultList.name) },\n        { ...loveList, name: t(loveList.name) },\n        ...userLists,\n      ].filter(l => !props.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: false }))\n      checkMusicExist(currentMusicInfo.value)\n    }\n\n    watch(() => props.show, show => {\n      if (!show) {\n        if (stopWatchUserList) {\n          stopWatchUserList()\n          stopWatchUserList = null\n        }\n        return\n      }\n      if (!props.musicInfo) return lists.value = []\n\n      currentMusicInfo.value = 'progress' in props.musicInfo ? props.musicInfo.metadata.musicInfo : props.musicInfo\n\n      getList()\n\n      stopWatchUserList = watch(userLists, getList)\n    })\n\n    onBeforeUnmount(() => {\n      if (stopWatchUserList) {\n        stopWatchUserList()\n        stopWatchUserList = null\n      }\n    })\n\n    return {\n      keyModDown,\n      lists,\n      checkMusicExist,\n      currentMusicInfo,\n    }\n  },\n  data() {\n    return {\n      isEditing: false,\n      newListName: '',\n      rowNum: 3,\n    }\n  },\n  computed: {\n    spaceNum() {\n      return this.lists.length < 2 ? 0 : (this.rowNum - this.lists.length % this.rowNum - 1)\n    },\n  },\n  mounted() {\n    window.addEventListener('resize', this.handleResize)\n    this.handleResize()\n  },\n  beforeUnmount() {\n    window.removeEventListener('resize', this.handleResize)\n  },\n  methods: {\n    handleResize() {\n      const width = window.innerWidth\n      this.rowNum = width < 1920\n        ? 3\n        : width < 2560\n          ? 4\n          : width < 3840 ? 5 : 6\n    },\n    handleClick(index) {\n      if (this.isMove) void moveListMusics(this.fromListId, this.lists[index].id, [this.currentMusicInfo])\n      else void addListMusics(this.lists[index].id, [this.currentMusicInfo])\n\n      this.lists[index].isExist = true\n      if (this.keyModDown && !this.isMove) return\n      this.$nextTick(() => {\n        this.handleClose()\n      })\n    },\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    handleEditing(event) {\n      if (this.isEditing) return\n      // if (!this.newListName) this.newListName = this.listName\n      this.isEditing = true\n      this.$nextTick(() => event.currentTarget.querySelector('.' + this.$style.newListInput).focus())\n    },\n    async handleSaveList(event) {\n      let name = event.target.value\n      this.newListName = event.target.value = ''\n      this.isEditing = false\n      if (!name || (\n        userLists.some(l => l.name == name) && !(await dialog.confirm(window.i18n.t('list_duplicate_tip'))))\n      ) return\n      void createUserList({ name })\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  // padding: 15px 0;\n  // max-width: 70%;\n  // min-width: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    padding: 15px;\n  }\n}\n\n.name {\n  color: var(--color-primary);\n}\n\n.btnContent {\n  flex: auto;\n  max-height: 100%;\n  padding-right: 15px;\n  display: flex;\n  flex-flow: row wrap;\n  justify-content: space-evenly;\n}\n\n@item-width: (100% / 3);\n.btn {\n  position: relative;\n  box-sizing: border-box;\n  margin-left: 15px;\n  margin-bottom: 15px;\n  height: 36px;\n  line-height: 36px;\n  padding: 0 10px !important;\n  width: calc(@item-width - 15px);\n  min-width: 160px;\n  .mixin-ellipsis-1();\n}\n\n.newList {\n  border: 1px dashed var(--color-primary-font-hover);\n  // background-color: var(--color-main-background);\n  color: var(--color-primary-font-hover);\n  opacity: .7;\n\n  svg {\n    height: 18px;\n    margin-top: 9px;\n  }\n\n  &.editing {\n    opacity: 1;\n\n    svg {\n      display: none;\n    }\n    .newListInput {\n      display: block;\n    }\n  }\n}\n.newListInput {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 34px;\n  line-height: 34px;\n  background: none !important;\n  font-size: 14px;\n  text-align: center;\n  font-family: inherit;\n  box-sizing: border-box;\n  padding: 0 10px;\n  border-radius: 0;\n  display: none;\n}\n\n@item-width2: (100% / 4);\n@media (min-width: 1920px){\n  .btn {\n    width: calc(@item-width2 - 15px);\n  }\n}\n@item-width3: (100% / 5);\n@media (min-width: 2560px){\n  .btn {\n    width: calc(@item-width3 - 15px);\n  }\n}\n@item-width4: (100% / 6);\n@media (min-width: 3840px){\n  .btn {\n    width: calc(@item-width4 - 15px);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/ListAddMultipleModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" :bg-close=\"bgClose\" max-width=\"70%\" :teleport=\"teleport\" @close=\"handleClose\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('list_add__multiple_' + (isMove ? 'title_move' : 'title_add'), { num: musicList.length }) }}</h2>\n      <div class=\"scroll\" :class=\"$style.btnContent\">\n        <base-btn v-for=\"(item, index) in lists\" :key=\"item.id\" :class=\"$style.btn\" :aria-label=\"$t('list_add__multiple_btn_title', { name: item.name })\" @click=\"handleClick(index)\">{{ item.name }}</base-btn>\n        <base-btn :class=\"[$style.btn, $style.newList, isEditing ? $style.editing : null]\" :aria-label=\"$t('lists__new_list_btn')\" @click=\"handleEditing($event)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 42 42\" space=\"preserve\">\n            <use xlink:href=\"#icon-addTo\" />\n          </svg>\n          <base-input :class=\"$style.newListInput\" :value=\"newListName\" :placeholder=\"$t('lists__new_list_input')\" @keyup.enter=\"handleSaveList($event)\" @blur=\"handleSaveList($event)\" />\n        </base-btn>\n        <span v-for=\"i in spaceNum\" :key=\"i\" :class=\"$style.btn\" />\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/state'\nimport { addListMusics, moveListMusics, createUserList } from '@renderer/store/list/action'\nimport useKeyDown from '@renderer/utils/compositions/useKeyDown'\nimport { useI18n } from '@root/lang'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicList: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    bgClose: {\n      type: Boolean,\n      default: true,\n    },\n    excludeListId: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    // listName: {\n    //   type: String,\n    //   default: '',\n    // },\n    fromListId: {\n      type: String,\n      default: null,\n    },\n    isMove: {\n      type: Boolean,\n      default: false,\n    },\n    teleport: {\n      type: String,\n      default: '#root',\n    },\n  },\n  emits: ['update:show', 'confirm'],\n  setup(props) {\n    const keyModDown = useKeyDown('mod')\n    const t = useI18n()\n\n    const lists = computed(() => {\n      return [\n        { ...defaultList, name: t(defaultList.name) },\n        { ...loveList, name: t(loveList.name) },\n        ...userLists,\n      ].filter(l => !props.excludeListId.includes(l.id))\n    })\n    return {\n      keyModDown,\n      lists,\n    }\n  },\n  data() {\n    return {\n      isEditing: false,\n      newListName: '',\n      rowNum: 3,\n    }\n  },\n  computed: {\n\n    spaceNum() {\n      return this.lists.length < 2 ? 0 : (this.rowNum - this.lists.length % this.rowNum - 1)\n    },\n  },\n  mounted() {\n    window.addEventListener('resize', this.handleResize)\n    this.handleResize()\n  },\n  beforeUnmount() {\n    window.removeEventListener('resize', this.handleResize)\n  },\n  methods: {\n    handleResize() {\n      const width = window.innerWidth\n      this.rowNum = width < 1920\n        ? 3\n        : width < 2560\n          ? 4\n          : width < 3840 ? 5 : 6\n    },\n    handleClick(index) {\n      const list = 'progress' in this.musicList[0] ? this.musicList.map(t => t.metadata.musicInfo) : this.musicList\n\n      if (this.isMove) void moveListMusics(this.fromListId, this.lists[index].id, list)\n      else void addListMusics(this.lists[index].id, list)\n\n      if (this.keyModDown && !this.isMove) return\n      this.$nextTick(() => {\n        this.handleClose()\n        this.$emit('confirm')\n      })\n    },\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    handleEditing(event) {\n      if (this.isEditing) return\n      // if (!this.newListName) this.newListName = this.listName\n      this.isEditing = true\n      this.$nextTick(() => event.currentTarget.querySelector('.' + this.$style.newListInput).focus())\n    },\n    async handleSaveList(event) {\n      let name = event.target.value\n      this.newListName = event.target.value = ''\n      this.isEditing = false\n      if (!name || (\n        userLists.some(l => l.name == name) && !(await dialog.confirm(window.i18n.t('list_duplicate_tip'))))\n      ) return\n      void createUserList({ name })\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  // padding: 15px 0;\n  // max-width: 620px;\n  min-width: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    padding: 15px;\n  }\n}\n\n.btnContent {\n  flex: auto;\n  max-height: 100%;\n  padding-right: 15px;\n  display: flex;\n  flex-flow: row wrap;\n  justify-content: space-evenly;\n}\n\n@item-width: (100% / 3);\n.btn {\n  position: relative;\n  box-sizing: border-box;\n  margin-left: 15px;\n  margin-bottom: 15px;\n  height: 36px;\n  line-height: 36px;\n  padding: 0 10px !important;\n  width: calc(@item-width - 15px);\n  min-width: 160px;\n  .mixin-ellipsis-1();\n}\n\n.newList {\n  border: 1px dashed var(--color-primary-font-hover);\n  // background-color: var(--color-main-background);\n  color: var(--color-primary-font-hover);\n  opacity: .7;\n\n  svg {\n    height: 18px;\n    margin-top: 9px;\n  }\n\n  &.editing {\n    opacity: 1;\n\n    svg {\n      display: none;\n    }\n    .newListInput {\n      display: block;\n    }\n  }\n}\n.newListInput {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 34px;\n  line-height: 34px;\n  background: none !important;\n  font-size: 14px;\n  text-align: center;\n  box-sizing: border-box;\n  padding: 0 10px;\n  border-radius: 0;\n  display: none;\n}\n\n@item-width2: (100% / 4);\n@media (min-width: 1920px){\n  .btn {\n    width: calc(@item-width2 - 15px);\n  }\n}\n@item-width3: (100% / 5);\n@media (min-width: 2560px){\n  .btn {\n    width: calc(@item-width3 - 15px);\n  }\n}\n@item-width4: (100% / 6);\n@media (min-width: 3840px){\n  .btn {\n    width: calc(@item-width4 - 15px);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/PlaybackRateBtn.vue",
    "content": "<template>\n  <material-popup-btn :class=\"$style.btnContent\">\n    <button :class=\"[$style.btn, { [$style.active]: playbackRate != 1 }]\" :aria-label=\"`${$t('player__playback_rate')}${playbackRate}x`\">\n      <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use xlink:href=\"#icon-plex\" />\n      </svg>\n    </button>\n    <template #content>\n      <div :class=\"$style.setting\">\n        <div :class=\"$style.info\">\n          <span>{{ playbackRate.toFixed(2) }}x</span>\n          <div :class=\"$style.control\">\n            <base-checkbox\n              id=\"player__playback_preserves_pitch\"\n              :model-value=\"appSetting['player.preservesPitch']\"\n              :label=\"$t('player__playback_preserves_pitch')\"\n              @update:model-value=\"updatePreservesPitch\"\n            />\n            <base-btn min @click=\"handleUpdatePlaybackRate(100)\">{{ $t('player__playback_rate_reset_btn') }}</base-btn>\n          </div>\n        </div>\n        <base-slider-bar :class=\"$style.slider\" :value=\"playbackRate * 100\" :min=\"50\" :max=\"200\" @change=\"handleUpdatePlaybackRate\" />\n      </div>\n    </template>\n  </material-popup-btn>\n</template>\n\n<script setup>\n// import { computed } from '@common/utils/vueTools'\nimport { playbackRate } from '@renderer/store/player/playbackRate'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nconst handleUpdatePlaybackRate = (val) => {\n  window.app_event.setPlaybackRate(Math.round(val) / 100)\n}\n\n\nconst updatePreservesPitch = (enabled) => {\n  updateSetting({ 'player.preservesPitch': enabled })\n}\n\n// const icon = computed(() => {\n//   return playbackRate.value == 0\n//     ? '#icon-volume-off-outline'\n//     : playbackRate.value < 0.3\n//       ? '#icon-volume-low-outline'\n//       : playbackRate.value < 0.7\n//         ? '#icon-volume-medium-outline'\n//         : '#icon-volume-high-outline'\n// })\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.btnContent {\n  flex: none;\n  height: 100%;\n}\n\n.btn {\n  position: relative;\n  // color: var(--color-button-font);\n  justify-content: center;\n  align-items: center;\n  transition: color @transition-normal;\n  cursor: pointer;\n  background-color: transparent;\n  border: none;\n  width: 24px;\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 0;\n\n  svg {\n    transition: opacity @transition-fast;\n    opacity: .5;\n    // filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    svg {\n      opacity: .9;\n    }\n  }\n  &:active {\n    svg {\n      opacity: 1;\n    }\n  }\n\n  &.active {\n    svg {\n      color: var(--color-primary);\n      opacity: .8;\n    }\n  }\n}\n\n.setting {\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 2px 3px;\n  gap: 8px;\n  width: 300px;\n}\n\n.info {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  font-size: 13px;\n  span {\n    line-height: 1.2;\n  }\n}\n.control {\n  align-items: center;\n  display: flex;\n  gap: 10px;\n}\n\n.slider {\n  width: 100%;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/ProgressBar.vue",
    "content": "<template>\n  <div :class=\"[$style.progress, className]\">\n    <div :class=\"[$style.progressBar, $style.progressBar2, {[$style.barTransition]: isActiveTransition}]\" :style=\"{ transform: `scaleX(${progress || 0})` }\" @transitionend=\"handleTransitionEnd\" />\n    <div v-show=\"dragging\" :class=\"[$style.progressBar, $style.progressBar3]\" :style=\"{ transform: `scaleX(${dragProgress || 0})` }\" />\n  </div>\n  <div ref=\"dom_progress\" :class=\"$style.progressMask\" @mousedown=\"handleMsDown\" />\n</template>\n\n<script>\nimport { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { playProgress } from '@renderer/store/player/playProgress'\n\nexport default {\n  props: {\n    className: {\n      type: String,\n      default: '',\n    },\n    progress: {\n      type: Number,\n      required: true,\n    },\n    isActiveTransition: {\n      type: Boolean,\n      required: true,\n    },\n    handleTransitionEnd: {\n      type: Function,\n      required: true,\n    },\n  },\n  setup(props) {\n    const msEvent = {\n      isMsDown: false,\n      msDownX: 0,\n      msDownProgress: 0,\n    }\n    const dom_progress = ref(null)\n    const dragging = ref(false)\n    const dragProgress = ref(0)\n\n    const handleMsDown = event => {\n      msEvent.isMsDown = true\n      msEvent.msDownX = event.clientX\n\n      let val = event.offsetX / dom_progress.value.clientWidth\n      if (val < 0) val = 0\n      if (val > 1) val = 1\n\n      dragProgress.value = msEvent.msDownProgress = val\n    }\n    const handleMsUp = () => {\n      if (msEvent.isMsDown) setProgress(dragProgress.value * playProgress.maxPlayTime)\n      msEvent.isMsDown = false\n      dragging.value = false\n    }\n    const handleMsMove = event => {\n      if (!msEvent.isMsDown) return\n      dragging.value ||= true\n\n      let progress = msEvent.msDownProgress + (event.clientX - msEvent.msDownX) / dom_progress.value.clientWidth\n      if (progress > 1) progress = 1\n      else if (progress < 0) progress = 0\n      dragProgress.value = progress\n    }\n\n    document.addEventListener('mousemove', handleMsMove)\n    document.addEventListener('mouseup', handleMsUp)\n    onBeforeUnmount(() => {\n      document.removeEventListener('mousemove', handleMsMove)\n      document.removeEventListener('mouseup', handleMsUp)\n    })\n\n    const setProgress = num => {\n      window.app_event.setProgress(num)\n    }\n\n    // const handleSetProgress = event => {\n    //   // setProgress(event.offsetX / dom_progress.value.clientWidth * playProgress.maxPlayTime)\n    // }\n\n    return {\n      dom_progress,\n      // handleSetProgress,\n      dragging,\n      dragProgress,\n      handleMsDown,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.progress {\n  width: 100%;\n  height: 5px;\n  overflow: hidden;\n  transition: @transition-normal;\n  transition-property: background-color;\n  background-color: var(--color-primary-light-100-alpha-800);\n  // background-color: #f5f5f5;\n  position: relative;\n  border-radius: 40px;\n}\n.progressMask {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  cursor: pointer;\n}\n.progressBar {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  transform-origin: 0;\n}\n.progressBar1 {\n  background-color: var(--color-primary-light-100-alpha-600);\n}\n\n.progressBar2 {\n  background-color: var(--color-primary-light-100-alpha-400);\n  will-change: transform;\n}\n\n.progressBar3 {\n  background-color: var(--color-primary-light-100-alpha-200);\n  box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);\n  opacity: 0.5;\n}\n\n.barTransition {\n  transition-property: transform;\n  transition-timing-function: ease-out;\n  transition-duration: 0.2s;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/AddConvolutionPresetBtn.vue",
    "content": "<template>\n  <base-btn min :disabled=\"disabled\" :class=\"[$style.newPreset, {[$style.editing]: isEditing}]\" :aria-label=\"$t('player__sound_effect_biquad_filter_save_btn')\" @click=\"handleEditing($event)\">\n    <svg-icon name=\"plus\" />\n    <base-input ref=\"input\" :class=\"$style.newPresetInput\" :value=\"newPresetName\" :placeholder=\"$t('player__sound_effect_biquad_filter_save_input')\" @keyup.enter=\"handleSave($event)\" @blur=\"handleSave($event)\" />\n  </base-btn>\n</template>\n\n<script setup>\nimport { ref, nextTick } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\nimport { saveUserConvolutionPreset } from '@renderer/store/soundEffect'\n\ndefineProps({\n  disabled: {\n    type: Boolean,\n    default: false,\n  },\n})\n\nconst isEditing = ref(false)\nconst input = ref(false)\nconst newPresetName = ref('')\n\nconst handleEditing = () => {\n  if (isEditing.value) return\n  // if (!this.newPresetName) this.newPresetName = this.listName\n  isEditing.value = true\n  void nextTick(() => {\n    input.value.$el.focus()\n  })\n}\n\nconst handleSave = (event) => {\n  let name = event.target.value.trim()\n  newPresetName.value = event.target.value = ''\n  isEditing.value = false\n  if (!name) return\n  if (name.length > 20) name = name.substring(0, 20)\n  void saveUserConvolutionPreset({\n    id: Date.now().toString(),\n    name,\n    source: appSetting['player.soundEffect.convolution.fileName'],\n    mainGain: appSetting['player.soundEffect.convolution.mainGain'],\n    sendGain: appSetting['player.soundEffect.convolution.sendGain'],\n  })\n}\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.newPreset {\n  position: relative;\n  border: 1px dashed var(--color-primary-font-hover);\n  // background-color: var(--color-main-background);\n  color: var(--color-primary-font-hover);\n  opacity: .7;\n  height: 22px;\n\n  &.editing {\n    opacity: 1;\n    width: 90px;\n\n    svg {\n      display: none;\n    }\n    .newPresetInput {\n      display: block;\n    }\n  }\n\n  :global {\n    .svg-icon {\n      vertical-align: 0;\n    }\n  }\n}\n.newPresetInput {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  // line-height: 16px;\n  background: none !important;\n  font-size: 12px;\n  text-align: center;\n  font-family: inherit;\n  box-sizing: border-box;\n  padding: 0 3px;\n  border-radius: 0;\n  display: none;\n  &::placeholder {\n    font-size: 12px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/AddEQPresetBtn.vue",
    "content": "<template>\n  <base-btn min :class=\"[$style.newPreset, {[$style.editing]: isEditing}]\" :aria-label=\"$t('player__sound_effect_biquad_filter_save_btn')\" @click=\"handleEditing($event)\">\n    <svg-icon name=\"plus\" />\n    <base-input ref=\"input\" :class=\"$style.newPresetInput\" :value=\"newPresetName\" :placeholder=\"$t('player__sound_effect_biquad_filter_save_input')\" @keyup.enter=\"handleSave($event)\" @blur=\"handleSave($event)\" />\n  </base-btn>\n</template>\n\n<script setup>\nimport { ref, nextTick } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\nimport { saveUserEQPreset } from '@renderer/store/soundEffect'\n\nconst isEditing = ref(false)\nconst input = ref(false)\nconst newPresetName = ref('')\n\nconst handleEditing = () => {\n  if (isEditing.value) return\n  // if (!this.newPresetName) this.newPresetName = this.listName\n  isEditing.value = true\n  void nextTick(() => {\n    input.value.$el.focus()\n  })\n}\n\nconst handleSave = (event) => {\n  let name = event.target.value.trim()\n  newPresetName.value = event.target.value = ''\n  isEditing.value = false\n  if (!name) return\n  if (name.length > 20) name = name.substring(0, 20)\n  void saveUserEQPreset({\n    id: Date.now().toString(),\n    name,\n    hz31: appSetting['player.soundEffect.biquadFilter.hz31'],\n    hz62: appSetting['player.soundEffect.biquadFilter.hz62'],\n    hz125: appSetting['player.soundEffect.biquadFilter.hz125'],\n    hz250: appSetting['player.soundEffect.biquadFilter.hz250'],\n    hz500: appSetting['player.soundEffect.biquadFilter.hz500'],\n    hz1000: appSetting['player.soundEffect.biquadFilter.hz1000'],\n    hz2000: appSetting['player.soundEffect.biquadFilter.hz2000'],\n    hz4000: appSetting['player.soundEffect.biquadFilter.hz4000'],\n    hz8000: appSetting['player.soundEffect.biquadFilter.hz8000'],\n    hz16000: appSetting['player.soundEffect.biquadFilter.hz16000'],\n  })\n}\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.newPreset {\n  position: relative;\n  border: 1px dashed var(--color-primary-font-hover);\n  // background-color: var(--color-main-background);\n  color: var(--color-primary-font-hover);\n  opacity: .7;\n  height: 22px;\n\n  &.editing {\n    opacity: 1;\n    width: 90px;\n\n    svg {\n      display: none;\n    }\n    .newPresetInput {\n      display: block;\n    }\n  }\n\n  :global {\n    .svg-icon {\n      vertical-align: 0;\n    }\n  }\n}\n.newPresetInput {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  // line-height: 16px;\n  background: none !important;\n  font-size: 12px;\n  text-align: center;\n  font-family: inherit;\n  box-sizing: border-box;\n  padding: 0 3px;\n  border-radius: 0;\n  display: none;\n  &::placeholder {\n    font-size: 12px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/AudioConvolution.vue",
    "content": "<template>\n  <div :class=\"$style.contnet\">\n    <h3 class=\"player__sound_effect_title\">{{ $t('player__sound_effect_convolution') }}</h3>\n    <div :class=\"$style.convolution\">\n      <div :class=\"$style.convolutionList\">\n        <base-checkbox\n          v-for=\"item in convolutions\"\n          :id=\"`player__convolution_${item.name}`\"\n          :key=\"item.name\"\n          :class=\"$style.checkbox\"\n          :model-value=\"appSetting['player.soundEffect.convolution.fileName']\"\n          :label=\"$t(`player__sound_effect_convolution_file_${item.name}`)\"\n          :value=\"item.source\"\n          @update:model-value=\"updateConvolution($event)\"\n        />\n      </div>\n      <div :class=\"[$style.sliderList, { [$style.disabled]: disabledConvolution }]\">\n        <div :class=\"$style.sliderItem\">\n          <span :class=\"$style.label\">{{ $t('player__sound_effect_convolution_main_gain') }}</span>\n          <base-slider-bar :class=\"$style.slider\" :value=\"appSetting['player.soundEffect.convolution.mainGain']\" :min=\"0\" :max=\"50\" :disabled=\"disabledConvolution\" @change=\"handleUpdateMainGain\" />\n          <span :class=\"[$style.value]\">{{ appSetting['player.soundEffect.convolution.mainGain'] * 10 }}%</span>\n        </div>\n        <div :class=\"$style.sliderItem\">\n          <span :class=\"$style.label\">{{ $t('player__sound_effect_convolution_send_gain') }}</span>\n          <base-slider-bar :class=\"$style.slider\" :value=\"appSetting['player.soundEffect.convolution.sendGain']\" :min=\"0\" :max=\"50\" :disabled=\"disabledConvolution\" @change=\"handleUpdateSendGain\" />\n          <span :class=\"[$style.value]\">{{ appSetting['player.soundEffect.convolution.sendGain'] * 10 }}%</span>\n        </div>\n      </div>\n    </div>\n    <div :class=\"$style.saveList\">\n      <base-btn v-for=\"item in userPresetList\" :key=\"item.id\" min @click=\"handleSetPreset(item)\" @contextmenu=\"handleRemovePreset(item.id)\">{{ item.name }}</base-btn>\n      <AddConvolutionPresetBtn v-if=\"userPresetList.length < 31\" :disabled=\"disabledConvolution\" />\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, computed } from '@common/utils/vueTools'\nimport { appSetting, saveMediaDeviceId, updateSetting } from '@renderer/store/setting'\nimport { convolutions, setMediaDeviceId } from '@renderer/plugins/player'\nimport AddConvolutionPresetBtn from './AddConvolutionPresetBtn.vue'\nimport { getUserConvolutionPresetList, removeUserConvolutionPreset } from '@renderer/store/soundEffect'\n\nconst updateConvolution = async val => {\n  if (appSetting['player.mediaDeviceId'] != 'default') {\n    await setMediaDeviceId('default').catch(_ => _)\n    saveMediaDeviceId('default')\n  }\n  const target = convolutions.find(c => c.source == val)\n  const setting = {\n    'player.soundEffect.convolution.fileName': val,\n  }\n  if (target) {\n    setting['player.soundEffect.convolution.mainGain'] = target.mainGain * 10\n    setting['player.soundEffect.convolution.sendGain'] = target.sendGain * 10\n  }\n  updateSetting(setting)\n}\n\nconst handleUpdateMainGain = (value) => {\n  updateSetting({ 'player.soundEffect.convolution.mainGain': Math.round(value) })\n}\nconst handleUpdateSendGain = (value) => {\n  updateSetting({ 'player.soundEffect.convolution.sendGain': Math.round(value) })\n}\n\nconst handleSetPreset = (item) => {\n  if (appSetting['player.mediaDeviceId'] != 'default') saveMediaDeviceId('default')\n  updateSetting({\n    'player.soundEffect.convolution.fileName': item.source,\n    'player.soundEffect.convolution.mainGain': item.mainGain,\n    'player.soundEffect.convolution.sendGain': item.sendGain,\n  })\n}\nconst userPresetList = ref([])\nconst handleRemovePreset = id => {\n  void removeUserConvolutionPreset(id)\n}\n\nconst disabledConvolution = computed(() => {\n  return !appSetting['player.soundEffect.convolution.fileName']\n})\n\nonMounted(() => {\n  void getUserConvolutionPresetList().then(list => {\n    userPresetList.value = list\n  })\n})\n\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.contnet {\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 3px;\n  min-height: 0;\n  flex: none;\n}\n.convolution {\n  display: flex;\n  flex-flow: column wrap;\n  gap: 15px;\n  width: 100%;\n}\n.convolutionList {\n  display: flex;\n  flex-flow: row wrap;\n  gap: 8px;\n  width: 100%;\n}\n.checkbox {\n  margin-right: 10px;\n  font-size: 12px;\n}\n\n.sliderList {\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 15px;\n  width: 100%;\n  transition: opacity @transition-normal;\n  &.disabled  {\n    opacity: .4;\n  }\n}\n.sliderItem {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 8px;\n}\n.slider {\n  flex: auto;\n}\n.label {\n  flex: none;\n  // width: 50px;\n  font-size: 12px;\n}\n.value {\n  flex: none;\n  width: 40px;\n  font-size: 12px;\n  text-align: center;\n\n  &.active {\n    color: var(--color-primary-font);\n  }\n}\n.saveList {\n  display: flex;\n  flex-flow: row wrap;\n  margin-top: 10px;\n  gap: 10px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/AudioPanner.vue",
    "content": "<template>\n  <div :class=\"$style.contnet\">\n    <div class=\"player__sound_effect_title\" :class=\"$style.header\">\n      <h3>{{ $t('player__sound_effect_panner') }}</h3>\n      <base-checkbox\n        id=\"player__sound_effect_panner_enabled\"\n        :class=\"$style.checkbox\"\n        :label=\"$t('player__sound_effect_panner_enabled')\"\n        :model-value=\"appSetting['player.soundEffect.panner.enable']\"\n        @update:model-value=\"updateEnabled\"\n      />\n    </div>\n    <div :class=\"$style.eqList\">\n      <div :class=\"$style.eqItem\">\n        <span :class=\"$style.label\">{{ $t('player__sound_effect_panner_sound_speed') }}</span>\n        <base-slider-bar :class=\"$style.slider\" :value=\"appSetting['player.soundEffect.panner.speed']\" :min=\"1\" :max=\"50\" @change=\"handleUpdateSpeed\" />\n        <span :class=\"[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.speed'] != 25 }]\">{{ appSetting['player.soundEffect.panner.speed'] }}</span>\n      </div>\n      <div :class=\"$style.eqItem\">\n        <span :class=\"$style.label\">{{ $t('player__sound_effect_panner_sound_r') }}</span>\n        <base-slider-bar :class=\"$style.slider\" :value=\"appSetting['player.soundEffect.panner.soundR']\" :min=\"1\" :max=\"30\" @change=\"handleUpdateSoundR\" />\n        <span :class=\"[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.soundR'] != 5 }]\">{{ appSetting['player.soundEffect.panner.soundR'] }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\n// import { reactive } from '@common/utils/vueTools'\nimport { setMediaDeviceId } from '@renderer/plugins/player'\nimport { appSetting, saveMediaDeviceId, updateSetting } from '@renderer/store/setting'\n\n// const setting = reactive({\n//   enabled: false,\n//   soundR: 5,\n//   speed: 25,\n// })\n\nconst updateEnabled = async(enabled) => {\n  // console.log(enabled)\n  if (appSetting['player.mediaDeviceId'] != 'default') {\n    await setMediaDeviceId('default').catch(_ => _)\n    saveMediaDeviceId('default')\n  }\n  updateSetting({ 'player.soundEffect.panner.enable': enabled })\n}\n\nconst handleUpdateSoundR = (value) => {\n  updateSetting({ 'player.soundEffect.panner.soundR': Math.round(value) })\n}\nconst handleUpdateSpeed = (value) => {\n  updateSetting({ 'player.soundEffect.panner.speed': Math.round(value) })\n}\n\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.contnet {\n  padding-top: 15px;\n  position: relative;\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 8px;\n  &:before {\n    .mixin-after();\n    position: absolute;\n    top: 0;\n    height: 1px;\n    width: 100%;\n    border-top: 1px dashed var(--color-primary-light-100-alpha-700);\n  }\n}\n.header {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  padding-bottom: 5px;\n  // padding-top: 5px;\n}\n.eqList {\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 15px;\n  width: 100%;\n}\n.eqItem {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 8px;\n}\n.label {\n  flex: none;\n  // width: 50px;\n  font-size: 12px;\n}\n.value {\n  flex: none;\n  width: 40px;\n  font-size: 12px;\n  text-align: center;\n\n  &.active {\n    color: var(--color-primary-font);\n  }\n}\n\n.footer {\n  display: flex;\n  flex-flow: row nowrap;\n  // justify-content: space-between;\n  justify-content: center;\n  align-items: center;\n  // font-size: 13px;\n  span {\n    line-height: 1.2;\n  }\n}\n\n.slider {\n  flex: auto;\n}\n\n.checkbox {\n  margin-right: 10px;\n  font-size: 13px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue",
    "content": "<template>\n  <div :class=\"$style.contnet\">\n    <div class=\"player__sound_effect_title\" :class=\"$style.header\">\n      <h3>{{ $t('player__sound_effect_biquad_filter') }}</h3>\n      <base-btn min @click=\"handleReset\">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>\n    </div>\n    <div :class=\"$style.eqList\">\n      <div v-for=\"(v, i) in freqs\" :key=\"v\" :class=\"$style.eqItem\">\n        <span :class=\"$style.label\">{{ labels[i] }}</span>\n        <base-slider-bar :class=\"$style.slider\" :value=\"appSetting[`player.soundEffect.biquadFilter.hz${v}`]\" :min=\"-15\" :max=\"15\" @change=\"handleUpdate(v, $event)\" />\n        <span :class=\"$style.value\">{{ appSetting[`player.soundEffect.biquadFilter.hz${v}`] }}db</span>\n      </div>\n    </div>\n    <div :class=\"$style.saveList\">\n      <!-- <base-btn min @click=\"handleSetPreset(item)\">{{ $t(`player__sound_effect_biquad_filter_preset_slow`) }}</base-btn> -->\n      <base-btn v-for=\"item in freqsPreset\" :key=\"item.name\" min @click=\"handleSetPreset(item)\">{{ $t(`player__sound_effect_biquad_filter_preset_${item.name}`) }}</base-btn>\n      <base-btn v-for=\"item in userPresetList\" :key=\"item.id\" min @click=\"handleSetPreset(item)\" @contextmenu=\"handleRemovePreset(item.id)\">{{ item.name }}</base-btn>\n      <AddEQPresetBtn v-if=\"userPresetList.length < 31\" />\n    </div>\n    <!-- <div :class=\"$style.footer\">\n      <base-btn min @click=\"handleReset\">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>\n    </div> -->\n  </div>\n</template>\n\n<script setup>\nimport { onMounted, ref } from '@common/utils/vueTools'\nimport { freqs, freqsPreset, setMediaDeviceId } from '@renderer/plugins/player'\nimport { appSetting, saveMediaDeviceId, updateSetting } from '@renderer/store/setting'\nimport AddEQPresetBtn from './AddEQPresetBtn.vue'\nimport { getUserEQPresetList, removeUserEQPreset } from '@renderer/store/soundEffect'\n\nconst labels = freqs.map(num => num < 1000 ? num : `${num / 1000}k`)\n\nconst handleUpdate = async(key, value) => {\n  if (appSetting['player.mediaDeviceId'] != 'default') {\n    await setMediaDeviceId('default').catch(_ => _)\n    saveMediaDeviceId('default')\n  }\n\n  value = Math.round(value)\n  // values[index] = value\n  updateSetting({ [`player.soundEffect.biquadFilter.hz${key}`]: value })\n  // console.log(index, event.target.value, bfs)\n}\n\nconst handleReset = () => {\n  const setting = {}\n  for (const key of freqs) {\n    setting[`player.soundEffect.biquadFilter.hz${key}`] = 0\n  }\n  updateSetting(setting)\n}\n\nconst handleSetPreset = (item) => {\n  updateSetting({\n    'player.soundEffect.biquadFilter.hz31': item.hz31,\n    'player.soundEffect.biquadFilter.hz62': item.hz62,\n    'player.soundEffect.biquadFilter.hz125': item.hz125,\n    'player.soundEffect.biquadFilter.hz250': item.hz250,\n    'player.soundEffect.biquadFilter.hz500': item.hz500,\n    'player.soundEffect.biquadFilter.hz1000': item.hz1000,\n    'player.soundEffect.biquadFilter.hz2000': item.hz2000,\n    'player.soundEffect.biquadFilter.hz4000': item.hz4000,\n    'player.soundEffect.biquadFilter.hz8000': item.hz8000,\n    'player.soundEffect.biquadFilter.hz16000': item.hz16000,\n  })\n}\n\nconst userPresetList = ref([])\n\nconst handleRemovePreset = id => {\n  void removeUserEQPreset(id)\n}\n\nonMounted(() => {\n  void getUserEQPresetList().then(list => {\n    userPresetList.value = list\n  })\n})\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.contnet {\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 8px;\n  min-height: 0;\n  flex: none;\n}\n.header {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  padding-bottom: 5px;\n  // padding-top: 5px;\n}\n.eqList {\n  display: flex;\n  flex-flow: row wrap;\n  // gap: 15px;\n  width: 100%;\n  justify-content: space-between;\n  position: relative;\n\n  &:before {\n    .mixin-after();\n    position: absolute;\n    left: 50%;\n    height: 100%;\n    border-left: 1px dashed var(--color-primary-light-100-alpha-700);\n  }\n}\n.eqItem {\n  display: flex;\n  flex-flow: row nowrap;\n  width: 50%;\n  gap: 8px;\n  margin-bottom: 15px;\n  box-sizing: border-box;\n  &:nth-child(odd) {\n    padding-right: 10px;\n  }\n  &:nth-child(even) {\n    padding-left: 10px;\n  }\n  &:nth-last-child(1), &:nth-last-child(2) {\n    margin-bottom: 0;\n  }\n}\n.label {\n  flex: none;\n  width: 40px;\n  font-size: 12px;\n  text-align: center;\n}\n.value {\n  flex: none;\n  width: 40px;\n  font-size: 12px;\n  text-align: center;\n}\n.footer {\n  display: flex;\n  flex-flow: row nowrap;\n  // justify-content: space-between;\n  justify-content: center;\n  align-items: center;\n  // font-size: 13px;\n  span {\n    line-height: 1.2;\n  }\n}\n\n.slider {\n  flex: auto;\n}\n\n.saveList {\n  display: flex;\n  flex-flow: row wrap;\n  margin-top: 10px;\n  gap: 10px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/PitchShifter.vue",
    "content": "<template>\n  <div :class=\"$style.contnet\">\n    <div class=\"player__sound_effect_title\" :class=\"$style.header\">\n      <h3>\n        {{ $t('player__sound_effect_pitch_shifter') }}\n        <svg-icon class=\"help-icon\" name=\"information-slab-circle-outline\" :aria-label=\"$t('player__sound_effect_pitch_shifter_tip')\" />\n      </h3>\n      <base-btn min @click=\"handleSetPreset(1)\">{{ $t('player__sound_effect_pitch_shifter_reset_btn') }}</base-btn>\n    </div>\n    <div :class=\"$style.eqList\">\n      <div :class=\"$style.eqItem\">\n        <span :class=\"$style.label\">{{ playbackRate.toFixed(2) }}x</span>\n        <base-slider-bar :class=\"$style.slider\" :value=\"playbackRate * 100\" :min=\"50\" :max=\"150\" @change=\"handleUpdatePlaybackRate\" />\n      </div>\n    </div>\n    <!-- <div :class=\"$style.saveList\">\n      <base-btn v-for=\"num in semitones\" :key=\"num\" min @click=\"handleSetSemitones(num)\">{{ $t(`player__sound_effect_pitch_shifter_preset_semitones`, { num: num > 0 ? `+${num}` : num }) }}</base-btn>\n      <base-btn v-for=\"item in userPresetList\" :key=\"item.id\" min @click=\"handleSetPreset(item.playbackRate)\" @contextmenu=\"handleRemovePreset(item.id)\">{{ item.name }}</base-btn>\n      <AddPitchShifterPresetBtn v-if=\"userPresetList.length < 31\" />\n    </div> -->\n  </div>\n</template>\n\n<script setup>\nimport { computed } from '@common/utils/vueTools'\nimport { setMediaDeviceId } from '@renderer/plugins/player'\nimport { appSetting, saveMediaDeviceId, updateSetting } from '@renderer/store/setting'\n// import AddPitchShifterPresetBtn from './AddPitchShifterPresetBtn.vue'\n// import { getUserPitchShifterPresetList, removeUserPitchShifterPreset } from '@renderer/store/soundEffect'\n// import { semitones } from '@renderer/plugins/player'\n\n// const setting = reactive({\n//   enabled: false,\n//   soundR: 5,\n//   speed: 25,\n// })\n\n\nconst playbackRate = computed(() => appSetting['player.soundEffect.pitchShifter.playbackRate'])\n\nconst handleSetPreset = async(value) => {\n  if (appSetting['player.mediaDeviceId'] != 'default') {\n    await setMediaDeviceId('default').catch(_ => _)\n    saveMediaDeviceId('default')\n  }\n  updateSetting({ 'player.soundEffect.pitchShifter.playbackRate': value })\n}\n\n// const handleSetSemitones = (value) => {\n//   // https://zpl.fi/pitch-shifting-in-web-audio-api/\n//   handleSetPreset(2 ** (value / 12))\n// }\n\nconst handleUpdatePlaybackRate = (value) => {\n  value = parseFloat((Math.round(value) / 100).toFixed(2))\n  void handleSetPreset(value)\n}\n\n\n// const userPresetList = ref([])\n\n// const handleRemovePreset = id => {\n//   removeUserPitchShifterPreset(id)\n// }\n\n// onMounted(() => {\n//   getUserPitchShifterPresetList().then(list => {\n//     userPresetList.value = list\n//   })\n// })\n\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.contnet {\n  padding-top: 15px;\n  position: relative;\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 8px;\n  min-height: 0;\n  flex: none;\n  &:before {\n    .mixin-after();\n    position: absolute;\n    top: 0;\n    height: 1px;\n    width: 100%;\n    border-top: 1px dashed var(--color-primary-light-100-alpha-700);\n  }\n}\n.header {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  padding-bottom: 5px;\n  // padding-top: 5px;\n}\n.eqList {\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 15px;\n  width: 100%;\n}\n.eqItem {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 8px;\n}\n.label {\n  flex: none;\n  // width: 50px;\n  font-size: 12px;\n}\n.value {\n  flex: none;\n  width: 40px;\n  font-size: 12px;\n  text-align: center;\n\n  &.active {\n    color: var(--color-primary-font);\n  }\n}\n\n.footer {\n  display: flex;\n  flex-flow: row nowrap;\n  // justify-content: space-between;\n  justify-content: center;\n  align-items: center;\n  // font-size: 13px;\n  span {\n    line-height: 1.2;\n  }\n}\n\n.slider {\n  flex: auto;\n}\n\n.checkbox {\n  margin-right: 10px;\n  font-size: 13px;\n}\n\n.saveList {\n  display: flex;\n  flex-flow: row wrap;\n  margin-top: 10px;\n  gap: 10px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SoundEffectBtn/index.vue",
    "content": "<template>\n  <button :class=\"$style.btn\" :aria-label=\"$t('player__sound_effect')\" @click=\"visible = true\">\n    <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"90%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n      <use xlink:href=\"#icon-tune-variant\" />\n    </svg>\n  </button>\n  <material-modal :show=\"visible\" bg-close=\"bg-close\" :teleport=\"teleport\" @close=\"visible = false\">\n    <!-- <main :class=\"$style.main\"> -->\n    <!-- <h2 :class=\"$style.title\">{{ $t('theme_edit_modal__title') }}</h2> -->\n    <div :class=\"$style.content\">\n      <div :class=\"['scroll', $style.row]\">\n        <AudioConvolution />\n        <PitchShifter />\n        <AudioPanner />\n      </div>\n      <div :class=\"['scroll', $style.row]\">\n        <BiquadFilter />\n      </div>\n    </div>\n    <p v-if=\"showTip\" :class=\"$style.tip\">{{ $t('player__sound_effect_features_tip') }}</p>\n    <!-- </main> -->\n  </material-modal>\n</template>\n\n<script setup>\nimport { ref, watch } from '@common/utils/vueTools'\n// import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'\n// import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'\n// import { musicInfo, playMusicInfo } from '@renderer/store/player/state'\n// import { saveVolumeIsMute } from '@renderer/store/setting'\n// import { volume, isMute } from '@renderer/store/player/volume'\n// import fs from 'node:fs'\nimport BiquadFilter from './BiquadFilter.vue'\nimport AudioPanner from './AudioPanner.vue'\nimport AudioConvolution from './AudioConvolution.vue'\nimport PitchShifter from './PitchShifter.vue'\nimport { appSetting } from '@renderer/store/setting'\n\ndefineProps({\n  teleport: {\n    type: String,\n    default: '#root',\n  },\n})\n\nconst visible = ref(false)\n\nconst showTip = ref(false)\n\nwatch(visible, (visible) => {\n  if (visible) showTip.value = appSetting['player.mediaDeviceId'] != 'default'\n})\n\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.btn {\n  position: relative;\n  // color: var(--color-button-font);\n  justify-content: center;\n  align-items: center;\n  transition: color @transition-normal;\n  cursor: pointer;\n  background-color: transparent;\n  border: none;\n  width: 24px;\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 0;\n\n  svg {\n    transition: opacity @transition-fast;\n    opacity: .6;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    svg {\n      opacity: .9;\n    }\n  }\n  &:active {\n    svg {\n      opacity: 1;\n    }\n  }\n}\n\n.main {\n  min-width: 300px;\n  // max-height: 100%;\n  // overflow: hidden;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n}\n// .title {\n//   flex: none;\n//   font-size: 16px;\n//   color: var(--color-font);\n//   line-height: 1.3;\n//   text-align: center;\n//   padding: 10px;\n// }\n.content {\n  display: flex;\n  flex-flow: row nowrap;\n  padding: 0 5px;\n  margin: 15px 0;\n  gap: 10px;\n  position: relative;\n  min-height: 0;\n\n  &:before {\n    .mixin-after();\n    position: absolute;\n    left: 50%;\n    height: 100%;\n    border-left: 1px dashed var(--color-primary-light-100-alpha-700);\n  }\n  // width: 400px;\n\n  :global {\n    // .player__sound_effect_contnet {\n    //   display: flex;\n    // }\n    .player__sound_effect_title {\n      // margin-bottom: 10px;\n      font-size: 14px;\n      padding-bottom: 8px;\n    }\n  }\n}\n\n.row {\n  width: 50%;\n  display: flex;\n  gap: 15px;\n  flex-flow: column nowrap;\n  padding: 0 10px;\n}\n\n.tip {\n  padding: 0 15px 15px;\n  margin-top: 5px;\n  font-size: 12px;\n  line-height: 1.25;\n  color: var(--color-font);\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/TogglePlayModeBtn.vue",
    "content": "<template>\n  <material-popup-btn ref=\"btn_ref\" :class=\"$style.btnContent\">\n    <button :class=\"$style.btn\" :aria-label=\"nextTogglePlayName\">\n      <svg\n        v-if=\"appSetting['player.togglePlayMethod'] == 'listLoop'\"\n        version=\"1.1\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        xlink=\"http://www.w3.org/1999/xlink\"\n        height=\"80%\" viewBox=\"0 0 24 24\" space=\"preserve\"\n      >\n        <use xlink:href=\"#icon-list-loop\" />\n      </svg>\n      <svg\n        v-else-if=\"appSetting['player.togglePlayMethod'] == 'random'\"\n        version=\"1.1\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        xlink=\"http://www.w3.org/1999/xlink\"\n        width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\"\n      >\n        <use xlink:href=\"#icon-list-random\" />\n      </svg>\n      <svg\n        v-else-if=\"appSetting['player.togglePlayMethod'] == 'list'\"\n        version=\"1.1\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        xlink=\"http://www.w3.org/1999/xlink\"\n        width=\"100%\" viewBox=\"0 0 32 32\" space=\"preserve\"\n      >\n        <use xlink:href=\"#icon-list-order\" />\n      </svg>\n      <svg\n        v-else-if=\"appSetting['player.togglePlayMethod'] == 'singleLoop'\"\n        version=\"1.1\"\n        xmlns=\"http://www.w3.org/2000/svg\"\n        xlink=\"http://www.w3.org/1999/xlink\"\n        width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\"\n      >\n        <use xlink:href=\"#icon-single-loop\" />\n      </svg>\n      <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 32 32\" space=\"preserve\">\n        <use xlink:href=\"#icon-single\" />\n      </svg>\n    </button>\n    <template #content>\n      <div :class=\"$style.setting\">\n        <button :class=\"$style.btn\" :aria-label=\"$t('player__play_toggle_mode_list_loop')\" @click=\"toggleMode('listLoop')\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-list-loop\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :aria-label=\"$t('player__play_toggle_mode_random')\" @click=\"toggleMode('random')\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-list-random\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :aria-label=\"$t('player__play_toggle_mode_list')\" @click=\"toggleMode('list')\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 32 32\" space=\"preserve\">\n            <use xlink:href=\"#icon-list-order\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :aria-label=\"$t('player__play_toggle_mode_single_loop')\" @click=\"toggleMode('singleLoop')\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-single-loop\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :aria-label=\"$t('player__play_toggle_mode_off')\" @click=\"toggleMode('none')\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 32 32\" space=\"preserve\">\n            <use xlink:href=\"#icon-single\" />\n          </svg>\n        </button>\n      </div>\n    </template>\n  </material-popup-btn>\n</template>\n\n<script setup>\nimport { ref } from '@common/utils/vueTools'\n// import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'\n// import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'\n// import { musicInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { appSetting } from '@renderer/store/setting'\nimport useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'\n\nconst btn_ref = ref(null)\n\nconst {\n  nextTogglePlayName,\n  toggleNextPlayMode,\n} = useNextTogglePlay()\n\nconst toggleMode = (mode) => {\n  btn_ref.value.hide()\n  toggleNextPlayMode(mode)\n}\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.btnContent {\n  flex: none;\n  height: 100%;\n}\n\n.btn {\n  position: relative;\n  // color: var(--color-button-font);\n  justify-content: center;\n  align-items: center;\n  transition: color @transition-normal;\n  cursor: pointer;\n  background-color: transparent;\n  border: none;\n  width: 24px;\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 0;\n\n  svg {\n    transition: opacity @transition-fast;\n    opacity: .6;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    svg {\n      opacity: .9;\n    }\n  }\n  &:active {\n    svg {\n      opacity: 1;\n    }\n  }\n}\n\n.setting {\n  display: flex;\n  flex-flow: row nowrap;\n  font-size: 14px;\n  gap: 10px;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/VolumeBtn.vue",
    "content": "<template>\n  <material-popup-btn :class=\"$style.btnContent\">\n    <button :class=\"$style.btn\" :aria-label=\"isMute ? $t('player__volume_muted') : `${$t('player__volume')}${parseInt(volume * 100)}%`\" @wheel=\"handleWheel\">\n      <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use :xlink:href=\"icon\" />\n      </svg>\n    </button>\n    <template #content>\n      <div :class=\"$style.setting\">\n        <div :class=\"$style.info\">\n          <span>{{ Math.trunc(volume * 100) }}%</span>\n          <base-checkbox\n            id=\"player__volume_mute\"\n            :model-value=\"isMute\"\n            :label=\"$t('player__volume_mute_label')\"\n            @update:model-value=\"saveVolumeIsMute($event)\"\n          />\n        </div>\n        <base-slider-bar :class=\"$style.slider\" :value=\"volume\" :min=\"0\" :max=\"1\" :step=\"0.01\" @change=\"handleUpdateVolume\" />\n      </div>\n    </template>\n  </material-popup-btn>\n</template>\n\n<script setup>\nimport { computed } from '@common/utils/vueTools'\n// import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'\n// import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'\n// import { musicInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { saveVolumeIsMute } from '@renderer/store/setting'\nimport { volume, isMute } from '@renderer/store/player/volume'\n\nconst handleWheel = (event) => {\n  window.app_event.setVolume(Math.round(volume.value * 100 + (-event.deltaY / 100 * 2)) / 100)\n}\n\nconst handleUpdateVolume = (val) => {\n  window.app_event.setVolume(val)\n}\n\nconst icon = computed(() => {\n  return isMute.value\n    ? '#icon-volume-mute-outline'\n    : volume.value == 0\n      ? '#icon-volume-off-outline'\n      : volume.value < 0.3\n        ? '#icon-volume-low-outline'\n        : volume.value < 0.7\n          ? '#icon-volume-medium-outline'\n          : '#icon-volume-high-outline'\n})\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.btnContent {\n  flex: none;\n  height: 100%;\n}\n\n.btn {\n  position: relative;\n  // color: var(--color-button-font);\n  justify-content: center;\n  align-items: center;\n  transition: color @transition-normal;\n  cursor: pointer;\n  background-color: transparent;\n  border: none;\n  width: 24px;\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 0;\n\n  svg {\n    transition: opacity @transition-fast;\n    opacity: .6;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    svg {\n      opacity: .9;\n    }\n  }\n  &:active {\n    svg {\n      opacity: 1;\n    }\n  }\n}\n\n.setting {\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 2px 3px;\n  gap: 8px;\n  width: 140px;\n}\n\n.info {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  font-size: 13px;\n  span {\n    line-height: 1.2;\n  }\n}\n\n.slider {\n  width: 100%;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/index.js",
    "content": "import upperFirst from 'lodash/upperFirst'\nimport camelCase from 'lodash/camelCase'\n\nconst requireComponent = require.context('./', true, /\\.vue$/)\n\nconst vueFileRxp = /\\.vue$/\n\nexport default app => {\n  requireComponent.keys().forEach(fileName => {\n    const filePath = fileName.replace(/^\\.\\//, '')\n\n    if (!filePath.split('/').every((path, index, arr) => {\n      const char = path.charAt(0)\n      return vueFileRxp.test(path) || char.toUpperCase() !== char || arr[index + 1] == 'index.vue'\n    })) return\n\n    const componentConfig = requireComponent(fileName)\n\n    let componentName = upperFirst(camelCase(filePath.replace(/\\.\\w+$/, '')))\n\n    if (componentName.endsWith('Index')) componentName = componentName.replace(/Index$/, '')\n\n    app.component(componentName, componentConfig.default || componentConfig)\n  })\n}\n"
  },
  {
    "path": "src/renderer/components/layout/Aside/ControlBtns.vue",
    "content": "<template>\n  <div v-show=\"!isFullscreen\" ref=\"dom_btns\" :class=\"$style.controlBtn\">\n    <button type=\"button\" :class=\"[$style.btn, $style.close]\" :aria-label=\"$t('close')\" ignore-tip :title=\"$t('close')\" @click=\"closeWindow\">\n      <svg :class=\"$style.controlBtniIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use xlink:href=\"#icon-window-close\" />\n      </svg>\n    </button>\n    <button type=\"button\" :class=\"[$style.btn, $style.min]\" :aria-label=\"$t('min')\" ignore-tip :title=\"$t('min')\" @click=\"minWindow\">\n      <svg :class=\"$style.controlBtniIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use xlink:href=\"#icon-window-minimize\" />\n      </svg>\n    </button>\n  </div>\n</template>\n\n<script setup>\nimport { minWindow, closeWindow } from '@renderer/utils/ipc'\nimport { onMounted, onBeforeUnmount, ref, useCssModule } from '@common/utils/vueTools'\n// import { getRandom } from '../../utils'\nimport { isFullscreen } from '@renderer/store'\n\nconst dom_btns = ref()\n\nconst cssModule = useCssModule()\n\nconst handle_focus = () => {\n  if (!dom_btns.value) return\n  dom_btns.value.classList.remove(cssModule.hover)\n}\nconst handle_mouseenter = () => {\n  dom_btns.value.classList.add(cssModule.hover)\n}\nconst handle_mouseleave = () => {\n  dom_btns.value.classList.remove(cssModule.hover)\n}\n\n\nonMounted(() => {\n  window.app_event.on('focus', handle_focus)\n  dom_btns.value.addEventListener('mouseenter', handle_mouseenter)\n  dom_btns.value.addEventListener('mouseleave', handle_mouseleave)\n})\nonBeforeUnmount(() => {\n  window.app_event.off('focus', handle_focus)\n  dom_btns.value.removeEventListener('mouseenter', handle_mouseenter)\n  dom_btns.value.removeEventListener('mouseleave', handle_mouseleave)\n})\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@control-btn-width: @height-toolbar * .26;\n@control-btn-height: 6%;\n.controlBtn {\n  box-sizing: border-box;\n  padding: 0 7px;\n  display: flex;\n  align-items: center;\n  justify-content: space-evenly;\n  width: 100%;\n  height: @control-btn-height;\n  -webkit-app-region: no-drag;\n  opacity: .5;\n  transition: opacity @transition-normal;\n  &.hover {\n    opacity: .8;\n    .controlBtniIcon {\n      opacity: 1;\n    }\n  }\n\n}\n.btn {\n  position: relative;\n  width: @control-btn-width;\n  height: @control-btn-width;\n  background: none;\n  border: none;\n  display: flex;\n  // justify-content: center;\n  // align-items: center;\n  outline: none;\n  padding: 1px;\n  cursor: pointer;\n  border-radius: 50%;\n  color: var(--color-font);\n\n  &.min {\n    background-color: var(--color-btn-min);\n  }\n  // &.max {\n  //   background-color: var(--color-btn-max);\n  // }\n  &.close {\n    background-color: var(--color-btn-close);\n  }\n}\n\n.controlBtniIcon {\n  opacity: 0;\n  transition: opacity 0.2s ease-in-out;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/Aside/NavBar.vue",
    "content": "<template>\n  <div ref=\"dom_menu\" :class=\"$style.menu\">\n    <ul :class=\"$style.list\" role=\"toolbar\">\n      <li v-for=\"item in menus\" :key=\"item.to\" :class=\"$style.navItem\" role=\"presentation\">\n        <router-link :class=\"[$style.link, {[$style.active]: $route.meta.name == item.name}]\" role=\"tab\" :aria-selected=\"$route.meta.name == item.name\" :to=\"item.to\" :aria-label=\"item.tips\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" :viewBox=\"item.iconSize\" :height=\"item.size\" :width=\"item.size\" space=\"preserve\">\n            <use :xlink:href=\"item.icon\" />\n          </svg>\n        </router-link>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { appSetting } from '@renderer/store/setting'\nimport { useI18n } from '@root/lang'\nimport { ref, computed } from '@common/utils/vueTools'\nimport { useIconSize } from '@renderer/utils/compositions/useIconSize'\n\nexport default {\n  name: 'NavBar',\n  setup() {\n    const t = useI18n()\n    const dom_menu = ref<HTMLElement>()\n    const iconSize = useIconSize(dom_menu, 0.32)\n\n    const menus = computed(() => {\n      const size = iconSize.value\n      return [\n        {\n          to: '/search',\n          tips: t('search'),\n          icon: '#icon-search-2',\n          iconSize: '0 0 425.2 425.2',\n          size,\n          name: 'Search',\n          enable: true,\n        },\n        {\n          to: '/songList/list',\n          tips: t('song_list'),\n          icon: '#icon-album',\n          iconSize: '0 0 425.2 425.2',\n          size,\n          name: 'SongList',\n          enable: true,\n        },\n        {\n          to: '/leaderboard',\n          tips: t('leaderboard'),\n          icon: '#icon-leaderboard',\n          iconSize: '0 0 425.22 425.2',\n          size,\n          name: 'Leaderboard',\n          enable: true,\n        },\n        {\n          to: '/list',\n          tips: t('my_list'),\n          icon: '#icon-love',\n          iconSize: '0 0 444.87 391.18',\n          size,\n          name: 'List',\n          enable: true,\n        },\n        {\n          to: '/download',\n          tips: t('download'),\n          icon: '#icon-download-2',\n          iconSize: '0 0 425.2 425.2',\n          size,\n          enable: appSetting['download.enable'],\n          name: 'Download',\n        },\n        {\n          to: '/setting',\n          tips: t('setting'),\n          icon: '#icon-setting',\n          iconSize: '0 0 493.23 436.47',\n          size,\n          enable: true,\n          name: 'Setting',\n        },\n      ].filter(m => m.enable)\n    })\n    return {\n      appSetting,\n      menus,\n      dom_menu,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.menu {\n  flex: auto;\n  // &.controlBtnLeft {\n  //   display: flex;\n  //   flex-flow: column nowrap;\n  //   justify-content: center;\n  //   padding-bottom: @control-btn-height;\n  // }\n  // padding: 5px;\n}\n.list {\n  -webkit-app-region: no-drag;\n  // margin-bottom: 15px;\n  &:last-child {\n    margin-bottom: 0;\n  }\n  // background-color: pink;\n  // dt {\n  //   padding-left: 5px;\n  //   font-size: 11px;\n  //   transition: @transition-normal;\n  //   transition-property: color;\n  //   color: @color-theme-font-label;\n  //   .mixin-ellipsis-1();\n  // }\n}\n.navItem {\n  position: relative;\n  &:before {\n    content: '';\n    display: block;\n    width: 100%;\n    padding-bottom: 84%;\n  }\n}\n.link {\n  position: absolute;\n  left: 0%;\n  top: 0%;\n  width: 100%;\n  height: 100%;\n  // left: 15%;\n  // top: 15%;\n  // width: 70%;\n  // height: 70%;\n  // display: block;\n  box-sizing: border-box;\n  // text-decoration: none;\n  // border-radius: 20%;\n\n  // padding: 18px 3px;\n  // margin: 5px 0;\n  // border-left: 5px solid transparent;\n  transition: @transition-fast;\n  transition-property: background-color, opacity;\n  color: var(--color-nav-font);\n  cursor: pointer;\n  // font-size: 11.5px;\n  text-align: center;\n  outline: none;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  // border-radius: @radius-border;\n  .mixin-ellipsis-1();\n  &:before {\n    .mixin-after();\n    left: 0;\n    top: 0;\n    width: 3px;\n    height: 100%;\n    background-color: var(--color-primary-dark-200-alpha-700);\n    border-radius: 4px;\n    transform: translateX(-100%);\n    transition: transform @transition-fast;\n  }\n\n  &.active {\n    // border-left-color: @color-theme-active;\n    background-color: var(--color-primary-light-300-alpha-700);\n\n    &:before {\n      transform: translateX(0);\n    }\n\n    &:hover {\n      background-color: var(--color-primary-light-300-alpha-800);\n    }\n  }\n\n\n  &:hover {\n    color: var(--color-nav-font);\n\n    &:not(.active) {\n      opacity: .8;\n      background-color: var(--color-primary-light-400-alpha-700);\n    }\n  }\n  &:active:not(.active) {\n    opacity: .6;\n    background-color: var(--color-primary-light-300-alpha-600);\n  }\n}\n\n// .icon {\n//   // margin-bottom: 5px;\n//   &> svg {\n//     width: 32%;\n//   }\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/Aside/index.vue",
    "content": "<template>\n  <div :class=\"[$style.aside, { [$style.fullscreen]: isFullscreen }]\">\n    <ControlBtns v-if=\"appSetting['common.controlBtnPosition'] == 'left'\" />\n    <div v-else :class=\"$style.logo\">L X</div>\n    <NavBar />\n  </div>\n</template>\n\n<script setup>\nimport { isFullscreen } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\n\nimport ControlBtns from './ControlBtns.vue'\nimport NavBar from './NavBar.vue'\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.aside {\n  // box-shadow: 0 0 5px rgba(0, 0, 0, .3);\n  transition: @transition-normal;\n  transition-property: background-color;\n  // background-color: @color-theme-sidebar;\n  // background-color: @color-aside-background;\n  // border-right: 2px solid var(--color-primary);\n  -webkit-app-region: drag;\n  -webkit-user-select: none;\n  display: flex;\n  flex-flow: column nowrap;\n\n  &.fullscreen {\n    -webkit-app-region: no-drag;\n    .logo {\n      display: none;\n    }\n  }\n}\n\n.logo {\n  box-sizing: border-box;\n  padding: 0 13%;\n  height: 50px;\n  color: var(--color-nav-font);\n  opacity: .8;\n  flex: none;\n  text-align: center;\n  line-height: 50px;\n  font-weight: bold;\n  // -webkit-app-region: no-drag;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/ChangeLogModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"isShowChangeLog\" max-width=\"60%\" @close=\"isShowChangeLog = false\")\n  main(:class=\"$style.main\")\n    h2 当前版本更新日志\n    div.scroll.select(:class=\"$style.info\")\n      div(:class=\"$style.current\")\n        h3 当前版本：{{ versionInfo.version }}\n        template(v-if=\"info.desc\")\n          h3 版本变化：\n          pre(:class=\"$style.desc\" v-text=\"info.desc\")\n      div(v-if=\"info.history.length\" :class=\"[$style.history, $style.desc]\")\n        h3 历史版本：\n        div(v-for=\"(ver, index) in info.history\" :key=\"index\" :class=\"$style.item\")\n          h4 v{{ ver.version }}\n          pre(v-text=\"ver.desc\")\n\n    div(:class=\"$style.footer\")\n      div(:class=\"$style.desc\")\n        p 📢&nbsp;为了减少疑问，我们墙裂建议阅读版本更新日志来了解当前所用版本的变化！\n        p 📢&nbsp;若遇到问题可以阅读\n          strong.hover.underline(aria-label=\"点击打开\" @click=\"openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')\") 桌面版常见问题\n          | 。\n        p(v-if=\"!info.isLatest\") 🚀&nbsp;发现新版本 (v{{ versionInfo.newVersion.version }})！建议去「设置 → 软件更新」更新新版本。\n</template>\n\n<script>\nimport { compareVer } from '@common/utils'\nimport { openUrl, clipboardWriteText } from '@common/utils/electron'\nimport { versionInfo, isShowChangeLog } from '@renderer/store'\nimport { getLastStartInfo } from '@renderer/utils/ipc'\nimport { computed, ref } from '@common/utils/vueTools'\n\nexport default {\n  setup() {\n    const lastStartVersion = ref(null)\n    void getLastStartInfo().then(version => {\n      lastStartVersion.value = version\n    })\n\n    const info = computed(() => {\n      let currentVer = process.versions.app\n      let lastStartVer = lastStartVersion.value\n      let info = {\n        version: '',\n        desc: '',\n        history: [],\n        isLatest: true,\n      }\n      if (!versionInfo.newVersion?.history) return info\n      info.isLatest = compareVer(currentVer, versionInfo.newVersion.version) >= 0\n\n      const history = [{ version: versionInfo.newVersion.version, desc: versionInfo.newVersion.desc }, ...versionInfo.newVersion.history]\n\n      if (lastStartVer) {\n        for (const ver of history) {\n          switch (compareVer(ver.version, currentVer)) {\n            case 0:\n              info.version = ver.version\n              info.desc = ver.desc\n              break\n            case -1:\n              if (compareVer(lastStartVer, ver.version) < 0) info.history.push(ver)\n          }\n        }\n      } else {\n        const verInfo = history.find(v => v.version == currentVer)\n        if (verInfo) {\n          info.version = verInfo.version\n          info.desc = verInfo.desc\n        } else {\n          info.desc = '未找到当前版本的更新日志'\n          info.version = currentVer\n        }\n      }\n\n      return info\n    })\n    return {\n      openUrl,\n      clipboardWriteText,\n      versionInfo,\n      info,\n      isShowChangeLog,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  position: relative;\n  padding: 15px 0;\n  // max-width: 450px;\n  min-width: 300px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  overflow: hidden;\n  // overflow-y: auto;\n  * {\n    box-sizing: border-box;\n  }\n  h2 {\n    flex: 0 0 none;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    margin-bottom: 15px;\n  }\n  h3 {\n    font-size: 14px;\n    line-height: 1.3;\n  }\n  pre {\n    white-space: pre-wrap;\n    text-align: justify;\n    margin-top: 10px;\n  }\n}\n\n.info {\n  flex: 1 1 auto;\n  font-size: 14px;\n  line-height: 1.5;\n  overflow-y: auto;\n  height: 100%;\n  padding: 0 15px;\n}\n.current {\n  > p {\n    padding-left: 15px;\n  }\n}\n\n.desc {\n  h3, h4 {\n    font-weight: bold;\n  }\n  h3 {\n    padding: 5px 0 3px;\n  }\n  ul {\n    list-style: initial;\n    padding-inline-start: 30px;\n  }\n  p {\n    font-size: 14px;\n    line-height: 1.5;\n  }\n}\n\n.history {\n  h3 {\n    padding-top: 15px;\n  }\n\n  .item {\n    h3 {\n      padding: 5px 0 3px;\n    }\n    padding: 0 15px;\n    + .item {\n      padding-top: 15px;\n    }\n    h4 {\n      font-weight: 700;\n    }\n    > p {\n      padding-left: 15px;\n    }\n  }\n\n}\n.footer {\n  flex: 0 0 none;\n  padding: 0 15px;\n  .desc {\n    padding-top: 20px;\n    font-size: 13px;\n    color: var(--color-primary-font);\n    line-height: 1.25;\n\n    p {\n      font-size: 13px;\n      color: var(--color-primary-font);\n      line-height: 1.25;\n    }\n  }\n}\n// .btns {\n//   display: flex;\n//   flex-flow: row nowrap;\n//   gap: 15px;\n// }\n\n// .btn {\n//   margin-top: 10px;\n//   display: block;\n//   width: 100%;\n// }\n// .btn2 {\n//   margin-top: 10px;\n//   display: block;\n//   width: 50%;\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/Icons.vue",
    "content": "<template>\n  <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" style=\"display: none;\" aria-hidden=\"true\">\n    <defs>\n      <g id=\"icon-search\" fill=\"currentColor\">\n        <!-- 30.239 30.239-->\n        <path d=\"M20.194,3.46c-4.613-4.613-12.121-4.613-16.734,0c-4.612,4.614-4.612,12.121,0,16.735c4.108,4.107,10.506,4.547,15.116,1.34c0.097,0.459,0.319,0.897,0.676,1.254l6.718,6.718c0.979,0.977,2.561,0.977,3.535,0c0.978-0.978,0.978-2.56,0-3.535l-6.718-6.72c-0.355-0.354-0.794-0.577-1.253-0.674C24.743,13.967,24.303,7.57,20.194,3.46zM18.073,18.074c-3.444,3.444-9.049,3.444-12.492,0c-3.442-3.444-3.442-9.048,0-12.492c3.443-3.443,9.048-3.443,12.492,0C21.517,9.026,21.517,14.63,18.073,18.074z\" />\n      </g>\n      <g id=\"icon-download\" fill=\"currentColor\">\n        <!-- 0 0 475.078 475.077-->\n        <path d=\"M467.083,318.627c-5.324-5.328-11.8-7.994-19.41-7.994H315.195l-38.828,38.827c-11.04,10.657-23.982,15.988-38.828,15.988c-14.843,0-27.789-5.324-38.828-15.988l-38.543-38.827H27.408c-7.612,0-14.083,2.669-19.414,7.994C2.664,323.955,0,330.427,0,338.044v91.358c0,7.614,2.664,14.085,7.994,19.414c5.33,5.328,11.801,7.99,19.414,7.99h420.266c7.61,0,14.086-2.662,19.41-7.99c5.332-5.329,7.994-11.8,7.994-19.414v-91.358C475.078,330.427,472.416,323.955,467.083,318.627zM360.025,414.841c-3.621,3.617-7.905,5.424-12.854,5.424s-9.227-1.807-12.847-5.424c-3.614-3.617-5.421-7.898-5.421-12.844c0-4.948,1.807-9.236,5.421-12.847c3.62-3.62,7.898-5.431,12.847-5.431s9.232,1.811,12.854,5.431c3.613,3.61,5.421,7.898,5.421,12.847C365.446,406.942,363.638,411.224,360.025,414.841z M433.109,414.841c-3.614,3.617-7.898,5.424-12.848,5.424c-4.948,0-9.229-1.807-12.847-5.424c-3.613-3.617-5.42-7.898-5.42-12.844c0-4.948,1.807-9.236,5.42-12.847c3.617-3.62,7.898-5.431,12.847-5.431c4.949,0,9.233,1.811,12.848,5.431c3.617,3.61,5.427,7.898,5.427,12.847C438.536,406.942,436.729,411.224,433.109,414.841z\" />\n        <path d=\"M224.692,323.479c3.428,3.613,7.71,5.421,12.847,5.421c5.141,0,9.418-1.808,12.847-5.421l127.907-127.908c5.899-5.519,7.234-12.182,3.997-19.986c-3.23-7.421-8.847-11.132-16.844-11.136h-73.091V36.543c0-4.948-1.811-9.231-5.421-12.847c-3.62-3.617-7.901-5.426-12.847-5.426h-73.096c-4.946,0-9.229,1.809-12.847,5.426c-3.615,3.616-5.424,7.898-5.424,12.847V164.45h-73.089c-7.998,0-13.61,3.715-16.846,11.136c-3.234,7.801-1.903,14.467,3.999,19.986L224.692,323.479z\" />\n      </g>\n      <g id=\"icon-testPlay\" fill=\"currentColor\">\n        <path d=\"M62.743,155.437v98.42c0,5.867,4.741,10.605,10.605,10.605c5.854,0,10.605-4.738,10.605-10.605v-98.42c0-5.856-4.751-10.605-10.605-10.605C67.484,144.832,62.743,149.576,62.743,155.437z\" />\n        <path d=\"M29.456,264.582h23.351v-116.85c0.064-0.56,0.166-1.119,0.166-1.693c0-50.412,40.69-91.42,90.698-91.42c50.002,0,90.692,41.008,90.692,91.42c0,0.771,0.113,1.518,0.228,2.263v116.28h23.354c16.254,0,29.442-13.64,29.442-30.469v-60.936c0-13.878-8.989-25.57-21.261-29.249c-1.129-66.971-55.608-121.124-122.45-121.124c-66.86,0-121.347,54.158-122.465,121.15C8.956,147.638,0,159.32,0,173.187v60.926C0,250.932,13.187,264.582,29.456,264.582z\" />\n        <path d=\"M203.454,155.437v98.42c0,5.867,4.748,10.605,10.604,10.605s10.604-4.738,10.604-10.605v-98.42c0-5.856-4.748-10.605-10.604-10.605C208.191,144.832,203.454,149.576,203.454,155.437z\" />\n      </g>\n      <g id=\"icon-addTo\" fill=\"currentColor\">\n        <path d=\"M37.059,16H26V4.941C26,2.224,23.718,0,21,0s-5,2.224-5,4.941V16H4.941C2.224,16,0,18.282,0,21s2.224,5,4.941,5H16v11.059C16,39.776,18.282,42,21,42s5-2.224,5-4.941V26h11.059C39.776,26,42,23.718,42,21S39.776,16,37.059,16z\" />\n      </g>\n      <g id=\"icon-delete\" fill=\"currentColor\">\n        <!-- 0 0 212.982 212.982-->\n        <path d=\"M131.804,106.491l75.936-75.936c6.99-6.99,6.99-18.323,0-25.312c-6.99-6.99-18.322-6.99-25.312,0l-75.937,75.937L30.554,5.242c-6.99-6.99-18.322-6.99-25.312,0c-6.989,6.99-6.989,18.323,0,25.312l75.937,75.936L5.242,182.427c-6.989,6.99-6.989,18.323,0,25.312c6.99,6.99,18.322,6.99,25.312,0l75.937-75.937l75.937,75.937c6.989,6.99,18.322,6.99,25.312,0c6.99-6.99,6.99-18.322,0-25.312L131.804,106.491z\" />\n      </g>\n      <g id=\"icon-left\" fill=\"currentColor\">\n        <!-- 451.847 451.847-->\n        <path d=\"M97.141,225.92c0-8.095,3.091-16.192,9.259-22.366L300.689,9.27c12.359-12.359,32.397-12.359,44.751,0c12.354,12.354,12.354,32.388,0,44.748L173.525,225.92l171.903,171.909c12.354,12.354,12.354,32.391,0,44.744c-12.354,12.365-32.386,12.365-44.745,0l-194.29-194.281C100.226,242.115,97.141,234.018,97.141,225.92z\" />\n      </g>\n      <g id=\"icon-right\" fill=\"currentColor\">\n        <!-- 451.846 451.847-->\n        <path d=\"M345.441,248.292L151.154,442.573c-12.359,12.365-32.397,12.365-44.75,0c-12.354-12.354-12.354-32.391,0-44.744L278.318,225.92L106.409,54.017c-12.354-12.359-12.354-32.394,0-44.748c12.354-12.359,32.391-12.359,44.75,0l194.287,194.284c6.177,6.18,9.262,14.271,9.262,22.366C354.708,234.018,351.617,242.115,345.441,248.292z\" />\n      </g>\n      <g id=\"icon-first\" fill=\"currentColor\">\n        <!-- 454.522 454.522-->\n        <path d=\"M248.299,399.167c12.354,12.354,12.354,32.391,0,44.744c-12.354,12.365-32.391,12.365-44.75,0L9.259,249.63C3.085,243.453,0,235.355,0,227.258c0-8.095,3.091-16.192,9.259-22.366l194.29-194.284c12.359-12.359,32.396-12.359,44.75,0c12.354,12.354,12.354,32.388,0,44.748L76.391,227.258L248.299,399.167z M273.349,227.258L445.258,55.355c12.354-12.359,12.354-32.394,0-44.748c-12.354-12.359-32.392-12.359-44.751,0L206.218,204.892c-6.174,6.18-9.26,14.271-9.26,22.366c0,8.098,3.092,16.195,9.26,22.372l194.289,194.281c12.359,12.365,32.397,12.365,44.751,0c12.354-12.354,12.354-32.391,0-44.744L273.349,227.258z\" />\n      </g>\n      <g id=\"icon-last\" fill=\"currentColor\">\n        <!-- 454.52 454.52-->\n        <path d=\"M378.135,227.256L206.224,55.354c-12.354-12.359-12.354-32.394,0-44.748c12.354-12.359,32.388-12.359,44.747,0L445.258,204.89c6.177,6.18,9.262,14.271,9.262,22.366c0,8.098-3.091,16.195-9.262,22.372L250.971,443.91c-12.359,12.365-32.394,12.365-44.747,0c-12.354-12.354-12.354-32.391,0-44.744L378.135,227.256z M9.265,399.166c-12.354,12.354-12.354,32.391,0,44.744c12.354,12.365,32.382,12.365,44.748,0l194.287-194.281c6.177-6.177,9.257-14.274,9.257-22.372c0-8.095-3.086-16.192-9.257-22.366L54.013,10.606c-12.365-12.359-32.394-12.359-44.748,0c-12.354,12.354-12.354,32.388,0,44.748L181.18,227.256L9.265,399.166z\" />\n      </g>\n      <g id=\"icon-check-true\" fill=\"currentColor\">\n        <!-- 0 32 448 448-->\n        <path d=\"M400 480H48c-26.51 0-48-21.49-48-48V80c0-26.51 21.49-48 48-48h352c26.51 0 48 21.49 48 48v352c0 26.51-21.49 48-48 48zm-204.686-98.059l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.248-16.379-6.249-22.628 0L184 302.745l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.25 16.379 6.25 22.628.001z\" />\n      </g>\n      <g id=\"icon-check-indeterminate\" fill=\"currentColor\">\n        <!-- 0 0 448 512-->\n        <path d=\"M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM92 296c-6.6 0-12-5.4-12-12v-56c0-6.6 5.4-12 12-12h264c6.6 0 12 5.4 12 12v56c0 6.6-5.4 12-12 12H92z\" />\n      </g>\n      <g id=\"icon-logo\" fill=\"currentColor\">\n        <path d=\"M9.891,22.917q0,3.644,7.161,3.646,8.2-.131,8.333-5.859,0.128-4.556-5.729-4.427a24.919,24.919,0,0,0-5.078.781h0.13l-2.865-.912h0a16.1,16.1,0,0,0-1.953,6.771h0Zm8.2-10.156Q22,14.845,25.906,17.188l1.3-2.344q-3.516-1.563-6.9-3.386Q24.732,8.6,24.6,6.25,24.6,3,20.047,2.995a23.248,23.248,0,0,0-4.3.391,14.5,14.5,0,0,1,1.823-2.214L14.839-.26a86.54,86.54,0,0,1-6.51,9.115l2.214,1.563q0.519-.781,1.432-2.083Q13.015,6.9,13.406,6.25A21.552,21.552,0,0,1,19.4,5.078q2.212,0.131,2.474,1.3-0.131,1.563-4.036,3.906L15.88,9.115A22.155,22.155,0,0,1,13.8,8.073l-1.3,1.953q1.431,0.781,2.995,1.563-3.125,1.434-7.422,2.995L9.24,16.927a62.119,62.119,0,0,0,8.854-4.167h0ZM22.651,20.7q0,3.906-5.989,3.906-4.038,0-4.036-2.083a18.068,18.068,0,0,1,.521-2.474,19.643,19.643,0,0,1,6.9-1.432q2.734-.128,2.6,2.083h0ZM5.073,19.922a9.4,9.4,0,0,1,.911-2.344,4.852,4.852,0,0,0,1.042-2.865q0.391-1.562-5.469-5.338L0.125,11.589q4.166,2.475,4.167,3.386a2.727,2.727,0,0,1-.781,1.953,6.678,6.678,0,0,0-1.172,3.125Q2.207,21.1,5.333,25.391l2.214-1.3q-2.606-3.125-2.474-4.167h0ZM4.682,0.521L2.859,2.214Q5.2,4.167,7.417,6.25L9.109,4.557Q6.766,2.6,4.682.521h0ZM37.365,12.24Q37.233,9.9,37.1,8.724q2.212-.391,4.3-0.781l-0.911.781a2.109,2.109,0,0,1,1.3,1.042,1.354,1.354,0,0,1-1.172,1.172q-1.3.522-1.172,1.042-0.131.781,2.6,2.865l1.3-1.3a2.708,2.708,0,0,1-1.3-1.432q0.259-1.172.911-1.172a1.19,1.19,0,0,0,1.693-.651,5.468,5.468,0,0,0-2.083-2.474,14.706,14.706,0,0,1,1.693-.26,14.121,14.121,0,0,1,1.563-.13q0.128,2.734.13,6.771h2.083v-6.9q1.822-.128,3.516-0.13l-1.172,1.3a2.109,2.109,0,0,1,1.3,1.042,1.125,1.125,0,0,1-.911,1.172A1.572,1.572,0,0,0,49.6,11.719q0.128,1.172,2.864,2.734l1.172-1.432a1.923,1.923,0,0,1-1.432-1.562q-0.131-1.172,1.432-.911a1.18,1.18,0,0,0,1.042-1.172q0-.909-2.344-2.214,4.947,0,4.688,1.693a15.6,15.6,0,0,1-.781,4.427l2.474,0.781q0.65-.781.912-4.3V9.115Q59.89,4.69,53.9,4.948a57.07,57.07,0,0,0-5.859.26V3.255h9.245V0.911q-6.9.262-20.052,0V3.385Q41.4,3.257,45.7,3.255c0,0.521.043,1.217,0.13,2.083A63.272,63.272,0,0,0,36.974,6.51V5.99L34.63,6.38a67.9,67.9,0,0,1,.781,8.2l2.214-.26a13.278,13.278,0,0,0-.26-2.083h0ZM48.432,26.432q9.113,0.391,8.724-6.771,0.128-4.556-5.469-4.427a94.959,94.959,0,0,0-14.193,1.432l0.651,2.344a57.233,57.233,0,0,1,12.891-1.562q3.644-.128,3.646,1.823-7.944.781-15.625,0.781V22.4q1.172-.128,3.906-0.26,7.159-.519,11.589-0.651-0.522,2.866-5.859,2.995-6.381,0-11.2-.391l-0.13,2.083q6.119,0.259,11.068.26h0ZM4.292,54.037q0.259,5.34,8.073,5.6,11.719-.262,11.979-7.812,0.26-5.469-6.641-5.469a33.081,33.081,0,0,0-9.115,1.3c0-.173.043-0.26,0.13-0.26L6.245,46.224a15.689,15.689,0,0,0-1.953,7.813h0ZM7.417,38.542q1.172,2.084,2.214,4.427Q4.811,43.359.125,43.49l0.26,2.474q13.541-1.172,25.391-1.953c0.781-.085,1.345-0.13,1.693-0.13l-0.13-2.474q-4.819.522-9.766,0.912,1.563-2.863,2.6-4.687a10.907,10.907,0,0,1,1.953-.13,17.99,17.99,0,0,1,1.823-.13l-0.26-2.344q-4.037.391-8.2,0.781a24.974,24.974,0,0,1-3.516-3.385l-1.953,1.432a10.99,10.99,0,0,1,1.823,1.563,2.825,2.825,0,0,1,.521.651q-4.559.391-9.245,0.521l0.26,2.474a30.239,30.239,0,0,1,4.036-.521h0Zm6.51,13.021q-2.344,0-6.641-.13a2.111,2.111,0,0,1,.13-0.521V50.521A43.444,43.444,0,0,1,17.443,48.7q4.425-.128,4.3,2.995-0.391,5.341-9.115,5.6-5.341-.26-5.729-3.386v-0.13q6.119,0,11.589.391l0.26-2.6a44.171,44.171,0,0,1-4.818,0h0ZM10.932,40.1q-0.653-1.172-1.042-1.823,3.775-.128,7.552-0.521-0.131.263-.521,0.912a23.766,23.766,0,0,1-1.953,3.385l1.042,0.391a24.38,24.38,0,0,0-2.865.26q-2.084.131-3.125,0.26l1.823-.911q-0.262-.65-0.911-1.953h0ZM38.146,37.76a1.017,1.017,0,0,1,.391-0.13q-2.216,5.469-2.344,7.813Q35.93,48.7,39.839,48.7a48.348,48.348,0,0,0,6.25-.391q0.259,3.516.26,8.073a1.328,1.328,0,0,1-1.432,1.693,11.872,11.872,0,0,1-3.776-.651L40.75,59.766a15.948,15.948,0,0,0,4.818.781q4.166,0.128,3.646-4.3,0-.65-0.13-2.734Q48.821,50,48.693,48.047a37.613,37.613,0,0,0,9.115-1.823L56.766,43.75a36.612,36.612,0,0,1-8.2,1.693q-0.262-4.034-.391-4.948l-2.734.391a43.768,43.768,0,0,1,.521,4.427V45.7q-3,.263-5.6.26-1.563-.128-1.562-1.3a27.8,27.8,0,0,1,2.734-7.552,133.766,133.766,0,0,1,14.063-1.823l-0.521-2.474q-5.209.912-13.672,1.823-2.734.391-4.167,0.521l0.521,2.734a0.532,0.532,0,0,1,.391-0.13h0Zm3.906,13.8-2.083-1.953a72.922,72.922,0,0,1-5.6,6.771L36.583,58.2q2.863-3.775,5.469-6.641h0ZM53.51,49.74l-1.953,1.953a57.424,57.424,0,0,1,6.25,5.859l1.823-2.083q-3-2.734-6.12-5.729h0Zm17.448,8.073q6.119-.781,9.245-1.042-0.391.781-1.042,1.953a5.6,5.6,0,0,0-.521,1.042l2.344,1.042a77.847,77.847,0,0,0,6.51-19.661h0.521q3.644-.391,3.255,2.995a31.071,31.071,0,0,1-1.562,10.938q-0.781,2.216-2.083,2.214A6.934,6.934,0,0,1,84.5,56.51l-0.651,2.214a8.911,8.911,0,0,0,4.037.911q3.125-.131,4.427-3.646A55.043,55.043,0,0,0,93.875,43.88q0.519-5.728-5.859-5.469A46.133,46.133,0,0,0,89.057,33.2l-2.474-.651q-0.131.912-.391,2.734Q85.8,37.5,85.542,38.542q-2.606.391-4.687,0.911l0.391,2.6q1.953-.391,3.776-0.781a73.334,73.334,0,0,1-4.427,14.583l-0.13-1.432a4.879,4.879,0,0,1-1.562.26q0.519-6.641.521-13.932,0.128-5.859-4.557-5.729a9.856,9.856,0,0,0-3.646.781V35.287l-2.6.13q0.391,7.812.651,20.573H68.094a7.74,7.74,0,0,1-1.172.13l0.13,2.214q1.3-.128,3.906-0.521h0ZM74.6,48.958q-2.344-.391-3.255-0.651l-0.13-3.776a8.078,8.078,0,0,0,1.3.261q1.822,0.262,2.865.521l0.521-2.344q-0.522-.128-1.432-0.26-2.084-.259-3.255-0.521V38.542A6.905,6.905,0,0,1,74.6,37.5q2.6-.259,2.344,4.167,0,6.122-.13,8.464-0.131,2.734-.391,4.948-0.781.131-2.474,0.26-1.563.262-2.474,0.391,0-2.6-.13-5.078,2.344,0.391,4.167.781l0.26-2.344a4,4,0,0,0-1.172-.13h0Zm32.813-10.677q3.124-.65,4.687-0.911,0.26,2.606.391,4.948-5.209.391-10.547,0.521l0.13,2.344q5.337-.259,10.547-0.651,0,0.781.13,2.474a15.467,15.467,0,0,0,.13,2.344q-3.255.131-9.114,0.391-2.475.131-3.516,0.13l0.13,2.474q3.126-.259,12.5-0.781,0.129,1.953.131,3.646,0.39,3.387-4.037,3.125l0.13,2.474q7.291,0.128,6.641-5.729V51.432q3.254-.128,8.724-0.391c1.387-.085,2.344-0.13,2.865-0.13l-0.131-2.474q-2.216.262-8.073,0.521-2.343.131-3.515,0.26a32.747,32.747,0,0,0-.261-4.3q0.26,1.953,0-.651,4.688-.259,8.594-0.651-3.906.391,0.651,0l-0.26-2.344q-0.781.131-2.474,0.26-4.428.391-6.641,0.521a43.486,43.486,0,0,1-.521-5.078q4.3-.781,8.724-1.432l-0.521-2.474q-7.812,1.825-20.7,3.516l0.521,2.6q1.563-.391,4.688-0.912h0Z\" />\n      </g>\n      <g id=\"icon-play\" fill=\"currentColor\">\n        <!-- 0 0 1024 1024-->\n        <path d=\"M209.962 21.763c-88.986-51.043-161.129-9.228-161.129 93.323v756.778c0 102.653 72.144 144.414 161.129 93.419l661.462-379.344c89.016-51.061 89.016-133.789 0-184.838l-661.462-379.338z\" />\n      </g>\n      <g id=\"icon-pause\" fill=\"currentColor\">\n        <!-- 0 0 1024 1024-->\n        <path d=\"M52.504 168.606v686.806c0 93.13 61.701 168.588 137.82 168.588 76.127 0 137.865-75.462 137.865-168.588v-686.806c0-93.085-61.738-168.577-137.865-168.577-76.119-0.030-137.82 75.492-137.82 168.577z\" />\n        <path d=\"M833.635 0c-76.112 0-137.813 75.492-137.813 168.577v686.806c0 93.13 61.701 168.558 137.813 168.558s137.861-75.433 137.861-168.558v-686.776c-0.033-93.085-61.749-168.606-137.861-168.606z\" />\n      </g>\n      <g id=\"icon-prevMusic\" fill=\"currentColor\">\n        <!-- 0 0 1024 1024-->\n        <path d=\"M96.902 152.172c-53.489 0-96.898 64.037-96.898 143.005v433.651c0 78.944 43.432 143 96.898 143 53.545 0 96.902-64.056 96.902-143v-119.627l352.203 201.97c67.023 38.471 121.477 8.351 123.795-67l-225.149-129.123c-44.415-25.451-69.917-63.036-69.917-103.083 0-40.084 25.502-77.637 69.917-103.134l225.149-129.072c-2.318-75.295-56.772-105.452-123.795-66.986l-352.203 201.919v-119.515c0.023-78.995-43.358-143.005-96.902-143.005z\" />\n        <path d=\"M502.256 583.092l397.712 228.079c68.475 39.291 124.032 7.103 124.032-71.883v-454.684c0-78.94-55.557-111.137-124.032-71.832l-397.74 228.019c-68.452 39.301-68.452 103.009 0.028 142.3z\" />\n      </g>\n      <g id=\"icon-nextMusic\" fill=\"currentColor\">\n        <!-- 0 0 1024 1024-->\n        <path d=\"M927.098 871.828c53.489 0 96.898-64.037 96.898-143.005v-433.651c0-78.944-43.432-143-96.898-143-53.545 0-96.902 64.056-96.902 143v119.627l-352.203-201.97c-67.023-38.471-121.477-8.351-123.795 67l225.149 129.123c44.415 25.451 69.917 63.036 69.917 103.083 0 40.084-25.502 77.637-69.917 103.134l-225.149 129.072c2.318 75.295 56.772 105.452 123.795 66.986l352.203-201.919v119.515c-0.023 78.995 43.358 143.005 96.902 143.005z\" />\n        <path d=\"M521.744 440.908l-397.712-228.079c-68.475-39.291-124.032-7.103-124.032 71.883v454.684c0 78.94 55.557 111.137 124.032 71.832l397.74-228.019c68.452-39.301 68.452-103.009-0.028-142.3z\" />\n      </g>\n      <g id=\"icon-sound\" fill=\"currentColor\">\n        <!-- 0 0 291.063 291.064-->\n        <path d=\"M26.512,204.255h18.292l106.48,67.761c12.354,7.855,22.369,2.361,22.369-12.282v-69.397c16.933-8.854,28.501-26.559,28.501-46.983c0-20.425-11.568-38.129-28.501-46.986V31.645c0-14.639-10.18-20.401-22.731-12.873L44.804,82.443H26.512C11.866,82.443,0,94.311,0,108.955v68.789C0,192.387,11.866,204.255,26.512,204.255z\" />\n        <path d=\"M219.791,152.899c-0.818,11.185-4.039,21.758-9.569,31.426c-3.635,6.354-1.43,14.452,4.919,18.087c2.082,1.187,4.34,1.751,6.576,1.751c4.599,0,9.062-2.393,11.517-6.675c7.508-13.138,11.889-27.491,12.986-42.663c1.714-23.397-4.836-46.781-18.455-65.845c-4.256-5.96-12.536-7.332-18.491-3.081c-5.959,4.259-7.337,12.531-3.08,18.491C216.218,118.425,221.055,135.653,219.791,152.899z\" />\n        <path d=\"M290.7,158c3.34-45.736-16.508-89.592-53.097-117.318c-5.841-4.433-14.146-3.27-18.568,2.556c-4.428,5.838-3.283,14.151,2.558,18.568c29.401,22.281,45.355,57.521,42.668,94.252c-2.02,27.636-14.375,53.159-34.787,71.867c-5.396,4.95-5.758,13.339-0.808,18.729c2.609,2.854,6.188,4.298,9.771,4.298c3.194,0,6.41-1.154,8.953-3.484C272.805,224.175,288.184,192.408,290.7,158z\" />\n      </g>\n      <g id=\"icon-sdCard\" fill=\"currentColor\">\n        <!-- 0 0 291.13 291.13-->\n        <path d=\"M 50.981 282.415 c 1.722 -2.372 5.494 -4.293 8.419 -4.293 h 172.331 c 2.92 0 6.695 1.921 8.419 4.293 l 6.339 8.715 h 41.74 V 18.538 C 288.23 8.298 279.929 0 269.698 0 H 48.988 L 2.9 49.632 V 291.13 h 41.744 L 50.981 282.415 Z M 240.348 27.418 c 0 -2.926 2.371 -5.302 5.302 -5.302 h 17.668 c 2.931 0 5.303 2.376 5.303 5.302 v 50.373 c 0 2.928 -2.372 5.302 -5.303 5.302 H 245.65 c -2.931 0 -5.302 -2.375 -5.302 -5.302 V 27.418 Z M 197.929 27.418 c 0 -2.926 2.371 -5.302 5.302 -5.302 h 17.668 c 2.931 0 5.303 2.376 5.303 5.302 v 50.373 c 0 2.928 -2.372 5.302 -5.303 5.302 h -17.668 c -2.931 0 -5.302 -2.375 -5.302 -5.302 V 27.418 Z M 155.509 27.418 c 0 -2.926 2.372 -5.302 5.303 -5.302 h 17.668 c 2.931 0 5.303 2.376 5.303 5.302 v 50.373 c 0 2.928 -2.372 5.302 -5.303 5.302 h -17.668 c -2.931 0 -5.303 -2.375 -5.303 -5.302 V 27.418 Z M 113.088 27.418 c 0 -2.926 2.376 -5.302 5.302 -5.302 h 17.673 c 2.926 0 5.303 2.376 5.303 5.302 v 50.373 c 0 2.928 -2.377 5.302 -5.303 5.302 H 118.39 c -2.926 0 -5.302 -2.375 -5.302 -5.302 V 27.418 Z M 70.668 27.418 c 0 -2.926 2.377 -5.302 5.303 -5.302 h 17.673 c 2.926 0 5.302 2.376 5.302 5.302 v 50.373 c 0 2.928 -2.376 5.302 -5.302 5.302 H 75.971 c -2.926 0 -5.303 -2.375 -5.303 -5.302 V 27.418 Z M 28.25 104.303 V 53.93 c 0 -2.926 2.377 -5.302 5.303 -5.302 h 17.673 c 2.925 0 5.302 2.376 5.302 5.302 v 50.373 c 0 2.928 -2.377 5.303 -5.302 5.303 H 33.552 C 30.626 109.605 28.25 107.231 28.25 104.303 Z\" />\n      </g>\n      <g id=\"icon-musicFolder\" fill=\"currentColor\">\n        <!-- 0 0 247.498 247.498-->\n        <path d=\"M 0 200.188 c 0 14.645 11.871 26.513 26.512 26.513 h 194.475 c 14.639 0 26.512 -11.868 26.512 -26.513 V 86.192 c 0 -14.641 -11.873 -26.512 -26.512 -26.512 H 0 V 200.188 Z M 85.553 156.537 c 4.137 -0.869 8.2 -0.678 11.788 0.373 v -43.856 c 0 -3 2.11 -5.605 5.049 -6.224 l 53.047 -11.19 l 0.011 -0.005 c 0.196 -0.041 0.414 -0.041 0.621 -0.062 c 0.228 -0.021 0.445 -0.07 0.673 -0.07 c 0 0 0 0 0.011 0 c 0.093 0 0.171 0.021 0.259 0.025 c 0.331 0.011 0.663 0.034 0.984 0.102 c 0.201 0.041 0.383 0.111 0.579 0.171 c 0.197 0.062 0.404 0.106 0.601 0.184 c 0.207 0.091 0.404 0.207 0.606 0.318 c 0.155 0.085 0.315 0.155 0.471 0.256 c 0.182 0.117 0.337 0.269 0.508 0.407 c 0.155 0.121 0.311 0.228 0.445 0.362 c 0.146 0.143 0.27 0.311 0.404 0.471 c 0.124 0.153 0.269 0.295 0.383 0.471 c 0.135 0.197 0.238 0.414 0.342 0.621 c 0.088 0.153 0.176 0.295 0.254 0.456 c 0.14 0.323 0.238 0.668 0.326 1.012 c 0.01 0.065 0.041 0.122 0.062 0.186 v 0.006 c 0.042 0.223 0.052 0.46 0.073 0.688 c 0.021 0.202 0.062 0.404 0.062 0.604 c 0 0.005 0 0.005 0 0.01 v 58.148 c 0 0.367 -0.041 0.74 -0.113 1.103 c -0.135 8.196 -7.208 15.866 -17.233 17.999 c -11.319 2.413 -22.113 -3.2 -24.096 -12.521 c -1.983 -9.331 5.6 -18.838 16.93 -21.251 c 4.137 -0.87 8.202 -0.674 11.785 0.373 v -36.001 l -40.317 8.502 v 52.985 c 0 0.373 -0.042 0.74 -0.106 1.098 c -0.143 8.202 -7.216 15.876 -17.233 18.01 c -11.33 2.407 -22.121 -3.2 -24.104 -12.521 C 66.64 168.452 74.223 158.945 85.553 156.537 Z\" />\n      </g>\n      <g id=\"icon-musicFile\" fill=\"currentColor\">\n        <!-- -61 0 512 512-->\n        <path d=\"m295 120.5h86.230469l-111.230469-111.695312v86.53125c0 13.875 11.214844 25.164062 25 25.164062zm0 0\" />\n        <path d=\"m240 346.5c0 8.269531 6.730469 15 15 15s15-6.730469 15-15v-15.25h-15c-7.960938 0-15 6.324219-15 15.25zm0 0\" />\n        <path d=\"m295 150.5c-30.328125 0-55-24.746094-55-55.167969v-95.332031h-185c-30.328125 0-55 24.746094-55 55.167969v401.667969c0 30.417968 24.671875 55.164062 55 55.164062h280c30.328125 0 55-24.746094 55-55.167969v-306.332031zm5 196c0 24.8125-20.1875 45-45 45s-45-20.1875-45-45c0-25.507812 20.53125-45.25 45-45.25h15v-56.144531l-90 22.59375v108.925781c0 24.8125-20.1875 45-45 45s-45-20.1875-45-45c0-25.507812 20.53125-45.25 45-45.25h15v-75.375c0-6.878906 4.675781-12.875 11.347656-14.546875l120-30.125c9.46875-2.382813 18.652344 4.796875 18.652344 14.546875zm0 0\" />\n        <path d=\"m120 376.625c0 8.269531 6.730469 15 15 15s15-6.730469 15-15v-15.25h-15c-7.960938 0-15 6.324219-15 15.25zm0 0\" />\n      </g>\n      <g id=\"icon-down\" fill=\"currentColor\">\n        <!-- 0 0 451.847 451.847-->\n        <path d=\"M225.923,354.706c-8.098,0-16.195-3.092-22.369-9.263L9.27,151.157c-12.359-12.359-12.359-32.397,0-44.751c12.354-12.354,32.388-12.354,44.748,0l171.905,171.915l171.906-171.909c12.359-12.354,32.391-12.354,44.744,0c12.365,12.354,12.365,32.392,0,44.751L248.292,345.449C242.115,351.621,234.018,354.706,225.923,354.706z\" />\n      </g>\n      <g id=\"icon-back\" fill=\"currentColor\">\n        <!-- 0 0 512 512-->\n        <path d=\"M511.563,434.259c-1.728-142.329-124.42-258.242-277.087-263.419V95.999c0-17.645-14.342-31.999-31.974-31.999 c-7.931,0-15.591,3.042-21.524,8.562c0,0-134.828,124.829-173.609,163.755C2.623,241.109,0,248.088,0,255.994 c0,7.906,2.623,14.885,7.369,19.687c38.781,38.915,173.609,163.745,173.609,163.745c5.933,5.521,13.593,8.562,21.524,8.562 c17.631,0,31.974-14.354,31.974-31.999v-74.591c153.479,2.156,255.792,50.603,255.792,95.924c0,5.896,4.767,10.666,10.658,10.666 c0.167,0.021,0.333,0.01,0.416,0c5.891,0,10.658-4.771,10.658-10.666C512,436.259,511.854,435.228,511.563,434.259z\" />\n      </g>\n      <g id=\"icon-refresh\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z\" />\n      </g>\n      <g id=\"icon-eraser\" fill=\"currentColor\">\n        <!-- 0 0 512 512-->\n        <path d=\"M497.941 273.941c18.745-18.745 18.745-49.137 0-67.882l-160-160c-18.745-18.745-49.136-18.746-67.883 0l-256 256c-18.745 18.745-18.745 49.137 0 67.882l96 96A48.004 48.004 0 0 0 144 480h356c6.627 0 12-5.373 12-12v-40c0-6.627-5.373-12-12-12H355.883l142.058-142.059zm-302.627-62.627l137.373 137.373L265.373 416H150.628l-80-80 124.686-124.686z\" />\n      </g>\n      <g id=\"icon-search-2\" fill=\"currentColor\">\n        <!-- 0 0 801.99 811.98-->\n        <path d=\"M308.92,54.32a180,180,0,1,0-3.16,254.6A179.48,179.48,0,0,0,308.92,54.32Zm-31.09,226A140,140,0,1,1,180,40h0a140,140,0,0,1,97.79,240.29Z\" />\n        <path d=\"M418.23,385.7,352,317.7a23,23,0,1,0-32.93,32.12l66.66,68.44A22.9,22.9,0,0,0,404.89,425a22.65,22.65,0,0,0,11.37-4.64,22.92,22.92,0,0,0,8.94-18.47A23.81,23.81,0,0,0,418.23,385.7Z\" />\n      </g>\n      <g id=\"icon-album\" fill=\"currentColor\">\n        <!-- 0 0 739.96 763.59-->\n        <!-- 0 0 425.2 425.2-->\n        <path d=\"M20,371.91a20,20,0,0,1-20-20V20A20,20,0,0,1,20,0H333.91a20,20,0,0,1,0,40H40V351.91A20,20,0,0,1,20,371.91Z\" />\n        <path d=\"M405.2,425.2h-306a20,20,0,0,1-20-20V258a20,20,0,0,1,40,0V385.2h266V122h-214a20,20,0,1,1,0-40h234a20,20,0,0,1,20,20V405.2A20,20,0,0,1,405.2,425.2Z\" />\n        <path d=\"M259,326.69a18,18,0,0,1-18-18V165A18,18,0,0,1,277,165v143.6A18,18,0,0,1,259,326.69Z\" />\n        <path d=\"M317.24,241.35a18,18,0,0,1-12.76-5.29l-58.26-58.26a18,18,0,0,1,25.52-25.52L330,210.55a18,18,0,0,1-12.76,30.81Z\" />\n        <path d=\"M223.08,360.18A53.95,53.95,0,1,1,277,306.23,54,54,0,0,1,223.08,360.18Zm0-71.8a17.85,17.85,0,1,0,17.85,17.85A17.87,17.87,0,0,0,223.08,288.38Z\" />\n      </g>\n      <g id=\"icon-leaderboard\" fill=\"currentColor\">\n        <!-- 0 0 805.65 805.58-->\n        <path d=\"M402.08,379.18H23a23,23,0,0,0,0,46H402.08a23,23,0,0,0,0-46Z\" />\n        <path d=\"M386.61,352.51a23,23,0,0,0,23-23V23a23,23,0,1,0-46,0V329.5A23,23,0,0,0,386.61,352.51Z\" />\n        <path d=\"M212.49,352.51a23,23,0,0,0,23-23V119.86a23,23,0,1,0-46,0V329.5A23,23,0,0,0,212.49,352.51Z\" />\n        <path d=\"M38.37,352.51a23,23,0,0,0,23-23V191.81a23,23,0,1,0-46,0V329.5A23,23,0,0,0,38.37,352.51Z\" />\n      </g>\n      <g id=\"icon-love\" fill=\"currentColor\">\n        <!-- 0 0 830.33 740.22-->\n        <path class=\"cls-1\" d=\"M222.37,391.18a18.8,18.8,0,0,1-10.21-3l-.14-.09c-14.14-7.82-123.14-69.63-181.13-148A159.49,159.49,0,0,1,1.45,124.43c5.48-40,25.61-75,56.68-98.58C93.49-1,135.15-7.09,178.6,8.33a185.39,185.39,0,0,1,43.83,22.9,185.39,185.39,0,0,1,43.83-22.9C309.73-7.08,351.39-1,386.74,25.85c31.07,23.62,51.2,58.63,56.68,98.58A159.48,159.48,0,0,1,414,240.11C355.66,318.88,245.75,381,232.62,388.2a19.23,19.23,0,0,1-10.25,3ZM132.78,40.37c-16.59,0-34.38,4.76-51.8,18C58.42,75.52,43.79,101,39.8,130.12a117.78,117.78,0,0,0,21.78,85.42c47.15,63.67,133,116.43,160.86,132.57C250.33,332,336.15,279.21,383.3,215.54a117.77,117.77,0,0,0,21.78-85.42c-4-29.11-18.62-54.59-41.19-71.75C303.1,12.17,237.78,69.17,235,71.62a18.81,18.81,0,0,1-25.19,0C207.88,69.88,174.07,40.37,132.78,40.37Z\" />\n      </g>\n      <g id=\"icon-download-2\" fill=\"currentColor\">\n        <!-- 0 0 798.85 718.96-->\n        <path d=\"M404.75,425.2H20.44A20.58,20.58,0,0,1,0,404.48V232.37a20.58,20.58,0,0,1,20.44-20.72,20.58,20.58,0,0,1,20.44,20.72V383.77H384.31V232a20.44,20.44,0,1,1,40.89,0V404.48a20.86,20.86,0,0,1-6,14.65A20.3,20.3,0,0,1,404.75,425.2Z\" />\n        <path d=\"M212.6,242a19.63,19.63,0,0,1-19.49-19.75V19.75a19.5,19.5,0,1,1,39,0V222.21A19.63,19.63,0,0,1,212.6,242Z\" />\n        <path d=\"M212.6,273.61a21.39,21.39,0,0,1-14.94-6.05l-80.29-77.44a19.94,19.94,0,0,1-.68-27.93,19.32,19.32,0,0,1,27.56-.69l68.34,65.92,68.34-65.92a19.32,19.32,0,0,1,27.56.69,19.94,19.94,0,0,1-.68,27.93l-80.3,77.45A21.4,21.4,0,0,1,212.6,273.61Zm11.94-34.68,0,0Z\" />\n      </g>\n      <g id=\"icon-setting\" fill=\"currentColor\">\n        <path class=\"cls-1\" d=\"M248.61,314.89a94.65,94.65,0,1,1,94.65-94.65A94.76,94.76,0,0,1,248.61,314.89Zm0-149.53a54.88,54.88,0,1,0,54.88,54.88A54.94,54.94,0,0,0,248.61,165.36Z\" />\n        <path class=\"cls-1\" d=\"M492.33,201.89l-106-183.54A36.8,36.8,0,0,0,354.58,0H142.65a36.81,36.81,0,0,0-31.79,18.35L4.9,201.89a36.81,36.81,0,0,0,0,36.7l106,183.54a36.81,36.81,0,0,0,31.78,18.35H354.58a36.8,36.8,0,0,0,31.78-18.35l106-183.54A36.81,36.81,0,0,0,492.33,201.89ZM146.73,396.7,44.85,220.24,146.73,43.77H350.5L452.38,220.24,350.5,396.7Z\" />\n      </g>\n      <g id=\"icon-list-add\" fill=\"currentColor\">\n        <path d=\"M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z\" />\n      </g>\n      <g id=\"icon-window-hide\" fill=\"currentColor\">\n        <path d=\"M29.994,10.183L15.363,24.812L0.733,10.184c-0.977-0.978-0.977-2.561,0-3.536c0.977-0.977,2.559-0.976,3.536,0l11.095,11.093L26.461,6.647c0.977-0.976,2.559-0.976,3.535,0C30.971,7.624,30.971,9.206,29.994,10.183z\" />\n      </g>\n      <g id=\"icon-window-minimize\" fill=\"currentColor\">\n        <path d=\"M20,14H4V10H20\" />\n      </g>\n      <g id=\"icon-window-minimize-2\" fill=\"currentColor\">\n        <path d=\"M19,13H5V11H19V13Z\" />\n      </g>\n      <g id=\"icon-window-close\" fill=\"currentColor\">\n        <path d=\"M20 6.91L17.09 4L12 9.09L6.91 4L4 6.91L9.09 12L4 17.09L6.91 20L12 14.91L17.09 20L20 17.09L14.91 12L20 6.91Z\" />\n      </g>\n      <g id=\"icon-window-close-2\" fill=\"currentColor\">\n        <path d=\"M13.46,12L19,17.54V19H17.54L12,13.46L6.46,19H5V17.54L10.54,12L5,6.46V5H6.46L12,10.54L17.54,5H19V6.46L13.46,12Z\" />\n      </g>\n      <g id=\"icon-list-loop\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n        <path d=\"M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z\" />\n      </g>\n      <g id=\"icon-single-loop\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n        <path d=\"M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4zm-4-2V9h-1l-2 1v1h1.5v4H13z\" />\n      </g>\n      <g id=\"icon-list-random\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n        <path d=\"M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z\" />\n      </g>\n      <g id=\"icon-list-order\" fill=\"currentColor\">\n        <!-- 0 0 32 32-->\n        <path d=\"M5.221 12.905h18.571v3.095h-18.571zM5.221 6.714h18.571v3.095h-18.571zM5.221 19.095h12.381v3.095h-12.381zM20.697 19.095v9.286l7.738-4.643z\" />\n      </g>\n      <g id=\"icon-single\" fill=\"currentColor\">\n        <path d=\"M26.75 6.683v17.738h-3.762v-9.496c0-1.433 0-2.15 0-2.508s-0.179-0.537-0.538-0.717c-0.179-0.179-0.896-0.179-1.792-0.179h-0.358v-1.971c1.792-0.537 3.225-1.433 4.121-2.867h2.329z\" />\n        <path d=\"M5.25 26.75l15.229-10.75-15.229-10.75z\" />\n      </g>\n      <g id=\"icon-desktop-lyric-on\" fill=\"currentColor\">\n        <path d=\"M162.64,335.554H81.017V152.1H55.143V359.7h107.5V335.554ZM288.807,291.4a117.012,117.012,0,0,0-11.222-20.092,29.568,29.568,0,0,0-13.365-10.562,54.323,54.323,0,0,0,17.225-7.7,50.6,50.6,0,0,0,12.579-12.23,52.253,52.253,0,0,0,7.648-15.724,64.034,64.034,0,0,0,2.573-18.345,67.706,67.706,0,0,0-3.5-22.316,42.831,42.831,0,0,0-11.007-17.233,51.262,51.262,0,0,0-19.227-11.118q-11.722-3.969-28.018-3.971H192.459V359.7H217.9v-92.44h12.008a37.092,37.092,0,0,1,10.078,1.271,24.136,24.136,0,0,1,8.291,4.209,32.591,32.591,0,0,1,7.076,7.941A76.14,76.14,0,0,1,261.79,293.3L289.664,359.7H318.4Zm-13.938-67.106a30.684,30.684,0,0,1-7.862,11.118,33.47,33.47,0,0,1-12.293,6.83,53.247,53.247,0,0,1-16.225,2.3H217.9V175.928H241.92q17.01,0,26.374,8.259t9.363,24.937A38.647,38.647,0,0,1,274.869,224.292ZM456.714,325.229a100.649,100.649,0,0,1-21.156,8.657,84.575,84.575,0,0,1-23.015,3.1q-28.448,0-43.17-20.489t-14.724-60.833a133.161,133.161,0,0,1,4-34.228q4-15.009,11.436-25.413a50.462,50.462,0,0,1,18.083-15.883,51.244,51.244,0,0,1,23.8-5.48,82.427,82.427,0,0,1,23.73,3.256,89.743,89.743,0,0,1,21.013,9.451v-27.8a94.1,94.1,0,0,0-21.442-7.544,112.019,112.019,0,0,0-24.158-2.462q-19.158,0-34.594,7.624a74.755,74.755,0,0,0-26.3,21.68q-10.865,14.056-16.724,34.228T327.632,258.2q0,51.461,21.227,77.748t60.825,26.286a111.234,111.234,0,0,0,47.03-10.324V325.229Z\" />\n      </g>\n      <g id=\"icon-desktop-lyric-off\" fill=\"currentColor\">\n        <path d=\"M162.64,335.554H81.017V152.1H55.143V359.7h107.5V335.554ZM288.807,291.4a117.012,117.012,0,0,0-11.222-20.092,29.568,29.568,0,0,0-13.365-10.562,54.323,54.323,0,0,0,17.225-7.7,50.6,50.6,0,0,0,12.579-12.23,52.253,52.253,0,0,0,7.648-15.724,64.034,64.034,0,0,0,2.573-18.345,67.706,67.706,0,0,0-3.5-22.316,42.831,42.831,0,0,0-11.007-17.233,51.262,51.262,0,0,0-19.227-11.118q-11.722-3.969-28.018-3.971H192.459V359.7H217.9v-92.44h12.008a37.092,37.092,0,0,1,10.078,1.271,24.136,24.136,0,0,1,8.291,4.209,32.591,32.591,0,0,1,7.076,7.941A76.14,76.14,0,0,1,261.79,293.3L289.664,359.7H318.4Zm-13.938-67.106a30.684,30.684,0,0,1-7.862,11.118,33.47,33.47,0,0,1-12.293,6.83,53.247,53.247,0,0,1-16.225,2.3H217.9V175.928H241.92q17.01,0,26.374,8.259t9.363,24.937A38.647,38.647,0,0,1,274.869,224.292ZM456.714,325.229a100.649,100.649,0,0,1-21.156,8.657,84.575,84.575,0,0,1-23.015,3.1q-28.448,0-43.17-20.489t-14.724-60.833a133.161,133.161,0,0,1,4-34.228q4-15.009,11.436-25.413a50.462,50.462,0,0,1,18.083-15.883,51.244,51.244,0,0,1,23.8-5.48,82.427,82.427,0,0,1,23.73,3.256,89.743,89.743,0,0,1,21.013,9.451v-27.8a94.1,94.1,0,0,0-21.442-7.544,112.019,112.019,0,0,0-24.158-2.462q-19.158,0-34.594,7.624a74.755,74.755,0,0,0-26.3,21.68q-10.865,14.056-16.724,34.228T327.632,258.2q0,51.461,21.227,77.748t60.825,26.286a111.234,111.234,0,0,0,47.03-10.324V325.229Z\" />\n        <path d=\"M69.148,97.826l17.7-17.651,333,334-17.7,17.652Z\" />\n      </g>\n      <g id=\"icon-add-2\" fill=\"currentColor\">\n        <path d=\"M256,170s-62.469-76.808-141-24C44.762,222.824,84.909,325.08,256,415c21.339-8.361,44-17,44-17,19-6.392,28.155,20.742,16,26-27.589,11.935,5.974-4.141-60,28C-35.524,313.85,43.993,149.031,95,117c86.8-65.89,162,10,162,10s58.158-60.523,140-23c104.032,58.528,64,161.9,45,196-9.152,15.154-39.559-4.159-32-16,20.34-37.888,45.522-107.349-25-150C314.919,103.92,256,170,256,170Z\" />\n        <path d=\"M383,368c-8.1.01-24.77-.155-40,0-15.713.16-15.282,34.964,0,35,15.1,0.035,40,0,40,0s-0.068,42.8,0,48c0.208,15.961,32.261,15.791,32-1-0.072-4.649,0-47,0-47s38.008-.031,43,0c15.732,0.046,14.947-33.98-1-34-4.884.093-42,0-42,0s-0.053-28.341,0-46c0.046-15.189-32.028-15.512-32,0C383.027,337.74,382.782,365.139,383,368Z\" />\n      </g>\n      <g id=\"icon-thumbs-up\" fill=\"currentColor\">\n        <!-- 0 0 512 512-->\n        <path d=\"M466.27 286.69C475.04 271.84 480 256 480 236.85c0-44.015-37.218-85.58-85.82-85.58H357.7c4.92-12.81 8.85-28.13 8.85-46.54C366.55 31.936 328.86 0 271.28 0c-61.607 0-58.093 94.933-71.76 108.6-22.747 22.747-49.615 66.447-68.76 83.4H32c-17.673 0-32 14.327-32 32v240c0 17.673 14.327 32 32 32h64c14.893 0 27.408-10.174 30.978-23.95 44.509 1.001 75.06 39.94 177.802 39.94 7.22 0 15.22.01 22.22.01 77.117 0 111.986-39.423 112.94-95.33 13.319-18.425 20.299-43.122 17.34-66.99 9.854-18.452 13.664-40.343 8.99-62.99zm-61.75 53.83c12.56 21.13 1.26 49.41-13.94 57.57 7.7 48.78-17.608 65.9-53.12 65.9h-37.82c-71.639 0-118.029-37.82-171.64-37.82V240h10.92c28.36 0 67.98-70.89 94.54-97.46 28.36-28.36 18.91-75.63 37.82-94.54 47.27 0 47.27 32.98 47.27 56.73 0 39.17-28.36 56.72-28.36 94.54h103.99c21.11 0 37.73 18.91 37.82 37.82.09 18.9-12.82 37.81-22.27 37.81 13.489 14.555 16.371 45.236-5.21 65.62zM88 432c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24z\" />\n      </g>\n      <g id=\"icon-close\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\" />\n      </g>\n      <g id=\"icon-comment\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M16 11H8V9H16V11M22 4V16C22 17.11 21.11 18 20 18H13.9L10.2 21.71C10 21.9 9.75 22 9.5 22H9C8.45 22 8 21.55 8 21V18H4C2.9 18 2 17.11 2 16V4C2 2.89 2.9 2 4 2H20C21.11 2 22 2.9 22 4M20 4H4V16H10V19.08L13.08 16H20V4\" />\n      </g>\n      <g id=\"icon-text\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path fill=\"currentColor\" d=\"M21,6V8H3V6H21M3,18H12V16H3V18M3,13H21V11H3V13Z\" />\n      </g>\n      <g id=\"icon-audio-wave\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path fill=\"currentColor\" d=\"M22 12L20 13L19 14L18 13L17 16L16 13L15 21L14 13L13 15L12 13L11 17L10 13L9 22L8 13L7 19L6 13L5 14L4 13L2 12L4 11L5 10L6 11L7 5L8 11L9 2L10 11L11 7L12 11L13 9L14 11L15 3L16 11L17 8L18 11L19 10L20 11L22 12Z\" />\n      </g>\n      <g id=\"icon-font-decrease\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M5.12,14L7.5,7.67L9.87,14M6.5,5L1,19H3.25L4.37,16H10.62L11.75,19H14L8.5,5H6.5M18,17L23,11.93L21.59,10.5L19,13.1V7H17V13.1L14.41,10.5L13,11.93L18,17Z\" />\n      </g>\n      <g id=\"icon-font-increase\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M5.12,14L7.5,7.67L9.87,14M6.5,5L1,19H3.25L4.37,16H10.62L11.75,19H14L8.5,5H6.5M18,7L13,12.07L14.41,13.5L17,10.9V17H19V10.9L21.59,13.5L23,12.07L18,7Z\" />\n      </g>\n    </defs>\n  </svg>\n</template>\n\n"
  },
  {
    "path": "src/renderer/components/layout/PactModal.vue",
    "content": "<template>\n  <material-modal :show=\"!isAgreePact || isShowPact\" max-width=\"70%\" :bg-close=\"isAgreePact\" :close-btn=\"isAgreePact\" @close=\"handleClose(false)\">\n    <main :class=\"$style.main\">\n      <h2>许可协议</h2>\n      <div class=\"select scroll\" :class=\"$style.content\">\n        <template v-if=\"!isAgreePact\"><p><strong>在使用本软件前，你（使用者）需签署本协议才可继续使用！</strong></p><br></template>\n        <p>本项目基于&nbsp;<strong class=\"hover underline\" @click=\"openUrl('http://www.apache.org/licenses/LICENSE-2.0')\">Apache License 2.0</strong>&nbsp;许可证发行。以下协议是对于 Apache License 2.0 的补充，如有冲突，以以下协议为准。</p><br>\n        <p>词语约定：本协议中的“本项目”指 LX Music（洛雪音乐助手）桌面版项目；“使用者”指签署本协议的使用者；“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称；“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。</p><br>\n        <p><strong>一、数据来源</strong></p><br>\n        <p>1.1&nbsp;本项目的各官方平台在线数据来源原理是从其公开服务器中拉取数据（与未登录状态在官方平台 APP 获取的数据相同），经过对数据简单地筛选与合并后进行展示，因此本项目不对数据的合法性、准确性负责。</p><br>\n        <p>1.2&nbsp;本项目本身没有获取某个音频数据的能力，本项目使用的在线音频数据来源来自软件设置内“自定义源”设置所选择的“源”返回的在线链接。例如播放某首歌，本项目所做的只是将希望播放的歌曲名、艺术家等信息传递给“源”，若“源”返回了一个链接，则本项目将认为这就是该歌曲的音频数据而进行使用，至于这是不是正确的音频数据本项目无法校验其准确性，所以使用本项目的过程中可能会出现希望播放的音频与实际播放的音频不对应或者无法播放的问题。</p><br>\n        <p>1.3&nbsp;本项目的非官方平台数据（例如“我的列表”内列表）来自使用者本地系统或者使用者连接的同步服务，本项目不对这些数据的合法性、准确性负责。</p><br>\n        <p><strong>二、版权数据</strong></p><br>\n        <p>2.1&nbsp;使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在&nbsp;<strong>24&nbsp;小时内</strong>&nbsp;清除使用本项目的过程中所产生的版权数据。</p><br>\n        <p><strong>三、音乐平台别名</strong></p><br>\n        <p>3.1&nbsp;本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼，不包含恶意。如果官方音乐平台觉得不妥，可联系本项目更改或移除。</p><br>\n        <p><strong>四、资源使用</strong></p><br>\n        <p>4.1&nbsp;本项目内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系本项目移除。</p><br>\n        <p><strong>五、免责声明</strong></p><br>\n        <p>5.1&nbsp;由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。</p><br>\n        <p><strong>六、使用限制</strong></p><br>\n        <p>6.1&nbsp;本项目完全免费，且开源发布于&nbsp;<span class=\"hover underline\" @click=\"openUrl('https://github.com/lyswhut/lx-music-desktop#readme')\">GitHub</span>&nbsp;面向全世界人用作对技术的学习交流，本项目不对项目内的技术可能存在违反当地法律法规的行为作保证。</p><br>\n        <p>6.2&nbsp;<strong>禁止在违反当地法律法规的情况下使用本项目。</strong>&nbsp;对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。</p><br>\n        <p><strong>七、版权保护</strong></p><br>\n        <p>7.1&nbsp;音乐平台不易，请尊重版权，支持正版。</p><br>\n        <p><strong>八、非商业性质</strong></p><br>\n        <p>8.1&nbsp;本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。</p><br>\n        <p><strong>九、接受协议</strong></p><br>\n        <p>9.1&nbsp;若你使用了本项目，即代表你接受本协议。</p><br>\n        <p><strong>*</strong>&nbsp;若协议更新，恕不另行通知，可到开源地址查看。</p>\n        <p v-if=\"!isAgreePact\"><strong>若你（使用者）接受以上协议，请点击下面的“接受”按钮签署本协议，若不接受，请点击“不接受”后退出软件并清除本软件的所有数据。</strong></p>\n      </div>\n      <div v-if=\"!isAgreePact\" :class=\"$style.btns\">\n        <base-btn :class=\"$style.btn\" @click=\"handleClose(true)\">{{ $t('not_agree') }}</base-btn>\n        <base-btn :class=\"$style.btn\" :disabled=\"!btnEnable\" @click=\"handleClick\">{{ $t('agree') }} {{ timeStr }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { checkUpdate, quitApp } from '@renderer/utils/ipc'\nimport { openUrl } from '@common/utils/electron'\nimport { isShowPact } from '@renderer/store'\nimport { appSetting, saveAgreePact } from '@renderer/store/setting'\nimport { computed } from '@common/utils/vueTools'\n\nexport default {\n  setup() {\n    const isAgreePact = computed(() => appSetting['common.isAgreePact'])\n\n    return {\n      isShowPact,\n      isAgreePact,\n      appSetting,\n    }\n  },\n  data() {\n    return {\n      time: 20,\n    }\n  },\n  computed: {\n    btnEnable() {\n      return this.time == 0\n    },\n    timeStr() {\n      return this.btnEnable ? '' : `(${this.time})`\n    },\n  },\n  watch: {\n    isAgreePact(n) {\n      if (n) return\n      this.time = 5\n      this.startTimeout()\n    },\n  },\n  mounted() {\n    this.$nextTick(() => {\n      if (!this.isAgreePact) {\n        this.startTimeout()\n      }\n    })\n  },\n  methods: {\n    handleClick() {\n      saveAgreePact(true)\n      window.setTimeout(() => {\n        this.$dialog({\n          message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),\n          confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),\n        }).then(() => {\n          checkUpdate()\n        })\n      }, 2e3)\n    },\n    handleClose(isExit) {\n      if (isExit) {\n        quitApp(true)\n        return\n      }\n      isShowPact.value = false\n    },\n    openUrl(url) {\n      void openUrl(url)\n    },\n    startTimeout() {\n      window.setTimeout(() => {\n        if (--this.time > 0) this.startTimeout()\n      }, 1e3)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px 8px 12px;\n  min-width: 200px;\n  min-height: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  h2 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n\n.content {\n  flex: auto;\n  margin: 15px 0;\n  padding: 0 7px;\n  h3 {\n    font-weight: bold;\n    line-height: 2;\n  }\n  p {\n    line-height: 1.5;\n    font-size: 14px;\n    text-align: justify;\n  }\n}\n\n.btns {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n.btn {\n  display: block;\n  width: 48%;\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/ControlBtns.vue",
    "content": "<template>\n  <div :class=\"$style.controlBtn\">\n    <!-- <common-volume-bar /> -->\n    <button :class=\"$style.titleBtn\" :aria-label=\"$t('player__add_music_to')\" @click=\"addMusicTo\">\n      <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"90%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n        <use xlink:href=\"#icon-add-2\" />\n      </svg>\n    </button>\n    <button :class=\"$style.titleBtn\" :aria-label=\"toggleDesktopLyricBtnTitle\" @click=\"toggleDesktopLyric\" @contextmenu=\"toggleLockDesktopLyric\">\n      <svg v-show=\"appSetting['desktopLyric.enable']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n        <use xlink:href=\"#icon-desktop-lyric-on\" />\n      </svg>\n      <svg v-show=\"!appSetting['desktopLyric.enable']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n        <use xlink:href=\"#icon-desktop-lyric-off\" />\n      </svg>\n    </button>\n    <common-volume-btn />\n    <common-toggle-play-mode-btn />\n    <common-list-add-modal v-model:show=\"isShowAddMusicTo\" :music-info=\"playMusicInfo.musicInfo\" />\n  </div>\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'\nimport { musicInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default {\n  setup() {\n    const isShowAddMusicTo = ref(false)\n    const {\n      toggleDesktopLyricBtnTitle,\n      toggleDesktopLyric,\n      toggleLockDesktopLyric,\n    } = useToggleDesktopLyric()\n    const addMusicTo = () => {\n      if (!musicInfo.id) return\n      isShowAddMusicTo.value = true\n    }\n    return {\n      appSetting,\n      isShowAddMusicTo,\n      toggleDesktopLyricBtnTitle,\n      toggleDesktopLyric,\n      toggleLockDesktopLyric,\n      addMusicTo,\n      playMusicInfo,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.controlBtn {\n  padding-left: 20px;\n  padding-right: 10px;\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 10px;\n\n  button {\n    color: var(--color-button-font);\n  }\n}\n\n.titleBtn {\n  flex: none;\n  height: 100%;\n  width: 24px;\n  transition: @transition-fast;\n  transition-property: color, opacity;\n  // color: var(--color-button-font);\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n  background-color: transparent;\n  border: none;\n  width: 24px;\n  padding: 0;\n\n  opacity: .6;\n  cursor: pointer;\n\n  svg {\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    opacity: 1;\n  }\n  &:active {\n    opacity: 1;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/FullWidthProgress.vue",
    "content": "<template>\n  <div :class=\"$style.player\">\n    <div :class=\"$style.progress\">\n      <common-progress-bar v-if=\"!isShowPlayerDetail\" :class-name=\"$style.progressBar\" :progress=\"progress\" :handle-transition-end=\"handleTransitionEnd\" :is-active-transition=\"isActiveTransition\" />\n    </div>\n    <div :class=\"$style.picContent\" :aria-label=\"$t('player__pic_tip')\" @contextmenu=\"handleToMusicLocation\" @click=\"showPlayerDetail\">\n      <img v-if=\"musicInfo.pic\" :src=\"musicInfo.pic\" decoding=\"async\" @error=\"imgError\">\n      <div v-else :class=\"$style.emptyPic\">L<span>X</span></div>\n    </div>\n    <div :class=\"$style.infoContent\">\n      <div :class=\"$style.title\" :aria-label=\"title + $t('copy_tip')\" @click=\"handleCopy(title)\">\n        {{ title }}\n      </div>\n      <div :class=\"$style.status\">{{ statusText }}</div>\n    </div>\n    <div :class=\"$style.timeContent\">\n      <span>{{ nowPlayTimeStr }}</span>\n      <span style=\"margin: 0 1px;\">/</span>\n      <span>{{ maxPlayTimeStr }}</span>\n    </div>\n    <!-- <play-progress /> -->\n    <control-btns />\n    <div :class=\"$style.playBtnContent\">\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__prev')\" @click=\"playPrev()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-prevMusic\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"isPlay ? $t('player__pause') : $t('player__play')\" @click=\"togglePlay\">\n        <svg v-if=\"isPlay\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-pause\" />\n        </svg>\n        <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-play\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__next')\" @click=\"playNext()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-nextMusic\" />\n        </svg>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { clipboardWriteText } from '@common/utils/electron'\nimport ControlBtns from './ControlBtns.vue'\n// import PlayProgress from './PlayProgress'\nimport usePlayProgress from '@renderer/utils/compositions/usePlayProgress'\n// import { lyric } from '@renderer/core/share/lyric'\nimport {\n  statusText,\n  musicInfo,\n  isShowPlayerDetail,\n  isPlay,\n  playInfo,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setMusicInfo,\n  setShowPlayerDetail,\n} from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { togglePlay, playNext, playPrev } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\n\nexport default {\n  name: 'CorePlayBar',\n  components: {\n    ControlBtns,\n    // PlayProgress,\n  },\n  setup() {\n    const router = useRouter()\n\n    const {\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n    } = usePlayProgress()\n\n    const showPlayerDetail = () => {\n      if (!playMusicInfo.musicInfo) return\n      setShowPlayerDetail(true)\n    }\n    const handleCopy = (text) => {\n      clipboardWriteText(text)\n    }\n\n    const imgError = () => {\n      // console.log(e)\n      setMusicInfo({ pic: null })\n    }\n\n    const handleToMusicLocation = () => {\n      const listId = playMusicInfo.listId\n      if (!listId || listId == LIST_IDS.DOWNLOAD || !playMusicInfo.musicInfo) return\n      if (playInfo.playIndex == -1) return\n      void router.push({\n        path: '/list',\n        query: {\n          id: listId,\n          scrollIndex: playInfo.playIndex,\n        },\n      })\n    }\n\n    const title = computed(() => {\n      return musicInfo.name\n        ? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)\n        : ''\n    })\n\n    // onBeforeUnmount(() => {\n    // window.eventHub.emit(eventPlayerNames.setTogglePlay)\n    // })\n\n    return {\n      musicInfo,\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n      handleCopy,\n      imgError,\n      statusText,\n      title,\n      showPlayerDetail,\n      isPlay,\n      togglePlay,\n      playNext,\n      playPrev,\n      handleToMusicLocation,\n      isShowPlayerDetail,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.player {\n  position: relative;\n  height: @height-player;\n  // border-top: 1px solid var(--color-primary-alpha-900);\n  box-sizing: border-box;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  contain: strict;\n  padding: 8px 6px 6px;\n  z-index: 2;\n  // box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);\n  * {\n    box-sizing: border-box;\n  }\n\n  &:before {\n    .mixin-after();\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-main-background);\n    opacity: .9;\n    z-index: -1;\n  }\n}\n.progress {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  padding-bottom: 6px;\n  // height: 15px;\n  .progressBar {\n    height: 2px;\n    border-radius: 0;\n  }\n}\n\n.picContent {\n  height: 100%;\n  aspect-ratio: 1 / 1;\n\n  // color: var(--color-primary);\n  // transition: @transition-normal;\n  // transition-property: color;\n  flex: none;\n  opacity: 1;\n  transition: opacity @transition-fast;\n  // transition-property: opacity;\n  display: flex;\n  justify-content: center;\n  // align-items: center;\n  cursor: pointer;\n\n  &:hover {\n    opacity: .8;\n  }\n\n  // svg {\n  //   fill: currentColor;\n  // }\n  img {\n    box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);\n    max-width: 100%;\n    max-height: 100%;\n    transition: @transition-normal;\n    transition-property: border-color;\n    // border-radius: 50%;\n    border-radius: @radius-border;\n    // border: 2px solid @color-theme_2-background_1;\n  }\n\n  .emptyPic {\n    background-color: var(--color-primary-light-900-alpha-200);\n    border-radius: @radius-border;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--color-primary-light-400-alpha-200);\n    user-select: none;\n    font-size: 20px;\n    font-family: Consolas, \"Courier New\", monospace;\n\n    span {\n      padding-left: 3px;\n    }\n  }\n}\n\n.infoContent {\n  padding-left: 10px;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: flex-start;\n  font-size: 13px;\n  color: var(--color-font);\n  min-width: 0;\n  line-height: 1.5;\n}\n\n.title {\n  max-width: 100%;\n  font-size: 12px;\n  color: var(--color-font-label);\n  .mixin-ellipsis-1();\n}\n.status {\n  padding-top: 3px;\n  height: 23px;\n  .mixin-ellipsis-1();\n  max-width: 100%;\n}\n\n.timeContent {\n  flex: none;\n  color: var(--color-550);\n  font-size: 13px;\n  padding-left: 10px;\n}\n\n.playBtnContent {\n  height: 100%;\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  padding-left: 10px;\n  padding-right: 15px;\n  gap: 18px;\n}\n\n.playBtn {\n  flex: none;\n  height: 52%;\n  // margin-top: -2px;\n  transition: @transition-fast;\n  transition-property: color, opacity;\n  color: var(--color-button-font);\n  opacity: 1;\n  cursor: pointer;\n\n  svg {\n    fill: currentColor;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    opacity: 0.8;\n  }\n  &:active {\n    opacity: 0.6;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/MiddleWidthProgress.vue",
    "content": "<template>\n  <div :class=\"$style.player\">\n    <div :class=\"$style.picContent\" :aria-label=\"$t('player__pic_tip')\" @contextmenu=\"handleToMusicLocation\" @click=\"showPlayerDetail\">\n      <img v-if=\"musicInfo.pic\" :src=\"musicInfo.pic\" decoding=\"async\" @error=\"imgError\">\n      <div v-else :class=\"$style.emptyPic\">L<span>X</span></div>\n    </div>\n    <div :class=\"$style.infoContent\">\n      <div :class=\"$style.title\" :aria-label=\"title + $t('copy_tip')\" @click=\"handleCopy(title)\">\n        {{ title }}\n      </div>\n      <div :class=\"$style.status\">{{ statusText }}</div>\n    </div>\n    <div :class=\"$style.timeContent\">\n      <span>{{ nowPlayTimeStr }}</span>\n      <div :class=\"$style.progress\">\n        <common-progress-bar v-if=\"!isShowPlayerDetail\" :class-name=\"$style.progressBar\" :progress=\"progress\" :handle-transition-end=\"handleTransitionEnd\" :is-active-transition=\"isActiveTransition\" />\n      </div>\n      <!-- <span style=\"margin: 0 1px;\">/</span> -->\n      <span>{{ maxPlayTimeStr }}</span>\n    </div>\n    <!-- <play-progress /> -->\n    <control-btns />\n    <div :class=\"$style.playBtnContent\">\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__prev')\" @click=\"playPrev()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-prevMusic\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"isPlay ? $t('player__pause') : $t('player__play')\" @click=\"togglePlay\">\n        <svg v-if=\"isPlay\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-pause\" />\n        </svg>\n        <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-play\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__next')\" @click=\"playNext()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-nextMusic\" />\n        </svg>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { clipboardWriteText } from '@common/utils/electron'\nimport ControlBtns from './ControlBtns.vue'\n// import PlayProgress from './PlayProgress'\nimport usePlayProgress from '@renderer/utils/compositions/usePlayProgress'\n// import { lyric } from '@renderer/core/share/lyric'\nimport {\n  statusText,\n  musicInfo,\n  isShowPlayerDetail,\n  isPlay,\n  playInfo,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setMusicInfo,\n  setShowPlayerDetail,\n} from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { togglePlay, playNext, playPrev } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\n\nexport default {\n  name: 'CorePlayBar',\n  components: {\n    ControlBtns,\n    // PlayProgress,\n  },\n  setup() {\n    const router = useRouter()\n\n    const {\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n    } = usePlayProgress()\n\n    const showPlayerDetail = () => {\n      if (!playMusicInfo.musicInfo) return\n      setShowPlayerDetail(true)\n    }\n    const handleCopy = (text) => {\n      clipboardWriteText(text)\n    }\n\n    const imgError = () => {\n      // console.log(e)\n      setMusicInfo({ pic: null })\n    }\n\n    const handleToMusicLocation = () => {\n      const listId = playMusicInfo.listId\n      if (!listId || listId == LIST_IDS.DOWNLOAD || !playMusicInfo.musicInfo) return\n      if (playInfo.playIndex == -1) return\n      void router.push({\n        path: '/list',\n        query: {\n          id: listId,\n          scrollIndex: playInfo.playIndex,\n        },\n      })\n    }\n\n    const title = computed(() => {\n      return musicInfo.name\n        ? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)\n        : ''\n    })\n\n    // onBeforeUnmount(() => {\n    // window.eventHub.emit(eventPlayerNames.setTogglePlay)\n    // })\n\n    return {\n      musicInfo,\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n      handleCopy,\n      imgError,\n      statusText,\n      title,\n      showPlayerDetail,\n      isPlay,\n      togglePlay,\n      playNext,\n      playPrev,\n      handleToMusicLocation,\n      isShowPlayerDetail,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.player {\n  position: relative;\n  height: @height-player;\n  border-top: 1px solid var(--color-primary-alpha-900);\n  box-sizing: border-box;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  contain: strict;\n  padding: 6px;\n  z-index: 2;\n  // box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);\n  * {\n    box-sizing: border-box;\n  }\n\n  &:before {\n    .mixin-after();\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-main-background);\n    opacity: .9;\n    z-index: -1;\n  }\n}\n\n.picContent {\n  height: 100%;\n  aspect-ratio: 1 / 1;\n\n  // color: var(--color-primary);\n  // transition: @transition-normal;\n  // transition-property: color;\n  flex: none;\n  opacity: 1;\n  transition: opacity @transition-fast;\n  // transition-property: opacity;\n  display: flex;\n  justify-content: center;\n  // align-items: center;\n  cursor: pointer;\n\n  &:hover {\n    opacity: .8;\n  }\n\n  // svg {\n  //   fill: currentColor;\n  // }\n  img {\n    box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);\n    max-width: 100%;\n    max-height: 100%;\n    transition: @transition-normal;\n    transition-property: border-color;\n    // border-radius: 50%;\n    border-radius: @radius-border;\n    // border: 2px solid @color-theme_2-background_1;\n  }\n\n  .emptyPic {\n    background-color: var(--color-primary-light-900-alpha-200);\n    border-radius: @radius-border;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--color-primary-light-400-alpha-200);\n    user-select: none;\n    font-size: 20px;\n    font-family: Consolas, \"Courier New\", monospace;\n\n    span {\n      padding-left: 3px;\n    }\n  }\n}\n\n.infoContent {\n  padding: 0 10px;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: flex-start;\n  font-size: 13px;\n  color: var(--color-font);\n  min-width: 0;\n  line-height: 1.5;\n}\n\n.title {\n  max-width: 100%;\n  font-size: 12px;\n  color: var(--color-font-label);\n  .mixin-ellipsis-1();\n}\n.status {\n  padding-top: 3px;\n  height: 23px;\n  .mixin-ellipsis-1();\n  max-width: 100%;\n}\n\n.timeContent {\n  width: 30%;\n  // position: relative;\n  flex: none;\n  color: var(--color-550);\n  font-size: 13px;\n  // padding-left: 10px;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n}\n.progress {\n  // position: absolute;\n  // top: 0;\n  // left: 0;\n  // width: 100%;\n  flex: auto;\n  // width: 160px;\n  position: relative;\n  // padding-bottom: 6px;\n  margin: 0 8px;\n  padding: 8px 0;\n  // height: 15px;\n  // .progressBar {\n  //   height: 4px;\n  //   // border-radius: 0;\n  // }\n}\n.time {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n}\n\n.playBtnContent {\n  height: 100%;\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  padding-left: 10px;\n  padding-right: 15px;\n  gap: 18px;\n}\n\n.playBtn {\n  flex: none;\n  height: 52%;\n  // margin-top: -2px;\n  transition: @transition-fast;\n  transition-property: color, opacity;\n  color: var(--color-button-font);\n  opacity: 1;\n  cursor: pointer;\n\n  svg {\n    fill: currentColor;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    opacity: 0.8;\n  }\n  &:active {\n    opacity: 0.6;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/MiniWidthProgress.vue",
    "content": "<template>\n  <div :class=\"$style.player\">\n    <div :class=\"$style.picContent\" :aria-label=\"$t('player__pic_tip')\" @contextmenu=\"handleToMusicLocation\" @click=\"showPlayerDetail\">\n      <img v-if=\"musicInfo.pic\" :src=\"musicInfo.pic\" decoding=\"async\" @error=\"imgError\">\n      <div v-else :class=\"$style.emptyPic\">L<span>X</span></div>\n    </div>\n    <div :class=\"$style.infoContent\">\n      <div :class=\"$style.title\" :aria-label=\"title + $t('copy_tip')\" @click=\"handleCopy(title)\">\n        {{ title }}\n      </div>\n      <div :class=\"$style.status\">{{ statusText }}</div>\n    </div>\n    <!-- <div :class=\"$style.timeContainer\">\n      <div :class=\"$style.timeContent\">\n        <span>{{ nowPlayTimeStr }}</span>\n        <span style=\"margin: 0 1px;\">/</span>\n        <span>{{ maxPlayTimeStr }}</span>\n        <div :class=\"$style.progress\">\n          <common-progress-bar v-if=\"!isShowPlayerDetail\" :class-name=\"$style.progressBar\" :progress=\"progress\" :handle-transition-end=\"handleTransitionEnd\" :is-active-transition=\"isActiveTransition\" />\n        </div>\n      </div>\n    </div> -->\n    <play-progress />\n    <control-btns />\n    <div :class=\"$style.playBtnContent\">\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__prev')\" @click=\"playPrev()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-prevMusic\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"isPlay ? $t('player__pause') : $t('player__play')\" @click=\"togglePlay\">\n        <svg v-if=\"isPlay\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-pause\" />\n        </svg>\n        <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-play\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__next')\" @click=\"playNext()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-nextMusic\" />\n        </svg>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { clipboardWriteText } from '@common/utils/electron'\nimport ControlBtns from './ControlBtns.vue'\nimport PlayProgress from './PlayProgress.vue'\nimport usePlayProgress from '@renderer/utils/compositions/usePlayProgress'\n// import { lyric } from '@renderer/core/share/lyric'\nimport {\n  statusText,\n  musicInfo,\n  isShowPlayerDetail,\n  isPlay,\n  playInfo,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setMusicInfo,\n  setShowPlayerDetail,\n} from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { togglePlay, playNext, playPrev } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\n\nexport default {\n  name: 'CorePlayBar',\n  components: {\n    ControlBtns,\n    PlayProgress,\n  },\n  setup() {\n    const router = useRouter()\n\n    const {\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n    } = usePlayProgress()\n\n    const showPlayerDetail = () => {\n      if (!playMusicInfo.musicInfo) return\n      setShowPlayerDetail(true)\n    }\n    const handleCopy = (text) => {\n      clipboardWriteText(text)\n    }\n\n    const imgError = () => {\n      // console.log(e)\n      setMusicInfo({ pic: null })\n    }\n\n    const handleToMusicLocation = () => {\n      const listId = playMusicInfo.listId\n      if (!listId || listId == LIST_IDS.DOWNLOAD || !playMusicInfo.musicInfo) return\n      if (playInfo.playIndex == -1) return\n      void router.push({\n        path: '/list',\n        query: {\n          id: listId,\n          scrollIndex: playInfo.playIndex,\n        },\n      })\n    }\n\n    const title = computed(() => {\n      return musicInfo.name\n        ? appSetting['download.fileName'].replace('歌名', musicInfo.name).replace('歌手', musicInfo.singer)\n        : ''\n    })\n\n    // onBeforeUnmount(() => {\n    // window.eventHub.emit(eventPlayerNames.setTogglePlay)\n    // })\n\n    return {\n      musicInfo,\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n      handleCopy,\n      imgError,\n      statusText,\n      title,\n      showPlayerDetail,\n      isPlay,\n      togglePlay,\n      playNext,\n      playPrev,\n      handleToMusicLocation,\n      isShowPlayerDetail,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.player {\n  position: relative;\n  height: @height-player;\n  border-top: 1px solid var(--color-primary-alpha-900);\n  box-sizing: border-box;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  contain: strict;\n  padding: 6px;\n  z-index: 2;\n  // box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);\n  * {\n    box-sizing: border-box;\n  }\n\n  &:before {\n    .mixin-after();\n    left: 0;\n    top: 0;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-main-background);\n    opacity: .9;\n    z-index: -1;\n  }\n}\n\n.picContent {\n  height: 100%;\n  aspect-ratio: 1 / 1;\n\n  // color: var(--color-primary);\n  // transition: @transition-normal;\n  // transition-property: color;\n  flex: none;\n  opacity: 1;\n  transition: opacity @transition-fast;\n  // transition-property: opacity;\n  display: flex;\n  justify-content: center;\n  // align-items: center;\n  cursor: pointer;\n\n  &:hover {\n    opacity: .8;\n  }\n\n  // svg {\n  //   fill: currentColor;\n  // }\n  img {\n    box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);\n    max-width: 100%;\n    max-height: 100%;\n    transition: @transition-normal;\n    transition-property: border-color;\n    // border-radius: 50%;\n    border-radius: @radius-border;\n    // border: 2px solid @color-theme_2-background_1;\n  }\n\n  .emptyPic {\n    background-color: var(--color-primary-light-900-alpha-200);\n    border-radius: @radius-border;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--color-primary-light-400-alpha-200);\n    user-select: none;\n    font-size: 20px;\n    font-family: Consolas, \"Courier New\", monospace;\n\n    span {\n      padding-left: 3px;\n    }\n  }\n}\n\n.infoContent {\n  padding: 0 10px;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: flex-start;\n  font-size: 13px;\n  color: var(--color-font);\n  min-width: 0;\n  line-height: 1.5;\n}\n\n.title {\n  max-width: 100%;\n  font-size: 12px;\n  color: var(--color-font-label);\n  .mixin-ellipsis-1();\n}\n.status {\n  padding-top: 3px;\n  height: 23px;\n  .mixin-ellipsis-1();\n  max-width: 100%;\n}\n\n// .timeContainer {\n//   flex: none;\n//   padding: 15px 0;\n//   &:hover {\n//     .progress {\n//       opacity: 1;\n//     }\n//   }\n// }\n// .timeContent {\n//   // width: 30%;\n//   position: relative;\n//   // flex: none;\n//   color: var(--color-300);\n//   font-size: 13px;\n//   // padding-left: 10px;\n//   // display: flex;\n//   // flex-flow: column nowrap;\n//   // align-items: center;\n//   padding-bottom: 3px;\n// }\n// .progress {\n//   position: absolute;\n//   top: 100%;\n//   left: 0;\n//   width: 100%;\n//   flex: auto;\n//   // width: 160px;\n//   // position: relative;\n//   // padding-bottom: 6px;\n//   // margin: 0 8px;\n//   padding: 2px 0;\n//   height: 8px;\n//   transition: opacity @transition-normal;\n//   opacity: .24;\n\n//   .progressBar {\n//     height: 2px;\n//     border-radius: 0;\n//   }\n// }\n// .time {\n//   display: flex;\n//   flex-flow: row nowrap;\n//   justify-content: space-between;\n// }\n\n.playBtnContent {\n  height: 100%;\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  padding-left: 10px;\n  padding-right: 15px;\n  gap: 18px;\n}\n\n.playBtn {\n  flex: none;\n  height: 52%;\n  // margin-top: -2px;\n  transition: @transition-fast;\n  transition-property: color, opacity;\n  color: var(--color-button-font);\n  opacity: 1;\n  cursor: pointer;\n\n  svg {\n    fill: currentColor;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    opacity: 0.8;\n  }\n  &:active {\n    opacity: 0.6;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/PlayProgress.vue",
    "content": "<template>\n  <div :class=\"$style.content\" @click=\"handleShowPopup\" @mouseenter=\"handlMsEnter\" @mouseleave=\"handlMsLeave\">\n    <div ref=\"dom_btn\" :class=\"$style.timeContent\">\n      <span>{{ nowPlayTimeStr }}</span>\n      <span style=\"margin: 0 1px;\">/</span>\n      <span>{{ maxPlayTimeStr }}</span>\n      <div :class=\"$style.progress\">\n        <div :class=\"[$style.progressBar, {[$style.barTransition]: isActiveTransition}]\" :style=\"{ transform: `scaleX(${progress || 0})` }\" @transitionend=\"handleTransitionEnd\" />\n      </div>\n      <base-popup v-model:visible=\"visible\" :btn-el=\"dom_btn\" @mouseenter=\"handlMsEnter\" @mouseleave=\"handlMsLeave\" @transitionend=\"handleTranEnd\">\n        <div :class=\"$style.popupProgress\">\n          <common-progress-bar v-if=\"visibleProgress\" :progress=\"progress\" :handle-transition-end=\"handleTransitionEnd\" :is-active-transition=\"isActiveTransition\" />\n        </div>\n      </base-popup>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport usePlayProgress from '@renderer/utils/compositions/usePlayProgress'\nimport { isShowPlayerDetail } from '@renderer/store/player/state'\n\nexport default {\n  setup() {\n    const visible = ref(false)\n    const visibleProgress = ref(false)\n    const dom_btn = ref(null)\n\n    const handleShowPopup = (evt) => {\n      if (visible.value) {\n        evt.stopPropagation()\n        handlMsLeave()\n      } else handlMsEnter()\n    }\n    const {\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n    } = usePlayProgress()\n\n    let timeout = null\n    const handlMsEnter = () => {\n      if (timeout) {\n        clearTimeout(timeout)\n        timeout = null\n      }\n      if (visible.value) return\n      timeout = setTimeout(() => {\n        visible.value = true\n        visibleProgress.value = true\n      }, 100)\n    }\n    const handlMsLeave = () => {\n      if (timeout) {\n        clearTimeout(timeout)\n        timeout = null\n      }\n      if (!visible.value) return\n      timeout = setTimeout(() => {\n        timeout = null\n        visible.value = false\n      }, 100)\n    }\n    const handleTranEnd = () => {\n      if (visible.value) return\n      visibleProgress.value = false\n    }\n\n    // onMounted(() => {\n    //   visible.value = true\n    //   requestAnimationFrame(() => {\n    //     visible.value = false\n    //   })\n    // })\n\n    return {\n      visible,\n      visibleProgress,\n      dom_btn,\n      handleShowPopup,\n      nowPlayTimeStr,\n      maxPlayTimeStr,\n      progress,\n      isActiveTransition,\n      handleTransitionEnd,\n      handlMsLeave,\n      handlMsEnter,\n      handleTranEnd,\n      isShowPlayerDetail,\n    }\n  },\n}\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n// .content {\n//   flex: none;\n//   position: relative;\n//   // display: inline-block;\n//   padding: 5px 0;\n//   color: var(--color-300);\n//   font-size: 13px;\n//   cursor: pointer;\n//   transition: opacity @transition-fast;\n\n//   &:hover {\n//     opacity: .7;\n//   }\n// }\n.content {\n  flex: none;\n  position: relative;\n  padding: 15px 0;\n  &:hover {\n    .progress {\n      opacity: 1;\n    }\n  }\n}\n.timeContent {\n  // width: 30%;\n  position: relative;\n  // flex: none;\n  color: var(--color-550);\n  font-size: 13px;\n  // padding-left: 10px;\n  // display: flex;\n  // flex-flow: column nowrap;\n  // align-items: center;\n  padding-bottom: 3px;\n}\n\n.progress {\n  position: absolute;\n  top: 100%;\n  left: 0;\n  width: 100%;\n  flex: auto;\n  margin-top: 2px;\n  // width: 160px;\n  // position: relative;\n  // padding-bottom: 6px;\n  // margin: 0 8px;\n  height: 2px;\n  opacity: .24;\n  overflow: hidden;\n  transition: @transition-normal;\n  transition-property: background-color, opacity;\n  background-color: var(--color-primary-light-100-alpha-800);\n\n  .progressBar {\n    height: 100%;\n    width: 100%;\n    // position: absolute;\n    background-color: var(--color-primary-light-100-alpha-400);\n    // left: 0;\n    // top: 0;\n    transform-origin: 0;\n    will-change: transform;\n  }\n\n  .barTransition {\n    transition-property: transform;\n    transition-timing-function: ease-out;\n    transition-duration: 0.2s;\n  }\n}\n\n.popupProgress {\n  position: relative;\n  width: 300px;\n  height: 15px;\n  box-sizing: border-box;\n  padding: 5px 0;\n  margin: 0 5px;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayBar/index.vue",
    "content": "<template>\n  <FullWidthProgress v-if=\"appSetting['common.playBarProgressStyle'] == 'full'\" />\n  <MiddleWidthProgress v-else-if=\"appSetting['common.playBarProgressStyle'] == 'middle'\" />\n  <MiniWidthProgress v-else />\n</template>\n\n<script setup>\nimport { appSetting } from '@renderer/store/setting'\nimport MiniWidthProgress from './MiniWidthProgress.vue'\nimport FullWidthProgress from './FullWidthProgress.vue'\nimport MiddleWidthProgress from './MiddleWidthProgress.vue'\n\n</script>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/ControlBtnsLeftHeader.vue",
    "content": "<template lang=\"pug\">\ndiv(:class=\"$style.header\")\n  div(ref=\"dom_btns\" :class=\"$style.controBtn\")\n    button(type=\"button\" :class=\"$style.hide\" :aria-label=\"$t('player__hide_detail_tip')\" ignore-tip :title=\"$t('player__hide_detail_tip')\" @click=\"hide\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"80%\" viewBox=\"0 0 30.727 30.727\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-hide\")\n    button(type=\"button\" :class=\"$style.fullscreenExit\" :aria-label=\"$t('fullscreen_exit')\" ignore-tip :title=\"$t('fullscreen_exit')\" @click=\"fullscreenExit\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\")\n        use(xlink:href=\"#icon-fullscreen-exit\")\n    button(type=\"button\" :class=\"$style.min\" :aria-label=\"$t('min')\" ignore-tip :title=\"$t('min')\" @click=\"minWindow\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-minimize\")\n\n    //- button(type=\"button\" :class=\"$style.max\" @click=\"max\")\n    button(type=\"button\" :class=\"$style.close\" :aria-label=\"$t('close')\" ignore-tip :title=\"$t('close')\" @click=\"closeWindow\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-close\")\n</template>\n\n\n<script setup>\nimport { ref, onMounted, onBeforeUnmount, useCssModule } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport { setShowPlayerDetail } from '@renderer/store/player/action'\nimport { closeWindow, minWindow, setFullScreen } from '@renderer/utils/ipc'\n\nconst dom_btns = ref()\n\nconst cssModule = useCssModule()\n\nconst handle_focus = () => {\n  if (!dom_btns.value) return\n  dom_btns.value.classList.remove(cssModule.hover)\n}\nconst handle_mouseenter = () => {\n  dom_btns.value.classList.add(cssModule.hover)\n}\nconst handle_mouseleave = () => {\n  dom_btns.value.classList.remove(cssModule.hover)\n}\n\n\nonMounted(() => {\n  window.app_event.on('focus', handle_focus)\n  dom_btns.value.addEventListener('mouseenter', handle_mouseenter)\n  dom_btns.value.addEventListener('mouseleave', handle_mouseleave)\n})\nonBeforeUnmount(() => {\n  window.app_event.off('focus', handle_focus)\n  dom_btns.value.removeEventListener('mouseenter', handle_mouseenter)\n  dom_btns.value.removeEventListener('mouseleave', handle_mouseleave)\n})\n\n\nconst hide = () => {\n  dom_btns.value?.classList.remove(cssModule.hover)\n  setShowPlayerDetail(false)\n}\nconst fullscreenExit = () => {\n  dom_btns.value?.classList.remove(cssModule.hover)\n  void setFullScreen(false).then((fullscreen) => {\n    isFullscreen.value = fullscreen\n  })\n}\n\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@control-btn-width: @height-toolbar * .26;\n\n:global(.fullscreen) {\n  .header {\n    -webkit-app-region: no-drag;\n    align-self: flex-start;\n    .controBtn {\n      .close, .min {\n        display: none;\n      }\n      .fullscreenExit {\n        display: flex;\n      }\n    }\n  }\n}\n.header {\n  position: relative;\n  flex: 0 0 @height-toolbar;\n  -webkit-app-region: drag;\n  width: 100%;\n\n  .controBtn {\n    position: absolute;\n    top: 0;\n    display: flex;\n    -webkit-app-region: no-drag;\n\n    button {\n      display: flex;\n      position: relative;\n      background: none;\n      border: none;\n      outline: none;\n      padding: 1px;\n      cursor: pointer;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n    }\n\n    .fullscreenExit {\n      display: none;\n    }\n  }\n  .controBtn {\n    align-items: center;\n    padding: 0 @control-btn-width;\n    left: 0;\n    flex-direction: row-reverse;\n    height: @height-toolbar * .7;\n    transition: opacity @transition-normal;\n    opacity: .5;\n    &.hover {\n      opacity: .8;\n      .controBtnIcon {\n        opacity: 1;\n      }\n    }\n\n    button {\n      width: @control-btn-width;\n      height: @control-btn-width;\n      border-radius: 50%;\n      color: var(--color-font);\n      + button {\n        margin-right: (@control-btn-width / 2);\n      }\n\n      &.hide {\n        background-color: var(--color-btn-hide);\n      }\n      &.min, &.fullscreenExit {\n        background-color: var(--color-btn-min);\n      }\n      // &.max {\n      //   background-color: var(--color-btn-max);\n      // }\n      &.close {\n        background-color: var(--color-btn-close);\n      }\n    }\n  }\n\n  .controBtnIcon {\n    opacity: 0;\n    transition: opacity 0.2s ease-in-out;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/ControlBtnsRightHeader.vue",
    "content": "<template lang=\"pug\">\ndiv(:class=\"$style.header\")\n  div(ref=\"dom_btns\" :class=\"$style.controBtn\")\n    button(ref=\"dom_hide_btn\" type=\"button\" :class=\"$style.hide\" :aria-label=\"$t('player__hide_detail_tip')\" ignore-tip :title=\"$t('player__hide_detail_tip')\" @click=\"hide\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"35%\" viewBox=\"0 0 30.727 30.727\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-hide\")\n    button(ref=\"dom_fullscreen_btn\" type=\"button\" :class=\"$style.fullscreenExit\" :aria-label=\"$t('fullscreen_exit')\" ignore-tip :title=\"$t('fullscreen_exit')\" @click=\"fullscreenExit\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"60%\")\n        use(xlink:href=\"#icon-fullscreen-exit\")\n    button(type=\"button\" :class=\"$style.min\" :aria-label=\"$t('min')\" ignore-tip :title=\"$t('min')\" @click=\"minWindow\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"60%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-minimize-2\")\n\n    //- button(type=\"button\" :class=\"$style.max\" @click=\"max\")\n    button(type=\"button\" :class=\"$style.close\" :aria-label=\"$t('close')\" ignore-tip :title=\"$t('close')\" @click=\"closeWindow\")\n      svg(:class=\"$style.controBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"60%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n        use(xlink:href=\"#icon-window-close-2\")\n</template>\n\n\n<script setup>\nimport { onMounted, onBeforeUnmount, ref, useCssModule } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport { setShowPlayerDetail } from '@renderer/store/player/action'\nimport { closeWindow, minWindow, setFullScreen } from '@renderer/utils/ipc'\n\nconst dom_btns = ref()\nconst cssModule = useCssModule()\n\nconst handle_focus = () => {\n  if (!dom_btns.value) return\n  for (const node of dom_btns.value.childNodes) {\n    if (node.tagName != 'BUTTON') continue\n    node.classList.remove(cssModule.hover)\n  }\n}\nconst getBtnEl = (el) => el.tagName == 'BUTTON' || !el ? el : getBtnEl(el.parentNode)\nconst handle_mouseover = (event) => {\n  const btn = getBtnEl(event.target)\n  if (!btn) return\n  btn.classList.add(cssModule.hover)\n}\nconst handle_mouseout = (event) => {\n  const btn = getBtnEl(event.target)\n  if (!btn) return\n  btn.classList.remove(cssModule.hover)\n}\n\n\nonMounted(() => {\n  window.app_event.on('focus', handle_focus)\n  dom_btns.value.addEventListener('mouseover', handle_mouseover)\n  dom_btns.value.addEventListener('mouseout', handle_mouseout)\n})\nonBeforeUnmount(() => {\n  window.app_event.off('focus', handle_focus)\n  dom_btns.value.removeEventListener('mouseover', handle_mouseover)\n  dom_btns.value.removeEventListener('mouseout', handle_mouseout)\n})\n\nconst dom_hide_btn = ref()\nconst hide = () => {\n  dom_hide_btn.value?.classList.remove(cssModule.hover)\n  setShowPlayerDetail(false)\n}\nconst dom_fullscreen_btn = ref()\nconst fullscreenExit = () => {\n  dom_fullscreen_btn.value?.classList.remove(cssModule.hover)\n  void setFullScreen(false).then((fullscreen) => {\n    isFullscreen.value = fullscreen\n  })\n}\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@control-btn-width: @height-toolbar * .26;\n\n:global(.fullscreen) {\n  .header {\n    -webkit-app-region: no-drag;\n    align-self: flex-start;\n    .controBtn {\n      .close, .min {\n        display: none;\n      }\n      .fullscreenExit {\n        display: flex;\n      }\n    }\n  }\n}\n.header {\n  position: relative;\n  flex: 0 0 @height-toolbar;\n  -webkit-app-region: drag;\n  width: 100%;\n  align-self: flex-start;\n\n  .controBtn {\n    position: absolute;\n    top: 0;\n    display: flex;\n    -webkit-app-region: no-drag;\n\n    button {\n      display: flex;\n      position: relative;\n      background: none;\n      border: none;\n      outline: none;\n      padding: 1px;\n      cursor: pointer;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n    }\n\n    .fullscreenExit {\n      display: none;\n    }\n  }\n\n  .controBtn {\n    right: 0;\n    button {\n      width: 46px;\n      height: 30px;\n      color: var(--color-font-label);\n      transition: background-color 0.2s ease-in-out;\n\n      &.hover {\n        background-color: var(--color-button-background-hover);\n\n        &.close {\n          background-color: var(--color-btn-close);\n        }\n      }\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/LyricPlayer.vue",
    "content": "<template>\n  <div :class=\"['right', $style.right]\" :style=\"lrcFontSize\">\n    <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n      <div\n        v-show=\"!isShowLrcSelectContent\"\n        ref=\"dom_lyric\"\n        :class=\"['lyric', $style.lyric, { [$style.draging]: isMsDown }, { [$style.lrcActiveZoom]: isZoomActiveLrc }]\" :style=\"lrcStyles\"\n        @wheel=\"handleWheel\" @mousedown=\"handleLyricMouseDown\" @touchstart=\"handleLyricTouchStart\"\n        @contextmenu.stop=\"handleShowLyricMenu\"\n      >\n        <div :class=\"['pre', $style.lyricSpace]\" />\n        <div ref=\"dom_lyric_text\" />\n        <div :class=\"$style.lyricSpace\" />\n      </div>\n    </transition>\n    <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n      <div v-if=\"isShowLyricProgressSetting\" v-show=\"isStopScroll && !isShowLrcSelectContent\" :class=\"$style.skip\">\n        <div ref=\"dom_skip_line\" :class=\"$style.line\" />\n        <span :class=\"$style.label\">{{ timeStr }}</span>\n        <base-btn :class=\"$style.skipBtn\" @mouseenter=\"handleSkipMouseEnter\" @mouseleave=\"handleSkipMouseLeave\" @click=\"handleSkipPlay\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n            <use xlink:href=\"#icon-play\" />\n          </svg>\n        </base-btn>\n      </div>\n    </transition>\n    <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n      <div v-if=\"isShowLrcSelectContent\" ref=\"dom_lrc_select_content\" tabindex=\"-1\" :class=\"[$style.lyricSelectContent, 'select', 'scroll', 'lyricSelectContent']\" @contextmenu=\"handleCopySelectText\">\n        <div v-for=\"(info, index) in lyric.lines\" :key=\"index\" :class=\"[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]\">\n          <span>{{ info.text }}</span>\n          <template v-for=\"(lrc, i) in info.extendedLyrics\" :key=\"i\">\n            <br>\n            <span :class=\"$style.lyricSelectlineExtended\">{{ lrc }}</span>\n          </template>\n        </div>\n      </div>\n    </transition>\n    <LyricMenu v-model=\"lyricMenuVisible\" :xy=\"lyricMenuXY\" :lyric-info=\"lyricInfo\" @update-lyric=\"handleUpdateLyric\" />\n  </div>\n</template>\n\n<script>\nimport { clipboardWriteText } from '@common/utils/electron'\nimport { lyric } from '@renderer/store/player/lyric'\nimport { playProgress } from '@renderer/store/player/playProgress'\nimport { isFullscreen } from '@renderer/store'\nimport {\n  isPlay,\n  isShowLrcSelectContent,\n  isShowPlayComment,\n  musicInfo as playerMusicInfo,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setMusicInfo,\n} from '@renderer/store/player/action'\nimport { onMounted, onBeforeUnmount, computed, reactive, ref, nextTick, watch } from '@common/utils/vueTools'\nimport useLyric from '@renderer/utils/compositions/useLyric'\nimport LyricMenu from './components/LyricMenu.vue'\nimport { appSetting } from '@renderer/store/setting'\nimport { setLyricOffset } from '@renderer/core/lyric'\nimport useSelectAllLrc from './useSelectAllLrc'\n\nexport default {\n  components: {\n    LyricMenu,\n  },\n  setup() {\n    const isZoomActiveLrc = computed(() => appSetting['playDetail.isZoomActiveLrc'])\n    const isShowLyricProgressSetting = computed(() => appSetting['playDetail.isShowLyricProgressSetting'])\n\n    const {\n      dom_lyric,\n      dom_lyric_text,\n      dom_skip_line,\n      isMsDown,\n      isStopScroll,\n      timeStr,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n      handleSkipPlay,\n      handleSkipMouseEnter,\n      handleSkipMouseLeave,\n      handleScrollLrc,\n    } = useLyric({ isPlay, lyric, playProgress, isShowLyricProgressSetting })\n\n    const dom_lrc_select_content = useSelectAllLrc()\n\n    watch([isFullscreen, isShowPlayComment], () => {\n      setTimeout(handleScrollLrc, 400)\n    })\n\n    const lyricMenuVisible = ref(false)\n    const lyricMenuXY = reactive({\n      x: 0,\n      y: 0,\n    })\n    const lyricInfo = reactive({\n      lyric: '',\n      tlyric: '',\n      rlyric: '',\n      lxlyric: '',\n      rawlyric: '',\n      musicInfo: null,\n    })\n    const updateMusicInfo = () => {\n      lyricInfo.lyric = playerMusicInfo.lrc\n      lyricInfo.tlyric = playerMusicInfo.tlrc\n      lyricInfo.rlyric = playerMusicInfo.rlrc\n      lyricInfo.lxlyric = playerMusicInfo.lxlrc\n      lyricInfo.rawlyric = playerMusicInfo.rawlrc\n      lyricInfo.musicInfo = playMusicInfo.musicInfo\n    }\n    const handleShowLyricMenu = event => {\n      updateMusicInfo()\n      lyricMenuXY.x = event.pageX\n      lyricMenuXY.y = event.pageY\n      if (lyricMenuVisible.value) return\n      void nextTick(() => {\n        lyricMenuVisible.value = true\n      })\n    }\n    const handleUpdateLyric = ({ lyric, tlyric, rlyric, lxlyric, offset }) => {\n      setMusicInfo({\n        lrc: lyric,\n        tlrc: tlyric,\n        rlrc: rlyric,\n        lxlrc: lxlyric,\n      })\n      console.log(offset)\n      setLyricOffset(offset)\n    }\n\n    const lrcStyles = computed(() => {\n      return {\n        textAlign: appSetting['playDetail.style.align'],\n      }\n    })\n    const lrcFontSize = computed(() => {\n      let size = appSetting['playDetail.style.fontSize'] / 100\n      if (isFullscreen.value) size = size *= 1.4\n      return {\n        '--playDetail-lrc-font-size': (isShowPlayComment.value ? size * 0.82 : size) + 'rem',\n      }\n    })\n\n    onMounted(() => {\n      window.app_event.on('musicToggled', updateMusicInfo)\n      window.app_event.on('lyricUpdated', updateMusicInfo)\n    })\n    onBeforeUnmount(() => {\n      window.app_event.off('musicToggled', updateMusicInfo)\n      window.app_event.off('lyricUpdated', updateMusicInfo)\n    })\n\n    return {\n      dom_lyric,\n      dom_lyric_text,\n      dom_skip_line,\n      dom_lrc_select_content,\n      isMsDown,\n      timeStr,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n      handleSkipPlay,\n      handleSkipMouseEnter,\n      handleSkipMouseLeave,\n      lyric,\n      lrcStyles,\n      lrcFontSize,\n      isShowLrcSelectContent,\n      isShowLyricProgressSetting,\n      isZoomActiveLrc,\n      isStopScroll,\n      lyricMenuVisible,\n      lyricMenuXY,\n      handleShowLyricMenu,\n      handleUpdateLyric,\n      lyricInfo,\n    }\n  },\n  methods: {\n    handleCopySelectText() {\n      let str = window.getSelection().toString()\n      str = str.trim()\n      if (!str.length) return\n      clipboardWriteText(str)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.right {\n  flex: 0 0 60%;\n  // padding: 0 30px;\n  position: relative;\n  transition: flex-basis @transition-normal;\n}\n.lyric {\n  text-align: center;\n  height: 100%;\n  overflow: hidden;\n  font-size: var(--playDetail-lrc-font-size, 16px);\n  -webkit-mask-image: linear-gradient(transparent 0%, #fff 20%,  #fff 80%, transparent 100%);\n  cursor: grab;\n  &.draging {\n    cursor: grabbing;\n  }\n  :global {\n    .font-lrc {\n      color: var(--color-450);\n    }\n    .line-content {\n      line-height: 1.2;\n      padding: calc(var(--playDetail-lrc-font-size, 16px) / 2) 1px;\n      overflow-wrap: break-word;\n      color: var(--color-450);\n      transition: @transition-normal;\n      transition-property: padding;\n\n      .extended {\n        font-size: 0.8em;\n        margin-top: 5px;\n      }\n      &.line-mode {\n        .font-lrc {\n          transition: @transition-fast;\n          transition-property: font-size, color;\n        }\n      }\n      &.line-mode.active .font-lrc, &.font-mode.played .font-lrc {\n        color: var(--color-primary-dark-200);\n      }\n      &.font-mode .extended .font-lrc {\n        transition: @transition-slow;\n        transition-property: font-size, color;\n      }\n\n      &.font-mode > .line > .font-lrc {\n        > span {\n          transition: @transition-normal;\n          transition-property: font-size;\n          font-size: 1em;\n          background-repeat: no-repeat;\n          background-color: var(--color-450);\n          background-image: -webkit-linear-gradient(top, var(--color-primary-dark-200), var(--color-primary-dark-200));\n          -webkit-text-fill-color: transparent;\n          -webkit-background-clip: text;\n          background-size: 0 100%;\n        }\n      }\n    }\n  }\n  // p {\n  //   padding: 8px 0;\n  //   line-height: 1.2;\n  //   overflow-wrap: break-word;\n  //   transition: @transition-normal !important;\n  //   transition-property: color, font-size;\n  // }\n  // .lrc-active {\n  //   color: var(--color-primary);\n  //   font-size: 1.2em;\n  // }\n}\n.lrcActiveZoom {\n  :global {\n    .line-content {\n      &.active {\n        .extended {\n          font-size: .94em;\n        }\n        .line {\n          font-size: 1.1em;\n        }\n      }\n    }\n  }\n}\n\n.skip {\n  position: absolute;\n  top: calc(38% + var(--playDetail-lrc-font-size, 16px) + 4px);\n  left: 0;\n  // height: 6px;\n  width: 100%;\n  pointer-events: none;\n  // opacity: .5;\n  .line {\n    border-top: 2px dotted var(--color-primary-dark-100);\n    opacity: .15;\n    margin-right: 30px;\n    -webkit-mask-image: linear-gradient(90deg, transparent 0%, transparent 15%, #fff 100%);\n  }\n  .label {\n    position: absolute;\n    right: 30px;\n    top: -14px;\n    line-height: 1.2;\n    font-size: 12px;\n    color: var(--color-primary-dark-100);\n    opacity: .7;\n  }\n  .skipBtn {\n    position: absolute;\n    right: 0;\n    top: 0;\n    transform: translateY(-50%);\n    width: 30px;\n    height: 30px;\n    padding: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: none !important;\n    pointer-events: initial;\n    transition: @transition-normal;\n    transition-property: opacity;\n    opacity: .8;\n    &:hover {\n      opacity: .6;\n    }\n  }\n}\n.lyricSelectContent {\n  position: absolute;\n  left: 0;\n  top: 0;\n  // text-align: center;\n  height: 100%;\n  width: 100%;\n  font-size: var(--playDetail-lrc-font-size, 16px);\n  z-index: 10;\n  color: var(--color-400);\n\n  .lyricSelectline {\n    padding: calc(var(--playDetail-lrc-font-size, 16px) / 2) 1px;\n    overflow-wrap: break-word;\n    transition: @transition-normal !important;\n    transition-property: color, font-size;\n    line-height: 1.3;\n  }\n  .lyricSelectlineExtended {\n    font-size: 14px;\n  }\n  .lrcActive {\n    color: var(--color-primary);\n  }\n}\n\n.lyricSpace {\n  height: 70%;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/PlayBar.vue",
    "content": "<template>\n  <div :class=\"$style.footer\">\n    <div :class=\"$style.footerLeft\">\n      <control-btns />\n      <div :class=\"$style.progressContainer\">\n        <div :class=\"$style.progressContent\">\n          <common-progress-bar\n            :class-name=\"$style.progress\"\n            :progress=\"progress\"\n            :handle-transition-end=\"handleTransitionEnd\"\n            :is-active-transition=\"isActiveTransition\"\n          />\n        </div>\n      </div>\n      <div :class=\"$style.timeLabel\"><span :class=\"$style.status\" style=\"margin-right: 15px\">{{ status }}</span><span>{{ nowPlayTimeStr }}</span><span style=\"margin: 0 5px;\">/</span><span>{{ maxPlayTimeStr }}</span></div>\n    </div>\n    <div :class=\"$style.playControl\">\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__prev')\" @click=\"playPrev()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-prevMusic\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"isPlay ? $t('player__pause') : $t('player__play')\" @click=\"togglePlay\">\n        <svg v-if=\"isPlay\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-pause\" />\n        </svg>\n        <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-play\" />\n        </svg>\n      </div>\n      <div :class=\"$style.playBtn\" :aria-label=\"$t('player__next')\" @click=\"playNext()\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n          <use xlink:href=\"#icon-nextMusic\" />\n        </svg>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { playNext, playPrev, togglePlay } from '@renderer/core/player'\nimport { status, isPlay } from '@renderer/store/player/state'\nimport usePlayProgress from '@renderer/utils/compositions/usePlayProgress'\n\nimport ControlBtns from './components/ControlBtns.vue'\n\nconst {\n  nowPlayTimeStr,\n  maxPlayTimeStr,\n  progress,\n  isActiveTransition,\n  handleTransitionEnd,\n} = usePlayProgress()\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.footer {\n  flex: 0 0 100px;\n  overflow: hidden;\n  display: flex;\n  align-items: center;\n}\n.footerLeft {\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  padding: 13px 13px 13px 30px;\n  overflow: hidden;\n}\n\n.progressContainer {\n  width: 100%;\n  position: relative;\n  padding: 3px 0;\n}\n\n.progressContent {\n  position: relative;\n  height: 16px;\n  padding: 5px 0;\n  width: 100%;\n}\n.progress {\n  height: 100%;\n}\n\n.barTransition {\n  transition-property: transform;\n  transition-timing-function: ease-out;\n  transition-duration: 0.2s;\n}\n.timeLabel {\n  width: 100%;\n  height: 18px;\n  display: flex;\n  span {\n    font-size: 13px;\n  }\n}\n.status {\n  flex: auto;\n}\n\n.playControl {\n  flex: none;\n  height: 100%;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  padding: 0 25px;\n  color: var(--color-button-font);\n}\n.playBtn {\n  height: 40%;\n  padding: 5px;\n  cursor: pointer;\n  flex: none;\n  // transition: @transition-normal;\n  // transition-property: color;\n  color: var(--color-button-font);\n  transition: opacity 0.2s ease;\n  opacity: 1;\n  cursor: pointer;\n\n  +.playBtn {\n    margin-left: 10px;\n  }\n  svg {\n    fill: currentColor;\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));\n  }\n  &:hover {\n    opacity: 0.8;\n  }\n  &:active {\n    opacity: 0.6;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/autoHideMounse.js",
    "content": "import { debounce } from '@common/utils/common'\nlet isAutoHide = false\nlet isLockedPointer = false\n// let dom = null\nlet event = null\nlet isMouseDown = false\n\nconst isControl = dom => {\n  if (!dom || dom === document.body) return false\n  // console.log(dom)\n  if (dom.getAttribute('aria-label') || dom.tagName == 'BUTTON') return true\n  return isControl(dom.parentNode)\n}\n\nconst lockPointer = () => {\n  if (!isAutoHide || isMouseDown) return\n  if (event && isControl(document.elementFromPoint(event.clientX, event.clientY))) return\n\n  document.body.requestPointerLock()\n  isLockedPointer = true\n}\nconst unLockPointer = () => {\n  if (!isLockedPointer) return\n  document.exitPointerLock()\n  isLockedPointer = false\n}\n\nconst startTimeout = debounce(lockPointer, 3000)\n\nconst handleMouseMove = (_event) => {\n  event = _event\n  startTimeout()\n  unLockPointer()\n}\n\nconst handleMouseDown = () => {\n  isMouseDown = true\n}\nconst handleMouseUp = () => {\n  isMouseDown = false\n  startTimeout()\n}\n\nexport const registerAutoHideMounse = () => {\n  if (isAutoHide) return\n  // if (!dom) dom = document.getElementById('root')\n  isAutoHide = true\n  document.body.addEventListener('mousemove', handleMouseMove)\n  document.body.addEventListener('mousedown', handleMouseDown)\n  document.body.addEventListener('mouseup', handleMouseUp)\n  startTimeout()\n}\n\nexport const unregisterAutoHideMounse = () => {\n  if (!isAutoHide) return\n  isAutoHide = false\n  // console.log(dom)\n  document.body.removeEventListener('mousemove', handleMouseMove)\n  document.body.removeEventListener('mousedown', handleMouseDown)\n  document.body.removeEventListener('mouseup', handleMouseUp)\n  unLockPointer()\n}\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/components/ControlBtns.vue",
    "content": "<template lang=\"pug\">\ndiv(:class=\"$style.footerLeftControlBtns\")\n  button(:class=\"[$style.footerLeftControlBtn, $style.lrcBtn]\" :aria-label=\"toggleDesktopLyricBtnTitle\" @click=\"toggleDesktopLyric\" @contextmenu=\"toggleLockDesktopLyric\")\n    svg(v-show=\"appSetting['desktopLyric.enable']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"125%\" viewBox=\"0 0 512 512\" space=\"preserve\")\n      use(xlink:href=\"#icon-desktop-lyric-on\")\n    svg(v-show=\"!appSetting['desktopLyric.enable']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"125%\" viewBox=\"0 0 512 512\" space=\"preserve\")\n      use(xlink:href=\"#icon-desktop-lyric-off\")\n  button(:class=\"[$style.footerLeftControlBtn, { [$style.active]: appSetting['player.audioVisualization'] }]\" :aria-label=\"$t('audio_visualization')\" @click=\"toggleAudioVisualization\")\n    svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"95%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n      use(xlink:href=\"#icon-audio-wave\")\n  button(:class=\"[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]\" :aria-label=\"$t('lyric__select')\" @click=\"toggleVisibleLrc\")\n    svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"95%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n      use(xlink:href=\"#icon-text\")\n  button(:class=\"[$style.footerLeftControlBtn, {[$style.active]: isShowPlayComment}]\" :aria-label=\"$t('comment__show')\" @click=\"toggleVisibleComment\")\n    svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"95%\" viewBox=\"0 0 24 24\" space=\"preserve\")\n      use(xlink:href=\"#icon-comment\")\n  common-sound-effect-btn\n  common-playback-rate-btn\n  common-volume-btn\n  common-toggle-play-mode-btn\n  button(:class=\"$style.footerLeftControlBtn\" :aria-label=\"$t('player__add_music_to')\" @click=\"isShowAddMusicTo = true\")\n    svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 512 512\" space=\"preserve\")\n      use(xlink:href=\"#icon-add-2\")\n  common-list-add-modal(v-model:show=\"isShowAddMusicTo\" :music-info=\"playMusicInfo.musicInfo\")\n\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\n\nimport {\n  isShowLrcSelectContent,\n  isShowPlayComment,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setShowPlayLrcSelectContentLrc,\n  setShowPlayComment,\n} from '@renderer/store/player/action'\n\nimport useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'\nimport useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { setMediaDeviceId } from '@renderer/plugins/player'\nimport { appSetting, saveMediaDeviceId, setEnableAudioVisualization } from '@renderer/store/setting'\n\nexport default {\n  setup() {\n    const t = useI18n()\n    // const setting = useRefGetter('setting')\n    // const setAudioVisualization = useCommit('setAudioVisualization')\n    // const saveMediaDeviceId = useCommit('setMediaDeviceId')\n\n    const toggleVisibleLrc = () => {\n      setShowPlayLrcSelectContentLrc(!isShowLrcSelectContent.value)\n    }\n    const toggleVisibleComment = () => {\n      setShowPlayComment(!isShowPlayComment.value)\n    }\n    const {\n      nextTogglePlayName,\n      toggleNextPlayMode,\n    } = useNextTogglePlay()\n\n    const {\n      toggleDesktopLyricBtnTitle,\n      toggleDesktopLyric,\n      toggleLockDesktopLyric,\n    } = useToggleDesktopLyric()\n\n    const isShowAddMusicTo = ref(false)\n\n    const toggleAudioVisualization = async() => {\n      const newSetting = !appSetting['player.audioVisualization']\n      if (newSetting && appSetting['player.mediaDeviceId'] != 'default') {\n        const confirm = await dialog.confirm({\n          message: t('setting__player_audio_visualization_tip'),\n          cancelButtonText: t('cancel_button_text'),\n          confirmButtonText: t('confirm_button_text'),\n        })\n        if (!confirm) return\n        await setMediaDeviceId('default').catch(_ => _)\n        saveMediaDeviceId('default')\n      }\n      setEnableAudioVisualization(newSetting)\n    }\n\n    return {\n      appSetting,\n      isShowLrcSelectContent,\n      toggleVisibleLrc,\n      isShowPlayComment,\n      toggleVisibleComment,\n      nextTogglePlayName,\n      toggleNextPlayMode,\n      toggleDesktopLyricBtnTitle,\n      toggleDesktopLyric,\n      toggleLockDesktopLyric,\n      toggleAudioVisualization,\n      isShowAddMusicTo,\n      playMusicInfo,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.footerLeftControlBtns {\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: flex-end;\n  align-items: center;\n  gap: 8px;\n\n  button {\n    width: 20px;\n    color: var(--color-font);\n  }\n\n  .footerLeftControlBtn {\n    // width: 18px;\n    // height: 18px;\n    opacity: .5;\n    cursor: pointer;\n    transition: opacity @transition-normal;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: transparent;\n    border: none;\n    padding: 0;\n\n    &:hover {\n      opacity: .9;\n    }\n\n    &.active {\n      color: var(--color-primary);\n      opacity: .8;\n    }\n  }\n\n  .lrcBtn {\n    width: 20px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/components/LyricMenu.vue",
    "content": "<template>\n  <teleport to=\"#root\">\n    <div ref=\"dom_menu\" :class=\"$style.container\" :style=\"menuStyles\" :aria-hidden=\"!modelValue\">\n      <!-- <div :class=\"$style.group\">\n      <div :class=\"$style.title\">{{ $t('lyric_menu__align') }}</div>\n      <div :class=\"$style.subGroup\">\n        <div :class=\"[$style.btn, { [$style.active]: appSetting['playDetail.style.align'] == 'left' }]\" role=\"button\" @click=\"setFontAlign('left')\" ignore-tip :aria-label=\"$t('lyric_menu__align_left')\">{{ $t('lyric_menu__align_left') }}</div>\n        <div :class=\"[$style.btn, { [$style.active]: appSetting['playDetail.style.align'] == 'center' }]\" role=\"button\" @click=\"setFontAlign('center')\" ignore-tip :aria-label=\"$t('lyric_menu__align_center')\">{{ $t('lyric_menu__align_center') }}</div>\n      </div>\n    </div> -->\n      <div :class=\"$style.group\">\n        <div :class=\"$style.subGroup\">\n          <div :class=\"$style.title\">{{ $t('lyric_menu__lrc_size', { size: appSetting['playDetail.style.fontSize'] }) }}</div>\n          <button :class=\"[$style.btn, $style.titleBtn]\" :disabled=\"appSetting['playDetail.style.fontSize'] == 100\" ignore-tip :aria-label=\"$t('lyric_menu__size_reset')\" @click=\"fontSizeReset\">{{ $t('lyric_menu__size_reset') }}</button>\n        </div>\n        <div :class=\"$style.subGroup\">\n          <button :class=\"$style.btn\" :aria-label=\"$t('lyric_menu__size_add')\" @click=\"fontSizeUp(5)\" @contextmenu=\"fontSizeUp(1)\">\n            <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"18px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n              <use xlink:href=\"#icon-font-increase\" />\n            </svg>\n          </button>\n          <button :class=\"$style.btn\" :aria-label=\"$t('lyric_menu__size_dec')\" @click=\"fontSizeDown(5)\" @contextmenu=\"fontSizeDown(1)\">\n            <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"18px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n              <use xlink:href=\"#icon-font-decrease\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n      <div :class=\"$style.group\">\n        <div :class=\"$style.subGroup\">\n          <div :class=\"$style.title\">{{ $t('lyric_menu__offset', { offset }) }}</div>\n          <button :class=\"[$style.btn, $style.titleBtn]\" :disabled=\"offsetDisabled || offset == originOffset\" @click=\"offsetReset\">{{ $t('lyric_menu__offset_reset') }}</button>\n        </div>\n        <div :class=\"$style.subGroup\">\n          <button :class=\"$style.btn\" :disabled=\"offsetDisabled\" ignore-tip :aria-label=\"$t('lyric_menu__offset_add_10')\" @click=\"setOffset(10)\">+ 10ms</button>\n          <button :class=\"$style.btn\" :disabled=\"offsetDisabled\" ignore-tip :aria-label=\"$t('lyric_menu__offset_dec_10')\" @click=\"setOffset(-10)\">- 10ms</button>\n        </div>\n        <div :class=\"$style.subGroup\">\n          <button :class=\"$style.btn\" :disabled=\"offsetDisabled\" ignore-tip :aria-label=\"$t('lyric_menu__offset_add_100')\" @click=\"setOffset(100)\">+ 100ms</button>\n          <button :class=\"$style.btn\" :disabled=\"offsetDisabled\" ignore-tip :aria-label=\"$t('lyric_menu__offset_dec_100')\" @click=\"setOffset(-100)\">- 100ms</button>\n        </div>\n      </div>\n    </div>\n  </teleport>\n</template>\n\n<script>\nimport { computed, ref, watch } from '@common/utils/vueTools'\nimport useMenuLocation from '@renderer/utils/compositions/useMenuLocation'\nimport { debounce } from '@common/utils/common'\nimport { saveLyricEdited, removeLyricEdited } from '@renderer/utils/ipc'\nimport { appSetting, setPlayDetailLyricFont, setPlayDetailLyricAlign } from '@renderer/store/setting'\n\nconst offsetTagRxp = /(?:^|\\n)\\s*\\[offset:\\s*(\\S+(?:\\d+)*)\\s*\\]/\nconst offsetTagAllRxp = /(^|\\n)\\s*\\[offset:\\s*(\\S+(?:\\d+)*)\\s*\\]/g\n\nconst saveLyric = debounce((musicInfo, lyricInfo) => {\n  void saveLyricEdited(musicInfo, lyricInfo)\n})\nconst removeLyric = debounce(musicInfo => {\n  void removeLyricEdited(musicInfo)\n})\n\nconst getOffset = lrc => {\n  let offset = offsetTagRxp.exec(lrc)\n  if (offset) {\n    offset = parseInt(offset[1])\n    if (Number.isNaN(offset)) offset = 0\n  } else offset = 0\n  return offset\n}\n\nexport default {\n  name: 'LyricMenu',\n  props: {\n    modelValue: Boolean,\n    xy: {\n      type: Object,\n      required: true,\n    },\n    lyricInfo: {\n      type: Object,\n      required: true,\n    },\n  },\n  emits: ['updateLyric', 'update:modelValue'],\n  setup(props, { emit }) {\n    // const appSetting = useRefGetter('appSetting')\n    // const playDetailSetting = useRefGetter('playDetailSetting')\n    // const setPlayDetailLyricAlign = useCommit('setPlayDetailLyricAlign')\n    // const setPlayDetailLyricFont = useCommit('setPlayDetailLyricFont')\n\n    const offset = ref(0)\n    const offsetDisabled = ref(true)\n    const originOffset = ref(0)\n\n    const visible = computed(() => props.modelValue)\n    const location = computed(() => props.xy)\n\n    const onHide = () => {\n      emit('update:modelValue', false)\n    }\n\n    const setFontAlign = val => {\n      if (appSetting['playDetail.style.align'] == val) return\n      setPlayDetailLyricAlign(val)\n    }\n\n    const fontSizeUp = step => {\n      if (appSetting['playDetail.style.fontSize'] >= 200) return\n      setPlayDetailLyricFont(Math.min(appSetting['playDetail.style.fontSize'] + step, 200))\n    }\n    const fontSizeDown = step => {\n      if (appSetting['playDetail.style.fontSize'] <= 70) return\n      setPlayDetailLyricFont(Math.max(appSetting['playDetail.style.fontSize'] - step, 70))\n    }\n    const fontSizeReset = () => {\n      setPlayDetailLyricFont(100)\n    }\n\n    const updateLyric = offset => {\n      let lyric = props.lyricInfo.lyric\n      let tlyric = props.lyricInfo.tlyric\n      let rlyric = props.lyricInfo.rlyric\n      let lxlyric = props.lyricInfo.lxlyric\n      if (offsetTagRxp.test(lyric)) {\n        lyric = lyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)\n        tlyric &&= tlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)\n        lxlyric &&= lxlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)\n        rlyric &&= rlyric.replace(offsetTagAllRxp, `$1[offset:${offset}]`)\n      } else {\n        lyric &&= `[offset:${offset}]\\n` + lyric\n        tlyric &&= `[offset:${offset}]\\n` + tlyric\n        lxlyric &&= `[offset:${offset}]\\n` + lxlyric\n        rlyric &&= `[offset:${offset}]\\n` + rlyric\n      }\n\n      const musicInfo = 'progress' in props.lyricInfo.musicInfo ? props.lyricInfo.musicInfo.metadata.musicInfo : props.lyricInfo.musicInfo\n\n      if (offset == originOffset.value) {\n        removeLyric(musicInfo)\n      } else {\n        saveLyric(musicInfo, {\n          lyric,\n          tlyric,\n          rlyric,\n          lxlyric,\n        })\n      }\n\n      emit('updateLyric', {\n        lyric,\n        tlyric,\n        rlyric,\n        lxlyric,\n        offset,\n      })\n    }\n    const setOffset = step => {\n      offset.value += step\n      updateLyric(offset.value)\n    }\n    const offsetReset = () => {\n      if (offset.value == originOffset.value) return\n      offset.value = originOffset.value\n      updateLyric(originOffset.value)\n    }\n\n    const parseLrcOffset = () => {\n      offset.value = getOffset(props.lyricInfo.lyric)\n      originOffset.value = getOffset(props.lyricInfo.rawlyric)\n      offsetDisabled.value = !props.lyricInfo.lyric\n    }\n\n\n    const { dom_menu, menuStyles } = useMenuLocation({\n      visible,\n      location,\n      onHide,\n    })\n\n    watch(() => props.lyricInfo.musicInfo, () => {\n      if (!props.modelValue) return\n      parseLrcOffset()\n    })\n    watch(visible, val => {\n      if (!val) return\n      parseLrcOffset()\n    })\n\n    return {\n      appSetting,\n      dom_menu,\n      menuStyles,\n      offset,\n      originOffset,\n      fontSizeUp,\n      fontSizeDown,\n      fontSizeReset,\n      setOffset,\n      offsetReset,\n      setFontAlign,\n      offsetDisabled,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  font-size: 12px;\n  position: absolute;\n  opacity: 0;\n  transform: scale(0);\n  transform-origin: 0 0 0;\n  transition: .14s ease;\n  transition-property: transform, opacity;\n  border-radius: @radius-border;\n  background-color: var(--color-content-background);\n  box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);\n  z-index: 10;\n  overflow: hidden;\n}\n\n.group {\n  display: flex;\n  flex-direction: column;\n}\n.title {\n  flex: auto;\n  padding: 10px 0 10px 10px;\n  color: var(--color-font-label);\n  white-space: nowrap;\n  min-width: 120px;\n}\n\n.subGroup {\n  display: flex;\n  flex-flow: row nowrap;\n}\n\n.btn {\n  flex: auto;\n  white-space: nowrap;\n  cursor: pointer;\n  min-width: 60px;\n  height: 34px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  // color: @color-btn;\n  padding: 0 10px;\n  outline: none;\n  transition: @transition-normal;\n  transition-property: background-color, opacity;\n  box-sizing: border-box;\n  .mixin-ellipsis-1();\n  background-color: var(--color-content-background);\n  border: none;\n\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-background-active);\n  }\n  &.active {\n    background-color: var(--color-content-background);\n    color: var(--color-button-font-selected);\n    cursor: default;\n    opacity: 1;\n  }\n\n  &[disabled] {\n    cursor: default;\n    opacity: .4;\n    &:hover {\n      background: none !important;\n    }\n  }\n}\n.titleBtn {\n  flex: none;\n  padding: 0 10;\n  min-width: 40px;\n  opacity: .7;\n\n  &[disabled] {\n    opacity: .3;\n  }\n}\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/components/MusicComment/CommentFloor.vue",
    "content": "<template lang=\"pug\">\ndiv(:class=\"$style.container\")\n  ul\n    li(v-for=\"item in comments\" :key=\"item.id\" :class=\"$style.listItem\")\n      div(:class=\"$style.content\")\n        div(:class=\"$style.left\")\n          img( :class=\"$style.avatar\" :src=\"item.avatar || commentDefImg\" @error=\"handleUserImg\")\n        div(:class=\"$style.right\")\n          div(:class=\"$style.info\")\n            div(:class=\"$style.baseInfo\")\n              div.select(:class=\"$style.name\") {{ item.userName }}\n              div(:class=\"$style.metaInfo\")\n                time(v-if=\"item.timeStr\" :class=\"$style.label\") {{ timeFormat(item.timeStr) }}\n                div(v-if=\"item.location\" :class=\"$style.label\") {{ $t('comment__location', { location: item.location }) }}\n            div(v-if=\"item.likedCount != null\" :class=\"$style.likes\")\n              svg(:class=\"$style.likesIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 512 512\" space=\"preserve\")\n                use(xlink:href=\"#icon-thumbs-up\")\n              | {{ item.likedCount }}\n          p.select(:class=\"$style.comment_text\") {{ item.text }}\n          div(v-if=\"item.images?.length\" :class=\"$style.comment_images\")\n            img(v-for=\"(url, index) in item.images\" :key=\"index\" :src=\"url\" loading=\"lazy\" decoding=\"async\")\n      comment-floor(v-if=\"item.reply && item.reply.length\" :class=\"$style.reply_floor\" :comments=\"item.reply\")\n</template>\n\n<script>\nimport commentDefImg from '@renderer/assets/images/defaultUser.jpg'\n\nexport default {\n  name: 'CommentFloor',\n  props: {\n    comments: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n  },\n  data() {\n    return {\n      commentDefImg,\n    }\n  },\n  methods: {\n    timeFormat(time) {\n      return time\n      // return formatTime(new Date(time), true)\n    },\n    handleUserImg(event) {\n      event.target.src = this.commentDefImg\n    },\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@padding: 15px;\n\n// .container {\n\n// }\n\n.listItem {\n  border-bottom: 1px dashed var(--color-primary-alpha-700);\n}\n\n.content {\n  padding: 12px 0;\n  font-size: 13px;\n  color: var(--color-font);\n  display: flex;\n}\n.left {\n  flex: none;\n}\n.avatar {\n  width: 40px;\n  border-radius: 4px;\n  box-shadow: 0 0 2px rgba(0, 0, 0, .15);\n}\n.right {\n  flex: auto;\n  min-width: 0;\n  margin-left: 10px;\n}\n\n.info {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 15px;\n  width: 100%;\n  height: 40px;\n  line-height: 1.3;\n  color: var(--color-450);\n}\n.baseInfo {\n  height: 100%;\n  flex: auto;\n  display: flex;\n  min-width: 0;\n  flex-flow: column nowrap;\n  justify-content: space-evenly;\n}\n.metaInfo {\n  display: flex;\n  flex-flow: row nowrap;\n  min-width: 0;\n  gap: 10px;\n  overflow: hidden;\n}\n.name {\n  flex: 0 1 auto;\n  min-width: 0;\n  .mixin-ellipsis-1();\n  color: var(--color-650);\n}\n.label {\n  flex: none;\n  font-size: 12px;\n  // margin-left: 5px;\n}\n.likes {\n  flex: none;\n  font-size: 11px;\n  text-align: right;\n  padding-top: 3px;\n  align-self: flex-start;\n}\n.likesIcon {\n  width: 12px;\n  height: 12px;\n  margin-right: 3px;\n  color: var(--color-primary-alpha-500);\n}\n.comment_text {\n  text-align: justify;\n  font-size: 14px;\n  line-height: 1.5;\n  word-break: break-all;\n  overflow-wrap: break-word;\n  white-space: pre-wrap;\n}\n.comment_images {\n  display: flex;\n  flex-flow: row wrap;\n  gap: 5px;\n  margin-top: 5px;\n\n  img {\n    max-width: 240px;\n  }\n}\n\n.reply_floor {\n  padding: 0 0 0 @padding;\n  margin-left: @padding * 2;\n  border-radius: .5rem;\n  &:last-child {\n    margin-bottom: 12px;\n  }\n  .listItem:last-child {\n    border-bottom: none;\n  }\n  .right {\n    margin-right: 10px;\n  }\n\n  background-color: var(--color-primary-light-500-alpha-700);\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/components/MusicComment/index.vue",
    "content": "<template lang=\"pug\">\ndiv.comment(ref=\"dom_container\" :class=\"$style.comment\")\n  div(:class=\"$style.commentHeader\")\n    h3 {{ $t('comment__title', { name: currentMusicInfo.name }) }}\n    div(:class=\"$style.commentHeaderBtns\")\n      div(:class=\"$style.commentHeaderBtn\" :aria-label=\"$t('comment__refresh')\" @click=\"handleShowComment\")\n        svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" style=\"transform: rotate(45deg);\" viewBox=\"0 0 24 24\" space=\"preserve\")\n          use(xlink:href=\"#icon-refresh\")\n      div(:class=\"$style.commentHeaderBtn\" @click=\"$emit('close')\")\n        svg(version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 24 24\" space=\"preserve\")\n          use(xlink:href=\"#icon-close\")\n\n  div(:class=\"$style.commentMain\")\n    template(v-if=\"available\")\n      header(:class=\"$style.tab_header\")\n        button(type=\"button\" :class=\"[$style.commentType, { [$style.active]: tabActiveId == 'hot' }]\" @click=\"handleToggleTab('hot')\") {{ $t('comment__hot_title') }} ({{ hotComment.total }})\n        button(type=\"button\" :class=\"[$style.commentType, { [$style.active]: tabActiveId == 'new' }]\" @click=\"handleToggleTab('new')\") {{ $t('comment__new_title') }} ({{ newComment.total }})\n      main(ref=\"dom_tabMain\" :class=\"$style.tab_main\")\n        div(:class=\"$style.tab_content\")\n          div.scroll(ref=\"dom_commentHot\" :class=\"$style.tab_content_scroll\")\n            p(v-if=\"hotComment.isLoadError\" :class=\"$style.commentLabel\" style=\"cursor: pointer;\" @click=\"handleGetHotComment(currentMusicInfo, hotComment.nextPage, hotComment.limit)\") {{ $t('comment__hot_load_error') }}\n            p(v-else-if=\"hotComment.isLoading && !hotComment.list.length\" :class=\"$style.commentLabel\") {{ $t('comment__hot_loading') }}\n            comment-floor(v-if=\"!hotComment.isLoadError && hotComment.list.length\" :class=\"[$style.commentFloor, hotComment.isLoading ? $style.loading : null]\" :comments=\"hotComment.list\")\n            p(v-else-if=\"!hotComment.isLoadError && !hotComment.isLoading\" :class=\"$style.commentLabel\") {{ $t('comment__no_content') }}\n            div(:class=\"$style.pagination\")\n              material-pagination(:count=\"hotComment.total\" :btn-length=\"5\" :limit=\"hotComment.limit\" :page=\"hotComment.page\" @btn-click=\"handleToggleHotCommentPage\")\n        div(:class=\"$style.tab_content\")\n          div.scroll(ref=\"dom_commentNew\" :class=\"$style.tab_content_scroll\")\n            p(v-if=\"newComment.isLoadError\" :class=\"$style.commentLabel\" style=\"cursor: pointer;\" @click=\"handleGetNewComment(currentMusicInfo, newComment.nextPage, newComment.limit)\") {{ $t('comment__new_load_error') }}\n            p(v-else-if=\"newComment.isLoading && !newComment.list.length\" :class=\"$style.commentLabel\") {{ $t('comment__new_loading') }}\n            comment-floor(v-if=\"!newComment.isLoadError && newComment.list.length\" :class=\"[$style.commentFloor, newComment.isLoading ? $style.loading : null]\" :comments=\"newComment.list\")\n            p(v-else-if=\"!newComment.isLoadError && !newComment.isLoading\" :class=\"$style.commentLabel\") {{ $t('comment__no_content') }}\n            div(:class=\"$style.pagination\")\n              material-pagination(:count=\"newComment.total\" :btn-length=\"5\" :limit=\"newComment.limit\" :page=\"newComment.page\" @btn-click=\"handleToggleCommentPage\")\n    div(v-else :class=\"$style.unavailable\")\n      p {{ $t('comment__unavailable') }}\n</template>\n\n<script>\nimport { toOldMusicInfo } from '@renderer/utils'\nimport music from '@renderer/utils/musicSdk'\nimport CommentFloor from './CommentFloor.vue'\n\nexport default {\n  name: 'MusicComment',\n  components: {\n    CommentFloor,\n  },\n  props: {\n    show: Boolean,\n    musicInfo: {\n      type: Object,\n      required: true,\n    },\n  },\n  emits: ['close'],\n  data() {\n    return {\n      available: false,\n      currentMusicInfo: {\n        name: '',\n        singer: '',\n      },\n      tabActiveId: 'hot',\n      newComment: {\n        isLoading: false,\n        isLoadError: false,\n        page: 1,\n        total: 0,\n        maxPage: 1,\n        nextPage: 1,\n        limit: 20,\n        list: [\n        // {\n        //   text: ['123123hhh'],\n        //   userName: 'dsads',\n        //   avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',\n        //   time: '2020-10-22 22:14:17',\n        //   timeStr: '2020-10-22 22:14:17',\n        //   likedCount: 100,\n        //   reply: [],\n        // },\n        ],\n      },\n      hotComment: {\n        isLoading: true,\n        isLoadError: true,\n        page: 1,\n        total: 0,\n        maxPage: 1,\n        nextPage: 1,\n        limit: 20,\n        list: [\n        // {\n        //   text: ['123123hhh'],\n        //   userName: 'dsads',\n        //   avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',\n        //   time: '2020-10-22 22:14:17',\n        //   timeStr: '2020-10-22 22:14:17',\n        //   likedCount: 100,\n        //   reply: [\n        //     {\n        //       text: ['123123hhh'],\n        //       userName: 'dsads',\n        //       avatar: 'http://img4.kuwo.cn/star/userhead/39/52/1602393411654_512039239s.jpg',\n        //       time: '2020-10-22 22:14:17',\n        //       timeStr: '2020-10-22 22:14:17',\n        //       likedCount: 100,\n        //     },\n        //   ],\n        // },\n        ],\n      },\n    }\n  },\n  watch: {\n    show(n) {\n      if (n) this.handleShowComment()\n    },\n  },\n  mounted() {\n    this.setWidth()\n    window.addEventListener('resize', this.setWidth)\n  },\n  beforeUnmount() {\n    window.removeEventListener('resize', this.setWidth)\n  },\n  methods: {\n    setWidth() {\n      setTimeout(() => {\n        this.$refs.dom_container.style.width = Math.floor(this.$refs.dom_container.parentNode.clientWidth * 0.5) + 'px'\n\n        setTimeout(() => {\n          this.handleToggleTab(this.tabActiveId, true)\n        })\n      })\n    },\n    async getComment(musicInfo, page, limit, retryNum = 0) {\n      let resp\n      try {\n        resp = await music[musicInfo.source].comment.getComment(musicInfo, page, limit)\n      } catch (error) {\n        if (error.message == '取消请求' || ++retryNum > 2) throw error\n        resp = await this.getComment(musicInfo, page, limit, retryNum)\n      }\n      return resp\n    },\n    async getHotComment(musicInfo, page, limit, retryNum = 0) {\n      let resp\n      try {\n        resp = await music[musicInfo.source].comment.getHotComment(musicInfo, page, limit)\n      } catch (error) {\n        if (error.message == '取消请求' || ++retryNum > 2) throw error\n        resp = await this.getHotComment(musicInfo, page, limit, retryNum)\n      }\n      return resp\n    },\n    handleGetNewComment(musicInfo, page, limit) {\n      this.newComment.isLoadError = false\n      this.newComment.isLoading = true\n      this.getComment(toOldMusicInfo(musicInfo), page, limit).then(comment => {\n        this.newComment.isLoading = false\n        this.newComment.total = comment.total\n        this.newComment.maxPage = comment.maxPage\n        this.newComment.page = page\n        this.newComment.list = comment.comments\n        this.$nextTick(() => {\n          this.$refs.dom_commentNew.scrollTo(0, 0)\n        })\n      }).catch(err => {\n        console.log(err)\n        if (err.message == '取消请求') return\n        this.newComment.isLoadError = true\n        this.newComment.isLoading = false\n      })\n    },\n    handleGetHotComment(musicInfo, page, limit) {\n      this.hotComment.isLoadError = false\n      this.hotComment.isLoading = true\n      this.getHotComment(toOldMusicInfo(musicInfo), page, limit).then(hotComment => {\n        this.hotComment.isLoading = false\n        this.hotComment.total = hotComment.total\n        this.hotComment.maxPage = hotComment.maxPage\n        this.hotComment.page = page\n        this.hotComment.list = hotComment.comments\n        this.$nextTick(() => {\n          this.$refs.dom_commentHot.scrollTo(0, 0)\n        })\n      }).catch(err => {\n        console.log(err)\n        if (err.message == '取消请求') return\n        this.hotComment.isLoadError = true\n        this.hotComment.isLoading = false\n      })\n    },\n    handleShowComment() {\n      this.currentMusicInfo = 'progress' in this.musicInfo ? this.musicInfo.metadata.musicInfo : this.musicInfo\n\n      if (this.currentMusicInfo.source == 'local' || !music[this.currentMusicInfo.source].comment) {\n        this.available = false\n        return\n      }\n      this.available = true\n      // if (this.musicInfo.songmid != this.currentMusicInfo.songmid) {\n      this.hotComment.page = 1\n      this.hotComment.total = 0\n      this.hotComment.maxPage = 1\n      this.hotComment.nextPage = 1\n\n      this.newComment.page = 1\n      this.newComment.total = 0\n      this.newComment.maxPage = 1\n      this.newComment.nextPage = 1\n      // }\n      this.isShowComment = true\n\n      this.handleGetHotComment(this.currentMusicInfo, this.hotComment.page, this.hotComment.limit)\n      this.handleGetNewComment(this.currentMusicInfo, this.newComment.page, this.newComment.limit)\n    },\n    handleToggleHotCommentPage(page) {\n      this.hotComment.nextPage = page\n      this.handleGetHotComment(this.currentMusicInfo, page, this.hotComment.limit)\n    },\n    handleToggleCommentPage(page) {\n      this.newComment.nextPage = page\n      this.handleGetNewComment(this.currentMusicInfo, page, this.newComment.limit)\n    },\n    handleToggleTab(id, force) {\n      if (!this.available || (!force && this.tabActiveId == id)) return\n      switch (id) {\n        case 'hot':\n          this.$refs.dom_tabMain.scrollLeft = 0\n          break\n        case 'new':\n          this.$refs.dom_tabMain.scrollLeft = this.$refs.dom_tabMain.clientWidth\n          break\n      }\n      this.tabActiveId = id\n    },\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.comment {\n  display: flex;\n  flex-flow: column nowrap;\n  transition: @transition-normal;\n  transition-property: transform,opacity;\n  transform-origin: 100%;\n  overflow: hidden;\n}\n.commentHeader {\n  flex: none;\n  padding-bottom: 5px;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  // border-bottom: 1px solid #eee;\n  h3 {\n    font-size: 14px;\n    .mixin-ellipsis-1();\n    line-height: 1.2;\n  }\n}\n.commentHeaderBtns {\n  flex: 1 0 auto;\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: flex-end;\n  color: var(--color-primary);\n}\n.commentHeaderBtn {\n  height: 22px;\n  width: 22px;\n  cursor: pointer;\n  transition: opacity @transition-normal;\n\n  +.commentHeaderBtn {\n    margin-left: 5px;\n  }\n\n  &:hover {\n    opacity: .7;\n  }\n}\n.commentMain {\n  flex: auto;\n  background-color: var(--color-primary-light-400-alpha-700);\n  border-radius: 4px;\n  display: flex;\n  flex-direction: column;\n}\n.tab_header {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 15px;\n  padding-left: 15px;\n  padding-right: 10px;\n}\n.tab_main {\n  flex: auto;\n  display: flex;\n  flex-flow: row nowrap;\n  overflow: hidden;\n  scroll-snap-type: x mandatory;\n  scroll-behavior: smooth;\n}\n.tab_content {\n  flex-shrink: 0;\n  width: 100%;\n  position: relative;\n}\n.tab_content_scroll {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  padding-left: 15px;\n  padding-right: 10px;\n  scroll-behavior: smooth;\n}\n.commentLabel {\n  padding: 15px;\n  color: var(--color-font-label);\n  font-size: 14px;\n}\n.commentType {\n  padding: 5px;\n  margin: 5px 0;\n  font-size: 13px;\n  background: none;\n  border: none;\n  cursor: pointer;\n  transition: @transition-normal;\n  transition-property: opacity, color;\n  &:hover {\n    opacity: .7;\n  }\n  &.active {\n    color: var(--color-primary);\n  }\n}\n.commentFloor {\n  opacity: 1;\n  transition: opacity @transition-normal;\n\n  &.loading {\n    opacity: .4;\n  }\n}\n.pagination {\n  padding: 10px 0;\n}\n\n.unavailable {\n  flex: auto;\n  padding-top: 10%;\n  text-align: center;\n  font-size: 14px;\n  color: var(--color-font-label);\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/index.vue",
    "content": "<template lang=\"pug\">\ntransition(enter-active-class=\"animated slideInRight\" leave-active-class=\"animated slideOutDown\" @after-enter=\"handleAfterEnter\" @after-leave=\"handleAfterLeave\")\n  div(v-if=\"isShowPlayerDetail\" :class=\"[$style.container, { fullscreen: isFullscreen }]\" @contextmenu=\"handleContextMenu\")\n    div(:class=\"$style.bg\")\n    //- div(:class=\"$style.bg\" :style=\"bgStyle\")\n    //- div(:class=\"$style.bg2\")\n    ControlBtnsLeftHeader(v-if=\"appSetting['common.controlBtnPosition'] == 'left'\")\n    ControlBtnsRightHeader(v-else)\n    div(:class=\"[$style.main, {[$style.showComment]: isShowPlayComment}]\")\n      div.left(:class=\"$style.left\")\n        //- div(:class=\"$style.info\")\n        div(:class=\"$style.info\")\n          img(v-if=\"musicInfo.pic\" :class=\"$style.img\" :src=\"musicInfo.pic\")\n          div.description(:class=\"['scroll', $style.description]\")\n            p {{ $t('player__music_name') }}{{ musicInfo.name }}\n            p {{ $t('player__music_singer') }}{{ musicInfo.singer }}\n            p(v-if=\"musicInfo.album\") {{ $t('player__music_album') }}{{ musicInfo.album }}\n\n      transition(enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\")\n        LyricPlayer(v-if=\"visibled\")\n      music-comment(v-if=\"visibled\" :class=\"$style.comment\" :show=\"isShowPlayComment\" :music-info=\"playMusicInfo.musicInfo\" @close=\"hideComment\")\n    transition(enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\")\n      play-bar(v-if=\"visibled\")\n    transition(enter-active-class=\"animated-slow fadeIn\" leave-active-class=\"animated-slow fadeOut\")\n      common-audio-visualizer(v-if=\"appSetting['player.audioVisualization'] && visibled\")\n</template>\n\n\n<script>\nimport { ref, watch } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport {\n  isShowPlayerDetail,\n  isShowPlayComment,\n  musicInfo,\n  playMusicInfo,\n} from '@renderer/store/player/state'\nimport {\n  setShowPlayerDetail,\n  setShowPlayComment,\n  setShowPlayLrcSelectContentLrc,\n} from '@renderer/store/player/action'\nimport LyricPlayer from './LyricPlayer.vue'\nimport PlayBar from './PlayBar.vue'\nimport MusicComment from './components/MusicComment/index.vue'\nimport ControlBtnsLeftHeader from './ControlBtnsLeftHeader.vue'\nimport ControlBtnsRightHeader from './ControlBtnsRightHeader.vue'\nimport { registerAutoHideMounse, unregisterAutoHideMounse } from './autoHideMounse'\nimport { appSetting } from '@renderer/store/setting'\nimport { closeWindow, maxWindow, minWindow, setFullScreen } from '@renderer/utils/ipc'\n\nexport default {\n  name: 'CorePlayDetail',\n  components: {\n    ControlBtnsLeftHeader,\n    ControlBtnsRightHeader,\n    LyricPlayer,\n    PlayBar,\n    MusicComment,\n  },\n  setup() {\n    const visibled = ref(false)\n\n    let clickTime = 0\n\n    const hide = () => {\n      setShowPlayerDetail(false)\n    }\n    const handleContextMenu = () => {\n      if (window.performance.now() - clickTime > 400) {\n        clickTime = window.performance.now()\n        return\n      }\n      clickTime = 0\n      hide()\n    }\n\n    const hideComment = () => {\n      setShowPlayComment(false)\n    }\n\n    const handleAfterEnter = () => {\n      if (isFullscreen.value) registerAutoHideMounse()\n\n      visibled.value = true\n    }\n\n    const handleAfterLeave = () => {\n      setShowPlayLrcSelectContentLrc(false)\n      hideComment(false)\n      visibled.value = false\n\n      unregisterAutoHideMounse()\n    }\n\n    watch(isFullscreen, isFullscreen => {\n      (isFullscreen ? registerAutoHideMounse : unregisterAutoHideMounse)()\n    })\n\n\n    return {\n      appSetting,\n      playMusicInfo,\n      isShowPlayerDetail,\n      isShowPlayComment,\n      musicInfo,\n      hide,\n      handleContextMenu,\n      hideComment,\n      handleAfterEnter,\n      handleAfterLeave,\n      visibled,\n      isFullscreen,\n      fullscreenExit() {\n        void setFullScreen(false).then((fullscreen) => {\n          isFullscreen.value = fullscreen\n        })\n      },\n      min() {\n        minWindow()\n      },\n      max() {\n        maxWindow()\n      },\n      close() {\n        closeWindow()\n      },\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@control-btn-width: @height-toolbar * .26;\n\n.container {\n  position: absolute;\n  display: flex;\n  flex-flow: column nowrap;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  background-color: var(--color-content-background);\n  z-index: 10;\n  // -webkit-app-region: drag;\n  overflow: hidden;\n  border-radius: @radius-border;\n  color: var(--color-font);\n  // border-left: 12px solid var(--color-primary-alpha-900);\n  -webkit-app-region: no-drag;\n  contain: strict;\n\n  box-sizing: border-box;\n\n  * {\n    box-sizing: border-box;\n  }\n}\n.bg {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  background: var(--background-image) var(--background-image-position) no-repeat;\n  background-size: var(--background-image-size);\n  // background-size: 110% 110%;\n  // filter: blur(60px);\n  opacity: .7;\n  z-index: -1;\n  &:before {\n    content: '';\n    display: block;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-app-background);\n  }\n  &:after {\n    position: absolute;\n    left: 0;\n    top: 0;\n    content: '';\n    display: block;\n    width: 100%;\n    height: 100%;\n    background-color: var(--color-main-background);\n  }\n}\n// .bg2 {\n//   position: absolute;\n//   width: 100%;\n//   height: 100%;\n//   top: 0;\n//   left: 0;\n//   z-index: -1;\n//   background-color: rgba(255, 255, 255, .8);\n// }\n\n.main {\n  flex: auto;\n  min-height: 0;\n  overflow: hidden;\n  display: flex;\n  margin: 0 30px;\n  position: relative;\n\n  &.showComment {\n    :global {\n      .left {\n        flex-basis: 18%;\n        .description p {\n          font-size: 12px;\n        }\n      }\n      .right {\n        flex-basis: 30%;\n        .lyricSelectContent {\n          font-size: 14px;\n        }\n      }\n      .comment {\n        opacity: 1;\n        transform: scaleX(1);\n      }\n    }\n  }\n}\n.left {\n  flex: 0 0 40%;\n  display: flex;\n  flex-flow: column nowrap;\n  align-items: center;\n  padding: 13px;\n  overflow: hidden;\n  transition: flex-basis @transition-normal;\n}\n\n.info {\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: flex-start;\n  max-width: 300px;\n  min-height: 0;\n}\n.img {\n  max-width: 100%;\n  max-height: 80%;\n  min-width: 100%;\n  box-shadow: 0 0 6px var(--color-primary-alpha-500);\n  border-radius: 6px;\n  opacity: .8;\n}\n.description {\n  max-width: 300px;\n  margin-top: 15px;\n  padding-bottom: 15px;\n  min-height: 0;\n  p {\n    line-height: 1.5;\n    font-size: 14px;\n    overflow-wrap: break-word;\n  }\n}\n\n\n.comment {\n  position: absolute;\n  right: 0;\n  top: 0;\n  width: 50%;\n  height: 100%;\n  opacity: 1;\n  margin-left: 10px;\n  transform: scaleX(0);\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/PlayDetail/useSelectAllLrc.js",
    "content": "import { ref, onBeforeUnmount, onMounted } from '@common/utils/vueTools'\n\nexport default () => {\n  const dom_lrc_select_content = ref()\n  const handle_key_mod_a_down = ({ event }) => {\n    if (event.target.tagName == 'INPUT' || !dom_lrc_select_content.value || document.activeElement != dom_lrc_select_content.value) return\n    event.preventDefault()\n    if (event.repeat) return\n\n    let selection = window.getSelection()\n    let range = document.createRange()\n    range.selectNodeContents(dom_lrc_select_content.value)\n    selection.removeAllRanges()\n    selection.addRange(range)\n  }\n\n  onMounted(() => {\n    window.key_event.on('key_mod+a_down', handle_key_mod_a_down)\n  })\n  onBeforeUnmount(() => {\n    window.key_event.off('key_mod+a_down', handle_key_mod_a_down)\n  })\n\n  return dom_lrc_select_content\n}\n"
  },
  {
    "path": "src/renderer/components/layout/SyncAuthCodeModal.vue",
    "content": "<template>\n  <material-modal :show=\"sync.isShowAuthCodeModal\" :bg-close=\"false\" @close=\"handleClose\" @after-enter=\"$refs.input.focus()\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('sync__auth_code_title') }}</h2>\n      <base-input\n        ref=\"input\"\n        v-model=\"authCode\"\n        :class=\"$style.input\"\n        :placeholder=\"$t('sync__auth_code_input_tip')\"\n        @submit=\"handleSubmit\" @blur=\"verify\"\n      />\n      <div :class=\"$style.footer\">\n        <base-btn :class=\"$style.btn\" @click=\"handleSubmit\">{{ $t('btn_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { sendSyncAction } from '@renderer/utils/ipc'\n\nexport default {\n  setup() {\n    const authCode = ref('')\n    const handleClose = () => {\n      sync.isShowAuthCodeModal = false\n    }\n    const verify = () => {\n      if (authCode.value.length > 100) authCode.value = authCode.value.substring(0, 100)\n      return authCode.value\n    }\n    const handleSubmit = () => {\n      let code = verify()\n      if (code == '') return\n      authCode.value = ''\n      handleClose()\n      sendSyncAction({\n        action: 'enable_client',\n        data: {\n          enable: appSetting['sync.enable'],\n          host: appSetting['sync.client.host'],\n          authCode: code,\n        },\n      }).catch(err => {\n        console.log(err)\n      })\n    }\n    return {\n      sync,\n      authCode,\n      handleClose,\n      verify,\n      handleSubmit,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 0 15px;\n  max-width: 530px;\n  min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    word-break: break-all;\n    // text-align: center;\n    padding: 15px 0 8px;\n  }\n}\n\n.input {\n  // width: 100%;\n  // height: 26px;\n  padding: 8px 8px;\n}\n.footer {\n  margin: 20px 0 15px auto;\n}\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n\n  +.btn {\n    margin-left: 10px;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/SyncModeModal.vue",
    "content": "<template>\n  <material-modal :show=\"sync.isShowSyncMode\" :bg-close=\"false\" :close-btn=\"false\" @close=\"handleClose(false)\">\n    <main v-if=\"sync.type == 'list'\" :class=\"$style.main\">\n      <h2>{{ $t('sync__list_title', { name: sync.deviceName }) }}</h2>\n      <div class=\"scroll\" :class=\"$style.content\">\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__merge_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('merge_local_remote')\">{{ $t('sync__merge_btn_local_remote') }}</base-btn>\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('merge_remote_local')\">{{ $t('sync__merge_btn_remote_local') }}</base-btn>\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__overwrite_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('overwrite_local_remote')\">{{ $t('sync__overwrite_btn_local_remote') }}</base-btn>\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('overwrite_remote_local')\">{{ $t('sync__overwrite_btn_remote_local') }}</base-btn>\n          </dd>\n          <dd style=\"font-size: 14px; margin-top: 5px;\">\n            <base-checkbox id=\"sync_mode_modal_isOverwrite\" v-model=\"isOverwrite\" :label=\"$t('sync__overwrite')\" />\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__other_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <!-- <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('none')\">{{ $t('sync__overwrite_btn_none') }}</base-btn> -->\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('cancel')\">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dd>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__merge_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__list_merge_tip_desc') }}</p>\n            </section>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__overwrite_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__list_overwrite_tip_desc') }}</p>\n            </section>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__other_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__list_other_tip_desc') }}</p>\n            </section>\n          </dd>\n        </dl>\n      </div>\n    </main>\n    <main v-else-if=\"sync.type == 'dislike'\" :class=\"$style.main\">\n      <h2>{{ $t('sync__dislike_title', { name: sync.deviceName }) }}</h2>\n      <div class=\"scroll\" :class=\"$style.content\">\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__merge_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('merge_local_remote')\">{{ $t('sync__merge_btn_local_remote') }}</base-btn>\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('merge_remote_local')\">{{ $t('sync__merge_btn_remote_local') }}</base-btn>\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__overwrite_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('overwrite_local_remote')\">{{ $t('sync__overwrite_btn_local_remote') }}</base-btn>\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('overwrite_remote_local')\">{{ $t('sync__overwrite_btn_remote_local') }}</base-btn>\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dt :class=\"$style.label\">{{ $t('sync__other_label') }}</dt>\n          <dd :class=\"$style.btns\">\n            <!-- <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('none')\">{{ $t('sync__overwrite_btn_none') }}</base-btn> -->\n            <base-btn :class=\"$style.btn\" @click=\"handleSelectMode('cancel')\">{{ $t('sync__overwrite_btn_cancel') }}</base-btn>\n          </dd>\n        </dl>\n        <dl :class=\"$style.btnGroup\">\n          <dd>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__merge_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__dislike_merge_tip_desc') }}</p>\n            </section>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__overwrite_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__dislike_overwrite_tip_desc') }}</p>\n            </section>\n            <section :class=\"$style.tipGroup\">\n              <h3 :class=\"$style.title\">{{ $t('sync__other_tip') }}</h3>\n              <p :class=\"$style.tip\">{{ $t('sync__dislike_other_tip_desc') }}</p>\n            </section>\n          </dd>\n        </dl>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { sendSyncAction } from '@renderer/utils/ipc'\n\nexport default {\n  setup() {\n    const isOverwrite = ref(false)\n    const handleClose = () => {\n      sync.isShowSyncMode = false\n    }\n    const handleSelectMode = (mode) => {\n      if (sync.type == 'list') {\n        if (mode.startsWith('overwrite') && isOverwrite.value) mode += '_full'\n      }\n      void sendSyncAction({ action: 'select_mode', data: { type: sync.type, mode } })\n      handleClose()\n    }\n    return {\n      sync,\n      isOverwrite,\n      handleClose,\n      handleSelectMode,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px;\n  max-width: 700px;\n  min-width: 200px;\n  min-height: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  h2 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n\n.content {\n  flex: auto;\n  padding: 15px 0 5px;\n  padding-right: 5px;\n  .btnGroup + .btnGroup {\n    margin-top: 10px;\n  }\n  .label {\n    color: var(--color-font-label);\n    font-size: 14px;\n    line-height: 2;\n  }\n  .desc {\n    line-height: 1.5;\n    font-size: 14px;\n    text-align: justify;\n  }\n\n  .tipGroup {\n    display: flex;\n    flex-direction: row;\n    font-size: 12px;\n\n    + .tipGroup {\n      margin-top: 5px;\n    }\n\n    .title {\n      white-space: nowrap;\n      font-weight: bold;\n      margin-right: 5px;\n    }\n\n    .tip {\n      line-height: 1.3;\n    }\n  }\n}\n\n.btns {\n  display: flex;\n  align-items: center;\n}\n.btn {\n  display: block;\n  white-space: nowrap;\n  +.btn {\n    margin-left: 15px;\n  }\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/Toolbar/ControlBtns.vue",
    "content": "<template>\n  <div v-show=\"!isFullscreen\" ref=\"dom_btns\" :class=\"$style.control\">\n    <button type=\"button\" :class=\"[$style.btn, $style.min]\" :aria-label=\"$t('min')\" ignore-tip :title=\"$t('min')\" @click=\"minWindow\">\n      <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"60%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use xlink:href=\"#icon-window-minimize-2\" />\n      </svg>\n    </button>\n    <button type=\"button\" :class=\"[$style.btn, $style.close]\" :aria-label=\"$t('close')\" ignore-tip :title=\"$t('close')\" @click=\"closeWindow\">\n      <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"60%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n        <use xlink:href=\"#icon-window-close-2\" />\n      </svg>\n    </button>\n  </div>\n</template>\n\n<script setup>\nimport { minWindow, closeWindow } from '@renderer/utils/ipc'\nimport { onMounted, onBeforeUnmount, ref, useCssModule } from '@common/utils/vueTools'\n// import { getRandom } from '../../utils'\nimport { isFullscreen } from '@renderer/store'\n\nconst dom_btns = ref()\n\nconst cssModule = useCssModule()\n\nconst handle_focus = () => {\n  if (!dom_btns.value) return\n  for (const node of dom_btns.value.childNodes) {\n    if (node.tagName != 'BUTTON') continue\n    node.classList.remove(cssModule.hover)\n  }\n}\nconst getBtnEl = (el) => el.tagName == 'BUTTON' || !el ? el : getBtnEl(el.parentNode)\nconst handle_mouseover = (event) => {\n  const btn = getBtnEl(event.target)\n  if (!btn) return\n  btn.classList.add(cssModule.hover)\n}\nconst handle_mouseout = (event) => {\n  const btn = getBtnEl(event.target)\n  if (!btn) return\n  btn.classList.remove(cssModule.hover)\n}\n\n\nonMounted(() => {\n  window.app_event.on('focus', handle_focus)\n  dom_btns.value.addEventListener('mouseover', handle_mouseover)\n  dom_btns.value.addEventListener('mouseout', handle_mouseout)\n})\nonBeforeUnmount(() => {\n  window.app_event.off('focus', handle_focus)\n  dom_btns.value.removeEventListener('mouseover', handle_mouseover)\n  dom_btns.value.removeEventListener('mouseout', handle_mouseout)\n})\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.control {\n  display: flex;\n  align-self: flex-start;\n  -webkit-app-region: no-drag;\n  height: 30px;\n\n  .btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: relative;\n    width: 46px;\n    height: 30px;\n    background: none;\n    border: none;\n    outline: none;\n    padding: 1px;\n    cursor: pointer;\n    color: var(--color-font-label);\n    transition: background-color 0.2s ease-in-out;\n    &.hover {\n      &.min, &.max {\n        background-color: var(--color-button-background-hover);\n      }\n      &.close {\n        background-color: var(--color-btn-close);\n      }\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/Toolbar/SearchInput.vue",
    "content": "<template>\n  <material-search-input v-model=\"searchText\" :list=\"tipList\" :visible-list=\"visibleList\" @event=\"handleEvent\" />\n</template>\n\n<script>\nimport music from '@renderer/utils/musicSdk'\nimport { debounce } from '@common/utils'\nimport {\n  ref,\n  watch,\n  nextTick,\n} from '@common/utils/vueTools'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\nimport { appSetting } from '@renderer/store/setting'\nimport { searchText as _searchText } from '@renderer/store/search/state'\nimport { setSearchText } from '@renderer/store/search/action'\nimport { getSearchSetting } from '@renderer/utils/data'\n\nexport default {\n  setup() {\n    const searchText = ref('')\n    const visibleList = ref(false)\n    const tipList = ref([])\n    let isFocused = false\n    let prevTempSearchSource = ''\n\n    const route = useRoute()\n    const router = useRouter()\n\n    watch(() => route.name, (newValue, oldValue) => {\n      if (oldValue == 'Search' && newValue != 'SongListDetail') {\n        setTimeout(() => {\n          if (appSetting['odc.isAutoClearSearchInput'] && searchText.value) searchText.value = ''\n          if (appSetting['odc.isAutoClearSearchList']) setSearchText('')\n        })\n      }\n    })\n\n    watch(_searchText, (newValue, oldValue) => {\n      searchText.value = newValue\n      if (newValue !== searchText.value) searchText.value = newValue\n    })\n    watch(searchText, () => {\n      handleTipSearch()\n    })\n\n\n    const tipSearch = debounce(async() => {\n      if (searchText.value === '' && prevTempSearchSource) {\n        tipList.value = []\n        music[prevTempSearchSource].tipSearch.cancelTipSearch()\n        return\n      }\n      const { temp_source } = await getSearchSetting()\n      prevTempSearchSource ||= temp_source\n      music[prevTempSearchSource].tipSearch.search(searchText.value).then(list => {\n        tipList.value = list\n      }).catch(() => {})\n    }, 50)\n\n    const handleTipSearch = () => {\n      if (!visibleList.value && isFocused) visibleList.value = true\n      tipSearch()\n    }\n\n    const handleSearch = () => {\n      visibleList.value &&= false\n      if (!searchText.value && route.path != '/search') {\n        setSearchText('')\n        return\n      }\n      setTimeout(() => {\n        router.push({\n          path: '/search',\n          query: {\n            text: searchText.value,\n          },\n        }).catch(_ => _)\n      }, searchText.value ? 200 : 0)\n    }\n\n    const handleEvent = ({ action, data }) => {\n      switch (action) {\n        case 'focus':\n          isFocused = true\n          visibleList.value ||= true\n          if (searchText.value) handleTipSearch()\n          break\n        case 'blur':\n          isFocused = false\n          setTimeout(() => {\n            visibleList.value &&= false\n          }, 50)\n          break\n        case 'submit':\n          handleSearch()\n          break\n        case 'listClick':\n          searchText.value = tipList.value[data]\n          void nextTick(handleSearch)\n      }\n    }\n\n    return {\n      searchText,\n      visibleList,\n      tipList,\n      handleEvent,\n    }\n  },\n}\n\n</script>\n"
  },
  {
    "path": "src/renderer/components/layout/Toolbar/index.vue",
    "content": "<template>\n  <div :class=\"[$style.toolbar, { [$style.fullscreen]: isFullscreen }, appSetting['common.controlBtnPosition'] == 'left' ? $style.controlBtnLeft : $style.controlBtnRight]\">\n    <SearchInput />\n    <div v-if=\"appSetting['common.controlBtnPosition'] == 'left'\" :class=\"$style.logo\">L X</div>\n    <ControlBtns v-else />\n  </div>\n</template>\n\n<script setup>\nimport { isFullscreen } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport ControlBtns from './ControlBtns.vue'\nimport SearchInput from './SearchInput.vue'\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.toolbar {\n  display: flex;\n  height: @height-toolbar;\n  align-items: center;\n  justify-content: space-between;\n  padding-left: 15px;\n  -webkit-app-region: drag;\n  z-index: 2;\n\n  &.fullscreen {\n    -webkit-app-region: no-drag;\n    .logo {\n      display: none;\n    }\n  }\n\n  &.controlBtnLeft {\n    .control {\n      display: none;\n    }\n  }\n  &.controlBtnRight {\n    justify-content: space-between;\n  }\n}\n\n.logo {\n  box-sizing: border-box;\n  padding: 0 @height-toolbar * .4;\n  height: @height-toolbar;\n  color: var(--color-primary);\n  flex: none;\n  text-align: center;\n  line-height: @height-toolbar;\n  font-weight: bold;\n  // -webkit-app-region: no-drag;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/layout/UpdateModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"versionInfo.showModal\" max-width=\"60%\" @close=\"handleClose\")\n  main(v-if=\"versionInfo.isLatest\" :class=\"$style.main\")\n    h2 🎉 已是最新版本 🎉\n    div.scroll.select(:class=\"$style.info\")\n      div(:class=\"$style.current\")\n        h3 最新版本：{{ versionInfo.newVersion?.version }}\n        h3 当前版本：{{ versionInfo.version }}\n        h3 版本变化：\n        pre(:class=\"$style.desc\" v-text=\"versionInfo.newVersion?.desc\")\n    div(:class=\"$style.footer\")\n      div(:class=\"$style.btns\")\n        base-btn(v-if=\"versionInfo.status == 'checking'\" :class=\"$style.btn\" disabled) 检查更新中...\n        base-btn(v-else :class=\"$style.btn\" @click=\"handleCheckUpdate\") 重新检查更新\n  main(v-else-if=\"versionInfo.isUnknown\" :class=\"$style.main\")\n    h2 ❓ 获取最新版本信息失败 ❓\n    div.scroll.select(:class=\"$style.info\")\n      div(:class=\"$style.current\")\n        h3 当前版本：{{ versionInfo.version }}\n        div(:class=\"$style.desc\")\n          p 更新信息获取失败，可能是无法访问 GitHub 导致的，请手动检查更新！\n          p\n            | 检查方法：打开\n            base-btn(min aria-label=\"点击打开\" @click=\"handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')\") 软件发布页\n            | ，查看「Latest」发布的\n            strong 版本号\n            | 与当前版本({{ versionInfo.version }})对比是否一致。\n          p 若一致则不必理会该弹窗，直接关闭即可；否则请手动下载新版本更新。\n    div(:class=\"$style.footer\")\n      div(:class=\"$style.btns\")\n        base-btn(v-if=\"versionInfo.status == 'error'\" :class=\"$style.btn2\" @click=\"handleCheckUpdate\") 重新检查更新\n        base-btn(v-else :class=\"$style.btn2\" disabled) 检查更新中...\n        base-btn(:disabled=\"disabledIgnoreFailBtn\" :class=\"$style.btn2\" @click=\"handleIgnoreFailTipClick\") 一个星期内不再提醒\n  main(v-else-if=\"versionInfo.status == 'downloaded'\" :class=\"$style.main\")\n    h2 🚀程序更新🚀\n\n    div.scroll.select(:class=\"$style.info\")\n      div(:class=\"$style.current\")\n        h3 最新版本：{{ versionInfo.newVersion?.version }}\n        h3 当前版本：{{ versionInfo.version }}\n        h3 版本变化：\n        pre(:class=\"$style.desc\" v-text=\"versionInfo.newVersion?.desc\")\n      div(v-if=\"history.length\" :class=\"[$style.history, $style.desc]\")\n        h3 历史版本：\n        div(v-for=\"(ver, index) in history\" :key=\"index\" :class=\"$style.item\")\n          h4 v{{ ver.version }}\n          pre(v-text=\"ver.desc\")\n    div(:class=\"$style.footer\")\n      div(:class=\"$style.desc\")\n        p 新版本已下载完毕，\n        p\n          | 你可以选择\n          strong 立即重启更新\n          | 或稍后\n          strong 关闭程序时\n          | 自动更新~\n      div(:class=\"$style.btns\")\n        base-btn(:class=\"$style.btn\" @click=\"handleRestartClick\") 立即重启更新\n  main(v-else :class=\"$style.main\")\n    h2 🌟发现新版本🌟\n    div.scroll.select(:class=\"$style.info\")\n      div(:class=\"$style.current\")\n        h3 最新版本：{{ versionInfo.newVersion?.version }}\n        h3 当前版本：{{ versionInfo.version }}\n        h3 版本变化：\n        pre(:class=\"$style.desc\" v-text=\"versionInfo.newVersion?.desc\")\n      div(v-if=\"history.length\" :class=\"[$style.history, $style.desc]\")\n        h3 历史版本：\n        div(v-for=\"(ver, index) in history\" :key=\"index\" :class=\"$style.item\")\n          h4 v{{ ver.version }}\n          pre(v-text=\"ver.desc\")\n\n    div(:class=\"$style.footer\")\n      div(:class=\"$style.desc\")\n        p 发现有新版本啦，你可以选择自动更新或手动更新。\n        p 手动更新可以去&nbsp;\n          strong.hover.underline(aria-label=\"点击打开\" @click=\"handleOpenUrl('https://github.com/lyswhut/lx-music-desktop/releases')\") 软件发布页\n          | 下载。\n        p 若遇到问题可以阅读\n          strong.hover.underline(aria-label=\"点击打开\" @click=\"handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')\") 桌面版常见问题\n          | 。\n        p(v-if=\"progress\") 当前下载进度：{{ progress }}\n        p(v-else) &nbsp;\n      div(:class=\"$style.btns\")\n        base-btn(:class=\"$style.btn2\" @click=\"handleIgnoreClick\") {{ isIgnored ? '取消忽略' : '忽略更新该版本' }}\n        base-btn(v-if=\"versionInfo.status == 'downloading'\" :class=\"$style.btn2\" disabled) 下载更新中...\n        base-btn(v-else :class=\"$style.btn2\" @click=\"handleDownloadClick\") 下载更新\n</template>\n\n<script>\nimport { compareVer, sizeFormate } from '@common/utils'\nimport { openUrl, clipboardWriteText } from '@common/utils/electron'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { versionInfo } from '@renderer/store'\nimport { getIgnoreVersion, saveIgnoreVersion, quitUpdate, downloadUpdate, checkUpdate } from '@renderer/utils/ipc'\n\nexport default {\n  setup() {\n    return {\n      versionInfo,\n    }\n  },\n  data() {\n    return {\n      ignoreVersion: null,\n      disabledIgnoreFailBtn: true,\n    }\n  },\n  computed: {\n    history() {\n      if (!this.versionInfo.newVersion?.history) return []\n      let arr = []\n      let currentVer = this.versionInfo.version\n      this.versionInfo.newVersion?.history.forEach(ver => {\n        if (compareVer(currentVer, ver.version) < 0) arr.push(ver)\n      })\n\n      return arr\n    },\n    progress() {\n      return this.versionInfo.status == 'downloading'\n        ? this.versionInfo.downloadProgress\n          ? `${this.versionInfo.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(this.versionInfo.downloadProgress.transferred)}/${sizeFormate(this.versionInfo.downloadProgress.total)} - ${sizeFormate(this.versionInfo.downloadProgress.bytesPerSecond)}/s`\n          : '处理更新中...'\n        : ''\n    },\n    isIgnored() {\n      return this.ignoreVersion == this.versionInfo.newVersion?.version\n    },\n  },\n  created() {\n    void getIgnoreVersion().then(version => {\n      this.ignoreVersion = version\n    })\n    this.disabledIgnoreFailBtn = Date.now() - parseInt(localStorage.getItem('update__check_failed_tip') ?? '0') < 7 * 86400000\n  },\n  methods: {\n    handleClose() {\n      versionInfo.showModal = false\n    },\n    handleOpenUrl(url) {\n      void openUrl(url)\n    },\n    handleRestartClick(event) {\n      this.handleClose()\n      event.target.disabled = true\n      quitUpdate()\n    },\n    handleCopy(text) {\n      clipboardWriteText(text)\n    },\n    async handleIgnoreClick() {\n      if (this.isIgnored) {\n        saveIgnoreVersion(this.ignoreVersion = null)\n        return\n      }\n\n      if (this.history.length >= 2) {\n        if (await dialog.confirm({\n          message: window.i18n.t('update__ignore_tip', { num: this.history.length + 1 }),\n          cancelButtonText: window.i18n.t('update__ignore_cancel'),\n          confirmButtonText: window.i18n.t('update__ignore_confirm'),\n        })) {\n          setTimeout(() => {\n            void dialog({\n              message: window.i18n.t('update__ignore_confirm_tip'),\n              confirmButtonText: window.i18n.t('update__ignore_confirm_tip_confirm'),\n            })\n          }, 500)\n          return\n        }\n      }\n      saveIgnoreVersion(this.ignoreVersion = this.versionInfo.newVersion?.version)\n      // saveIgnoreVersion(this.versionInfo.newVersion?.version)\n      // this.handleClose()\n    },\n    handleDownloadClick() {\n      if (this.isIgnored) saveIgnoreVersion(this.ignoreVersion = null)\n      versionInfo.status = 'downloading'\n      downloadUpdate()\n    },\n    handleCheckUpdate() {\n      if (this.isIgnored) saveIgnoreVersion(this.ignoreVersion = null)\n      versionInfo.status = 'checking'\n      versionInfo.reCheck = true\n      checkUpdate()\n    },\n    handleIgnoreFailTipClick() {\n      localStorage.setItem('update__check_failed_tip', Date.now().toString())\n      this.disabledIgnoreFailBtn = true\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  position: relative;\n  padding: 15px 0;\n  // max-width: 450px;\n  min-width: 300px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  overflow: hidden;\n  // overflow-y: auto;\n  * {\n    box-sizing: border-box;\n  }\n  h2 {\n    flex: 0 0 none;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    margin-bottom: 15px;\n  }\n  h3 {\n    font-size: 14px;\n    line-height: 1.3;\n  }\n  pre {\n    white-space: pre-wrap;\n    text-align: justify;\n    margin-top: 10px;\n  }\n}\n\n.info {\n  flex: 1 1 auto;\n  font-size: 14px;\n  line-height: 1.5;\n  overflow-y: auto;\n  height: 100%;\n  padding: 0 15px;\n}\n.current {\n  > p {\n    padding-left: 15px;\n  }\n}\n\n.desc {\n  h3, h4 {\n    font-weight: bold;\n  }\n  h3 {\n    padding: 5px 0 3px;\n  }\n  ul {\n    list-style: initial;\n    padding-inline-start: 30px;\n  }\n  p {\n    font-size: 14px;\n    line-height: 1.5;\n  }\n}\n\n.history {\n  h3 {\n    padding-top: 15px;\n  }\n\n  .item {\n    h3 {\n      padding: 5px 0 3px;\n    }\n    padding: 0 15px;\n    + .item {\n      padding-top: 15px;\n    }\n    h4 {\n      font-weight: 700;\n    }\n    > p {\n      padding-left: 15px;\n    }\n  }\n\n}\n.footer {\n  flex: 0 0 none;\n  padding: 0 15px;\n  .desc {\n    padding-top: 10px;\n    font-size: 13px;\n    color: var(--color-primary-font);\n    line-height: 1.25;\n\n    p {\n      font-size: 13px;\n      color: var(--color-primary-font);\n      line-height: 1.25;\n    }\n  }\n}\n.btns {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 15px;\n}\n\n.btn {\n  margin-top: 10px;\n  display: block;\n  width: 100%;\n}\n.btn2 {\n  margin-top: 10px;\n  display: block;\n  width: 50%;\n}\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/components/layout/View.vue",
    "content": "<template>\n  <div :class=\"$style.view\">\n    <router-view v-slot=\"{ Component }\">\n      <!-- <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated-fast fadeOut\"> -->\n      <component :is=\"Component\" class=\"view-container\" />\n      <!-- </transition> -->\n    </router-view>\n  </div>\n</template>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.view {\n  position: relative;\n  z-index: 1;\n  > :global(.view-container) {\n    position: absolute !important;\n    left: 0;\n    top: 0;\n    height: 100%;\n    width: 100%;\n  }\n  // background: #fff;\n  // overflow: hidden;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/ListButtons.vue",
    "content": "<template>\n  <div :class=\"$style.btns\">\n    <button v-if=\"playBtn\" type=\"button\" :aria-label=\"$t('list__play')\" @contextmenu.capture.stop @click.stop=\"handleClick('play')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 287.386 287.386\" space=\"preserve\">\n        <use xlink:href=\"#icon-testPlay\" />\n      </svg>\n    </button>\n    <button v-if=\"listAddBtn\" type=\"button\" :aria-label=\"$t('list__add_to')\" @contextmenu.capture.stop @click.stop=\"handleClick('listAdd')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 42 42\" space=\"preserve\">\n        <use xlink:href=\"#icon-addTo\" />\n      </svg>\n    </button>\n    <button v-if=\"downloadBtn && appSetting['download.enable']\" type=\"button\" :aria-label=\"$t('list__download')\" @contextmenu.capture.stop @click.stop=\"handleClick('download')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 475.078 475.077\" space=\"preserve\">\n        <use xlink:href=\"#icon-download\" />\n      </svg>\n    </button>\n    <button v-if=\"startBtn\" type=\"button\" :aria-label=\"$t('list__start')\" @contextmenu.capture.stop @click.stop=\"handleClick('start')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n        <use xlink:href=\"#icon-play\" />\n      </svg>\n    </button>\n    <button v-if=\"pauseBtn\" type=\"button\" :aria-label=\"$t('list__pause')\" @contextmenu.capture.stop @click.stop=\"handleClick('pause')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 1024 1024\" space=\"preserve\">\n        <use xlink:href=\"#icon-pause\" />\n      </svg>\n    </button>\n    <button v-if=\"fileBtn\" type=\"button\" :aria-label=\"$t('list__file')\" @contextmenu.capture.stop @click.stop=\"handleClick('file')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"-61 0 512 512\" space=\"preserve\">\n        <use xlink:href=\"#icon-musicFile\" />\n      </svg>\n    </button>\n    <button v-if=\"searchBtn\" type=\"button\" :aria-label=\"$t('list__search')\" @contextmenu.capture.stop @click.stop=\"handleClick('search')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 30.239 30.239\" space=\"preserve\">\n        <use xlink:href=\"#icon-search\" />\n      </svg>\n    </button>\n    <button v-if=\"removeBtn\" type=\"button\" :aria-label=\"$t('list__remove')\" @click.stop=\"handleClick('remove')\">\n      <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\">\n        <use xlink:href=\"#icon-delete\" />\n      </svg>\n    </button>\n  </div>\n</template>\n\n<script>\nimport { appSetting } from '@renderer/store/setting'\n\nexport default {\n  props: {\n    index: {\n      type: Number,\n      required: true,\n    },\n    startBtn: {\n      type: Boolean,\n      default: false,\n    },\n    pauseBtn: {\n      type: Boolean,\n      default: false,\n    },\n    removeBtn: {\n      type: Boolean,\n      default: false,\n    },\n    downloadBtn: {\n      type: Boolean,\n      default: true,\n    },\n    playBtn: {\n      type: Boolean,\n      default: true,\n    },\n    listAddBtn: {\n      type: Boolean,\n      default: true,\n    },\n    searchBtn: {\n      type: Boolean,\n      default: false,\n    },\n    fileBtn: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['btn-click'],\n  setup() {\n    return {\n      appSetting,\n    }\n  },\n  methods: {\n    handleClick(action) {\n      this.$emit('btn-click', { action, index: this.index })\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.btns {\n  line-height: 1.2;\n\n  button {\n    background-color: transparent;\n    border: none;\n    border-radius: @form-radius;\n    margin-right: 5px;\n    cursor: pointer;\n    padding: 4px 7px;\n    color: var(--color-button-font);\n    outline: none;\n    transition: background-color 0.2s ease;\n    line-height: 0;\n    &:last-child {\n      margin-right: 0;\n    }\n\n    svg {\n      height: 16px;\n    }\n\n    &:hover {\n      background-color: var(--color-button-background-hover);\n    }\n    &:active {\n      background-color: var(--color-button-background-active);\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/Modal.vue",
    "content": "<template>\n  <teleport :to=\"teleport\">\n    <div v-if=\"showModal\" ref=\"dom_container\" :class=\"$style.container\">\n      <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n        <div v-show=\"showContent\" :class=\"[$style.modal, {[$style.filter]: filter}]\" @click=\"bgClose && close()\">\n          <transition :enter-active-class=\"inClass\" :leave-active-class=\"outClass\" @after-enter=\"$emit('after-enter', $event)\" @after-leave=\"handleAfterLeave\">\n            <div v-show=\"showContent\" :class=\"$style.content\" :style=\"contentStyle\" @click.stop>\n              <header :class=\"$style.header\">\n                <button v-if=\"closeBtn\" type=\"button\" @click=\"close\">\n                  <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\">\n                    <use xlink:href=\"#icon-delete\" />\n                  </svg>\n                </button>\n              </header>\n              <slot />\n            </div>\n          </transition>\n        </div>\n      </transition>\n    </div>\n  </teleport>\n</template>\n\n<script>\nimport { getRandom } from '@common/utils/common'\nimport { nextTick } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\n\nlet modalCount = 0\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    closeBtn: {\n      type: Boolean,\n      default: true,\n    },\n    bgClose: {\n      type: Boolean,\n      default: false,\n    },\n    teleport: {\n      type: String,\n      default: '#root',\n    },\n    maxWidth: {\n      type: String,\n      default: '76%',\n    },\n    minWidth: {\n      type: String,\n      default: '280px',\n    },\n    maxHeight: {\n      type: String,\n      default: '76%',\n    },\n    width: {\n      type: String,\n      default: 'auto',\n    },\n    height: {\n      type: String,\n      default: 'auto',\n    },\n  },\n  emits: ['after-enter', 'after-leave', 'close'],\n  data() {\n    return {\n      animates: [\n        [['jackInTheBox', 'flipInX', 'flipInY', 'lightSpeedIn'], ['flipOutX', 'flipOutY', 'lightSpeedOut']],\n        // [['jackInTheBox', 'lightSpeedIn'], ['lightSpeedOut']],\n        [['rotateInDownLeft', 'rotateInDownRight', 'rotateInUpLeft', 'rotateInUpRight'], ['rotateOutDownLeft', 'rotateOutDownRight', 'rotateOutUpLeft', 'rotateOutUpRight']],\n        [['jackInTheBox', 'zoomInDown', 'zoomInUp'], ['zoomOutDown', 'zoomOutUp']],\n        [['slideInDown', 'slideInLeft', 'slideInRight', 'slideInUp'], ['slideOutDown', 'slideOutLeft', 'slideOutRight', 'slideOutUp']],\n\n        // ['flipInX', 'flipOutX'],\n        // ['flipInY', 'flipOutY'],\n        // ['lightSpeedIn', 'lightSpeedOut'],\n        // ['rotateInDownLeft', 'rotateOutDownLeft'],\n        // ['rotateInDownRight', 'rotateOutDownRight'],\n        // ['rotateInUpLeft', 'rotateOutUpLeft'],\n        // ['rotateInUpRight', 'rotateOutUpRight'],\n        // // ['rollIn', 'rollOut'],\n        // // ['zoomIn', 'zoomOut'],\n        // ['zoomInDown', 'zoomOutDown'],\n        // // ['zoomInLeft', 'zoomOutLeft'],\n        // // ['zoomInRight', 'zoomOutRight'],\n        // ['zoomInUp', 'zoomOutUp'],\n        // ['slideInDown', 'slideOutDown'],\n        // ['slideInLeft', 'slideOutLeft'],\n        // ['slideInRight', 'slideOutRight'],\n        // ['slideInUp', 'slideOutUp'],\n        // // ['jackInTheBox', 'hinge'],\n      ],\n      // animateIn: [\n      //   'flipInX',\n      //   'flipInY',\n      //   // 'fadeIn',\n      //   // 'bounceIn',\n      //   'lightSpeedIn',\n      //   'rotateInDownLeft',\n      //   'rotateInDownRight',\n      //   'rotateInUpLeft',\n      //   'rotateInUpRight',\n      //   'rollIn',\n      //   'zoomIn',\n      //   'zoomInDown',\n      //   'zoomInLeft',\n      //   'zoomInRight',\n      //   'zoomInUp',\n      //   'slideInDown',\n      //   'slideInLeft',\n      //   'slideInRight',\n      //   'slideInUp',\n      //   'jackInTheBox',\n      // ],\n      // animateOut: [\n      //   'flipOutX',\n      //   'flipOutY',\n      //   // 'fadeOut',\n      //   // 'bounceOut',\n      //   'lightSpeedOut',\n      //   'rotateOutDownLeft',\n      //   'rotateOutDownRight',\n      //   'rotateOutUpLeft',\n      //   'rotateOutUpRight',\n      //   'rollOut',\n      //   'zoomOut',\n      //   'zoomOutDown',\n      //   'zoomOutLeft',\n      //   'zoomOutRight',\n      //   'zoomOutUp',\n      //   'slideOutDown',\n      //   'slideOutLeft',\n      //   'slideOutRight',\n      //   'slideOutUp',\n      //   'hinge',\n      // ],\n      inClass: 'animated jackInTheBox',\n      outClass: 'animated slideOutRight',\n      showModal: false,\n      showContent: false,\n      modalCount: false,\n      isAddedClass: false,\n      // ai: 0,\n    }\n  },\n  computed: {\n    contentStyle() {\n      return {\n        maxWidth: this.maxWidth,\n        minWidth: this.minWidth,\n        width: this.width,\n        height: this.height,\n        maxHeight: this.maxHeight,\n      }\n    },\n    filter() {\n      return this.teleport == '#root' || this.modalCount > 1\n    },\n  },\n  watch: {\n    show(val) {\n      this.handleShowChange(val)\n    },\n  },\n  mounted() {\n    if (this.show) this.handleShowChange(true)\n    this.setRandomAnimation()\n  },\n  beforeUnmount() {\n    this.removeClass()\n  },\n  methods: {\n    handleShowChange(val) {\n      if (val) {\n        // const dom = document.getElementById(this.teleport)\n        // if (dom) {\n        //   // dom.t\n        // }\n        this.setRandomAnimation()\n        this.modalCount = ++modalCount\n        this.showModal = true\n        void nextTick(() => {\n          const node = this.$refs.dom_container.parentNode\n          if (!node.classList.contains('show-modal')) {\n            node.classList.add('show-modal')\n            this.isAddedClass = true\n          }\n          this.showContent = true\n        })\n      } else {\n        if (modalCount > 0) this.modalCount = --modalCount\n        this.removeClass()\n        this.showContent = false\n      }\n    },\n    removeClass() {\n      if (!this.isAddedClass) return\n      this.$refs.dom_container?.parentNode.classList.remove('show-modal')\n    },\n    setRandomAnimation() {\n      if (appSetting['common.randomAnimate']) {\n        const [animIn, animOut] = this.animates[getRandom(0, this.animates.length)]\n        // const [animIn, animOut] = this.animates[this.ai]\n        // if (++this.ai >= this.animates.length) this.ai = 0\n        // console.log(animIn, animOut)\n        // this.inClass = 'animated ' + animIn\n        // this.outClass = 'animated ' + animOut\n        this.inClass = 'animated ' + animIn[getRandom(0, animIn.length)]\n        this.outClass = 'animated ' + animOut[getRandom(0, animOut.length)]\n      }\n    },\n    close() {\n      this.$emit('close')\n    },\n    handleAfterLeave(event) {\n      this.$emit('after-leave', event)\n      this.showModal = false\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 99;\n}\n\n.modal {\n  width: 100%;\n  height: 100%;\n  // background-color: rgba(0, 0, 0, .2);\n  // background-color: rgba(255, 255, 255, .6);\n  // background-color: var(--color-primary-light-600-alpha-900);\n  // backdrop-filter: blur(4px);\n  // backdrop-filter: grayscale(70%);\n  display: grid;\n  align-items: center;\n  justify-items: center;\n  // will-change: transform;\n\n  &.filter {\n    backdrop-filter: grayscale(70%);\n  }\n\n  // &:before {\n  //   .mixin-after();\n  //   position: absolute;\n  //   left: 0;\n  //   top: 0;\n  //   width: 100%;\n  //   height: 100%;\n  //   background-color: var(--color-000);\n  //   opacity: .6;\n  // }\n}\n\n.content {\n  position: relative;\n  border-radius: 4px;\n  box-shadow: 0 0 4px rgba(0, 0, 0, .25);\n  overflow: hidden;\n  // max-height: 80%;\n  // max-width: 76%;\n  min-width: 220px;\n  position: relative;\n  display: flex;\n  flex-flow: column nowrap;\n  z-index: 100;\n  background-color: var(--color-content-background);\n}\n\n.header {\n  flex: none;\n  background-color: var(--color-primary-light-100-alpha-100);\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  height: 18px;\n\n  button {\n    border: none;\n    cursor: pointer;\n    padding: 4px 7px;\n    background-color: transparent;\n    color: var(--color-primary-dark-500-alpha-500);\n    outline: none;\n    transition: background-color 0.2s ease;\n    line-height: 0;\n\n    svg {\n      height: .7em;\n    }\n\n    &:hover {\n      background-color: var(--color-primary-dark-100-alpha-600);\n    }\n    &:active {\n      background-color: var(--color-primary-dark-200-alpha-600);\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/index.vue",
    "content": "<template>\n  <div :class=\"$style.songList\">\n    <!-- <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated-fast fadeOut\"> -->\n    <div :class=\"$style.list\">\n      <div class=\"thead\">\n        <table>\n          <thead>\n            <tr v-if=\"actionButtonsVisible\">\n              <th class=\"num\" style=\"width: 5%;\">#</th>\n              <th class=\"nobreak\">{{ $t('music_name') }}</th>\n              <th class=\"nobreak\" style=\"width: 22%;\">{{ $t('music_singer') }}</th>\n              <th class=\"nobreak\" style=\"width: 22%;\">{{ $t('music_album') }}</th>\n              <th class=\"nobreak\" style=\"width: 9%;\">{{ $t('music_time') }}</th>\n              <th class=\"nobreak\" style=\"width: 16%;\">{{ $t('action') }}</th>\n            </tr>\n            <tr v-else>\n              <th class=\"num\" style=\"width: 5%;\">#</th>\n              <th class=\"nobreak\">{{ $t('music_name') }}</th>\n              <th class=\"nobreak\" style=\"width: 24%;\">{{ $t('music_singer') }}</th>\n              <th class=\"nobreak\" style=\"width: 27%;\">{{ $t('music_album') }}</th>\n              <th class=\"nobreak\" style=\"width: 10%;\">{{ $t('music_time') }}</th>\n            </tr>\n          </thead>\n        </table>\n      </div>\n      <div :class=\"$style.content\">\n        <div v-show=\"!noItem\" ref=\"dom_listContent\" :class=\"$style.content\">\n          <base-virtualized-list v-if=\"actionButtonsVisible\" ref=\"listRef\" :list=\"list\" key-name=\"id\" :item-height=\"listItemHeight\" container-class=\"scroll\" content-class=\"list\" @contextmenu.capture=\"handleListRightClick\">\n            <template #default=\"{ item, index }\">\n              <div\n                class=\"list-item\" :class=\"[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]\"\n                @click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n              >\n                <div class=\"list-item-cell no-select num\" style=\"flex: 0 0 5%;\" @click.stop>{{ index + 1 }}</div>\n                <div class=\"list-item-cell auto name\">\n                  <span class=\"select name\" :aria-label=\"item.name\">{{ item.name }}</span>\n                  <span v-if=\"item.meta._qualitys.flac24bit\" class=\"no-select badge badge-theme-primary\">{{ $t('tag__lossless_24bit') }}</span>\n                  <span v-else-if=\"item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav\" class=\"no-select badge badge-theme-primary\">{{ $t('tag__lossless') }}</span>\n                  <span v-else-if=\"item.meta._qualitys['320k']\" class=\"no-select badge badge-theme-secondary\">{{ $t('tag__high_quality') }}</span>\n                  <span v-if=\"sourceTag\" class=\"no-select badge badge-theme-tertiary\">{{ item.source }}</span>\n                </div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 22%;\"><span class=\"select\" :aria-label=\"item.singer\">{{ item.singer }}</span></div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 22%;\"><span class=\"select\" :aria-label=\"item.meta.albumName\">{{ item.meta.albumName }}</span></div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 9%;\"><span class=\"no-select\">{{ item.interval || '--/--' }}</span></div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 16%; padding-left: 0; padding-right: 0;\">\n                  <material-list-buttons :index=\"index\" :remove-btn=\"false\" :download-btn=\"assertApiSupport(item.source)\" :play-btn=\"checkApiSource ? assertApiSupport(item.source) : true\" @btn-click=\"handleListBtnClick\" />\n                </div>\n              </div>\n            </template>\n            <template #footer>\n              <div :class=\"$style.pagination\">\n                <material-pagination :count=\"total\" :limit=\"limit\" :page=\"page\" @btn-click=\"$emit('togglePage', $event)\" />\n              </div>\n            </template>\n          </base-virtualized-list>\n          <base-virtualized-list v-else ref=\"listRef\" :list=\"list\" key-name=\"id\" :item-height=\"listItemHeight\" container-class=\"scroll\" content-class=\"list\" @contextmenu.capture=\"handleListRightClick\">\n            <template #default=\"{ item, index }\">\n              <div\n                class=\"list-item\" :class=\"[{ selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]\"\n                @click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n              >\n                <div class=\"list-item-cell no-select num\" style=\"flex: 0 0 5%;\" @click.stop>{{ index + 1 }}</div>\n                <div class=\"list-item-cell auto name\">\n                  <span class=\"select name\" :aria-label=\"item.name\">{{ item.name }}</span>\n                  <span v-if=\"item.meta._qualitys.flac24bit\" class=\"no-select badge badge-theme-primary\">{{ $t('tag__lossless_24bit') }}</span>\n                  <span v-else-if=\"item.meta._qualitys.ape || item.meta._qualitys.flac || item.meta._qualitys.wav\" class=\"no-select badge badge-theme-primary\">{{ $t('tag__lossless') }}</span>\n                  <span v-else-if=\"item.meta._qualitys['320k']\" class=\"no-select badge badge-theme-secondary\">{{ $t('tag__high_quality') }}</span>\n                  <span v-if=\"sourceTag\" class=\"no-select badge badge-theme-tertiary\">{{ item.source }}</span>\n                </div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 24%;\"><span class=\"select\" :aria-label=\"item.singer\">{{ item.singer }}</span></div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 27%;\"><span class=\"select\" :aria-label=\"item.meta.albumName\">{{ item.meta.albumName }}</span></div>\n                <div class=\"list-item-cell\" style=\"flex: 0 0 10%;\"><span class=\"no-select\">{{ item.interval || '--/--' }}</span></div>\n              </div>\n            </template>\n            <template #footer>\n              <div :class=\"$style.pagination\">\n                <material-pagination :count=\"total\" :limit=\"limit\" :page=\"page\" @btn-click=\"$emit('togglePage', $event)\" />\n              </div>\n            </template>\n          </base-virtualized-list>\n        </div>\n        <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n          <div v-show=\"noItem\" :class=\"$style.noitem\">\n            <p v-text=\"noItem\" />\n          </div>\n        </transition>\n      </div>\n    </div>\n    <!-- </transition> -->\n    <!-- <material-flow-btn :show=\"isShowEditBtn && assertApiSupport(source)\" :remove-btn=\"false\" @btn-click=\"handleFlowBtnClick\" /> -->\n    <!-- <common-download-modal v-model:show=\"isShowDownload\" :music-info=\"selectedDownloadMusicInfo\" teleport=\"#view\" />\n    <common-download-multiple-modal v-model:show=\"isShowDownloadMultiple\" :list=\"selectedList\" teleport=\"#view\" @confirm=\"removeAllSelect\" /> -->\n    <common-list-add-modal v-model:show=\"isShowListAdd\" :music-info=\"selectedAddMusicInfo\" teleport=\"#view\" />\n    <common-list-add-multiple-modal v-model:show=\"isShowListAddMultiple\" :music-list=\"selectedList\" teleport=\"#view\" @confirm=\"removeAllSelect\" />\n    <common-download-modal v-model:show=\"isShowDownload\" :music-info=\"selectedDownloadMusicInfo\" teleport=\"#view\" />\n    <common-download-multiple-modal v-model:show=\"isShowDownloadMultiple\" :list=\"selectedList\" teleport=\"#view\" @confirm=\"removeAllSelect\" />\n    <base-menu v-model=\"isShowItemMenu\" :menus=\"menus\" :xy=\"menuLocation\" item-name=\"name\" @menu-click=\"handleMenuClick\" />\n  </div>\n</template>\n\n<script>\nimport { clipboardWriteText } from '@common/utils/electron'\nimport { assertApiSupport } from '@renderer/store/utils'\nimport { ref } from '@common/utils/vueTools'\nimport useList from './useList'\nimport useMenu from './useMenu'\nimport usePlay from './usePlay'\nimport useMusicDownload from './useMusicDownload'\nimport useMusicAdd from './useMusicAdd'\nimport useMusicActions from './useMusicActions'\nimport { appSetting } from '@renderer/store/setting'\nexport default {\n  name: 'MaterialOnlineList',\n  props: {\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    page: {\n      type: Number,\n      required: true,\n    },\n    limit: {\n      type: Number,\n      required: true,\n    },\n    total: {\n      type: Number,\n      required: true,\n    },\n    sourceTag: {\n      type: Boolean,\n      default: false,\n    },\n    noItem: {\n      type: String,\n      default: '',\n    },\n    checkApiSource: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['show-menu', 'play-list', 'togglePage'],\n  setup(props, { emit }) {\n    const actionButtonsVisible = appSetting['list.actionButtonsVisible']\n    const rightClickSelectedIndex = ref(-1)\n    const dom_listContent = ref(null)\n    const listRef = ref(null)\n\n    const {\n      selectedList,\n      listItemHeight,\n      handleSelectData,\n      removeAllSelect,\n    } = useList({ props, listRef })\n\n    const {\n      handlePlayMusic,\n      handlePlayMusicLater,\n      doubleClickPlay,\n    } = usePlay({ selectedList, props, removeAllSelect, emit })\n\n    const {\n      isShowListAdd,\n      isShowListAddMultiple,\n      selectedAddMusicInfo,\n      handleShowMusicAddModal,\n    } = useMusicAdd({ selectedList, props })\n\n    const {\n      isShowDownload,\n      isShowDownloadMultiple,\n      selectedDownloadMusicInfo,\n      handleShowDownloadModal,\n    } = useMusicDownload({ selectedList, props })\n\n    const {\n      handleSearch,\n      handleOpenMusicDetail,\n      handleDislikeMusic,\n    } = useMusicActions({ props })\n\n    const {\n      menus,\n      menuLocation,\n      isShowItemMenu,\n      showMenu,\n      menuClick,\n    } = useMenu({\n      props,\n      assertApiSupport,\n      emit,\n\n      handleShowDownloadModal,\n      handlePlayMusic,\n      handlePlayMusicLater,\n      handleSearch,\n      handleShowMusicAddModal,\n      handleOpenMusicDetail,\n      handleDislikeMusic,\n    })\n\n    const handleListItemClick = (event, index) => {\n      if (rightClickSelectedIndex.value > -1) return\n      handleSelectData(index)\n      doubleClickPlay(index)\n    }\n    const handleListItemRightClick = (event, index) => {\n      rightClickSelectedIndex.value = index\n      showMenu(event, props.list[index], index)\n    }\n    const handleMenuClick = (action) => {\n      let index = rightClickSelectedIndex.value\n      rightClickSelectedIndex.value = -1\n      menuClick(action, index)\n    }\n    const handleListRightClick = (event) => {\n      if (!event.target.classList.contains('select')) return\n      event.stopImmediatePropagation()\n      let classList = dom_listContent.value.classList\n      classList.add('copying')\n      window.requestAnimationFrame(() => {\n        let str = window.getSelection().toString()\n        classList.remove('copying')\n        str = str.split(/\\n\\n/).map(s => s.replace(/\\n/g, '  ')).join('\\n').trim()\n        if (!str.length) return\n        clipboardWriteText(str)\n      })\n    }\n    const handleListBtnClick = ({ action, index }) => {\n      switch (action) {\n        case 'download':\n          handleShowDownloadModal(index, true)\n          break\n        case 'play':\n          void handlePlayMusic(index, true)\n          break\n        case 'search':\n          handleSearch(index)\n          break\n        case 'listAdd':\n          handleShowMusicAddModal(index, true)\n          break\n      }\n    }\n    const scrollToTop = () => {\n      listRef.value.scrollTo(0, true)\n    }\n\n    return {\n      listItemHeight,\n      handleListItemClick,\n      selectedList,\n      handleListItemRightClick,\n      removeAllSelect,\n      handleListBtnClick,\n      rightClickSelectedIndex,\n      dom_listContent,\n      listRef,\n\n      menus,\n      isShowItemMenu,\n      menuLocation,\n      handleMenuClick,\n\n      handleListRightClick,\n      assertApiSupport,\n\n      isShowListAdd,\n      isShowListAddMultiple,\n      selectedAddMusicInfo,\n\n      isShowDownload,\n      isShowDownloadMultiple,\n      selectedDownloadMusicInfo,\n\n      scrollToTop,\n      actionButtonsVisible,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.songList {\n  overflow: hidden;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  position: relative;\n}\n\n.list {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  font-size: 14px;\n}\n\n.content {\n  flex: auto;\n  min-height: 0;\n  position: relative;\n  height: 100%;\n}\n\n.pagination {\n  text-align: center;\n  padding: 15px 0;\n  // left: 50%;\n  // transform: translateX(-50%);\n}\n.noitem {\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n  // background-color: var(--color-000);\n\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/useList.ts",
    "content": "import { computed, watch, ref, onBeforeUnmount, type Ref } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { getFontSizeWithScreen } from '@renderer/utils'\n\nconst useKeyEvent = ({ handleSelectAllData, listRef }: {\n  handleSelectAllData: () => void\n  listRef: Ref<any>\n}) => {\n  const keyEvent = {\n    isShiftDown: false,\n    isModDown: false,\n  }\n\n  const handle_key_shift_down = () => {\n    keyEvent.isShiftDown ||= true\n  }\n  const handle_key_shift_up = () => {\n    keyEvent.isShiftDown &&= false\n  }\n  const handle_key_mod_down = () => {\n    keyEvent.isModDown ||= true\n  }\n  const handle_key_mod_up = () => {\n    keyEvent.isModDown &&= false\n  }\n  const handle_key_mod_a_down = ({ event }: LX.KeyDownEevent) => {\n    if (!event || (event.target as HTMLElement).tagName == 'INPUT' || document.activeElement != listRef.value?.$el) return\n    event.preventDefault()\n    if (event.repeat) return\n    keyEvent.isModDown = false\n    handleSelectAllData()\n  }\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_shift_down', handle_key_shift_down)\n    window.key_event.off('key_shift_up', handle_key_shift_up)\n    window.key_event.off('key_mod_down', handle_key_mod_down)\n    window.key_event.off('key_mod_up', handle_key_mod_up)\n    window.key_event.off('key_mod+a_down', handle_key_mod_a_down)\n  })\n  window.key_event.on('key_shift_down', handle_key_shift_down)\n  window.key_event.on('key_shift_up', handle_key_shift_up)\n  window.key_event.on('key_mod_down', handle_key_mod_down)\n  window.key_event.on('key_mod_up', handle_key_mod_up)\n  window.key_event.on('key_mod+a_down', handle_key_mod_a_down)\n\n  return keyEvent\n}\n\n\nexport default ({ props, listRef }: {\n  props: {\n    list: LX.Music.MusicInfoOnline[]\n  }\n  listRef: Ref<any>\n}) => {\n  const selectedList = ref<LX.Music.MusicInfoOnline[]>([])\n  let lastSelectIndex = -1\n  const listItemHeight = computed(() => {\n    return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)\n  })\n\n  const removeAllSelect = () => {\n    selectedList.value = []\n  }\n  const handleSelectAllData = () => {\n    removeAllSelect()\n    selectedList.value = [...props.list]\n  }\n  const keyEvent = useKeyEvent({ handleSelectAllData, listRef })\n\n  const handleSelectData = (clickIndex: number) => {\n    if (keyEvent.isShiftDown) {\n      if (selectedList.value.length) {\n        removeAllSelect()\n        if (lastSelectIndex != clickIndex) {\n          let isNeedReverse = false\n          let _lastSelectIndex = lastSelectIndex\n          if (clickIndex < _lastSelectIndex) {\n            let temp = _lastSelectIndex\n            _lastSelectIndex = clickIndex\n            clickIndex = temp\n            isNeedReverse = true\n          }\n          selectedList.value = props.list.slice(_lastSelectIndex, clickIndex + 1)\n          if (isNeedReverse) selectedList.value.reverse()\n        }\n      } else {\n        selectedList.value.push(props.list[clickIndex])\n        lastSelectIndex = clickIndex\n      }\n    } else if (keyEvent.isModDown) {\n      lastSelectIndex = clickIndex\n      let item = props.list[clickIndex]\n      let index = selectedList.value.indexOf(item)\n      if (index < 0) {\n        selectedList.value.push(item)\n      } else {\n        selectedList.value.splice(index, 1)\n      }\n    } else if (selectedList.value.length) {\n      removeAllSelect()\n    }\n  }\n\n  watch(() => props.list, removeAllSelect)\n\n  return {\n    selectedList,\n    listItemHeight,\n    removeAllSelect,\n    handleSelectData,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/useMenu.js",
    "content": "import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { hasDislike } from '@renderer/core/dislikeList'\n\nexport default ({\n  props,\n  assertApiSupport,\n  emit,\n\n  handleShowDownloadModal,\n  handlePlayMusic,\n  handlePlayMusicLater,\n  handleSearch,\n  handleShowMusicAddModal,\n  handleOpenMusicDetail,\n  handleDislikeMusic,\n}) => {\n  const itemMenuControl = reactive({\n    play: true,\n    addTo: true,\n    playLater: true,\n    download: true,\n    search: true,\n    sourceDetail: true,\n    dislike: true,\n  })\n  const t = useI18n()\n  const menuLocation = reactive({ x: 0, y: 0 })\n  const isShowItemMenu = ref(false)\n\n  const menus = computed(() => {\n    return [\n      {\n        name: t('list__play'),\n        action: 'play',\n        disabled: !itemMenuControl.play,\n      },\n      {\n        name: t('list__download'),\n        action: 'download',\n        disabled: !itemMenuControl.download,\n      },\n      {\n        name: t('list__play_later'),\n        action: 'playLater',\n        disabled: !itemMenuControl.playLater,\n      },\n      {\n        name: t('list__search'),\n        action: 'search',\n        disabled: !itemMenuControl.search,\n      },\n      {\n        name: t('list__add_to'),\n        action: 'addTo',\n        disabled: !itemMenuControl.addTo,\n      },\n      {\n        name: t('list__source_detail'),\n        action: 'sourceDetail',\n        disabled: !itemMenuControl.sourceDetail,\n      },\n      {\n        name: t('list__dislike'),\n        action: 'dislike',\n        disabled: !itemMenuControl.dislike,\n      },\n    ]\n  })\n\n  const showMenu = (event, musicInfo) => {\n    itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source]?.getMusicDetailPageUrl\n    // this.listMenu.itemMenuControl.play =\n    //   this.listMenu.itemMenuControl.playLater =\n    itemMenuControl.download = assertApiSupport(musicInfo.source)\n\n    itemMenuControl.dislike = !hasDislike(musicInfo)\n\n    if (props.checkApiSource) {\n      itemMenuControl.playLater =\n      itemMenuControl.play =\n        itemMenuControl.download\n    }\n\n    menuLocation.x = event.pageX\n    menuLocation.y = event.pageY\n\n    if (isShowItemMenu.value) return\n    emit('show-menu')\n    nextTick(() => {\n      isShowItemMenu.value = true\n    })\n  }\n\n  const hideMenu = () => {\n    isShowItemMenu.value = false\n  }\n\n  const menuClick = (action, index) => {\n    // console.log(action)\n    hideMenu()\n    if (!action) return\n\n    switch (action.action) {\n      case 'download':\n        handleShowDownloadModal(index)\n        break\n      case 'play':\n        handlePlayMusic(index)\n        break\n      case 'playLater':\n        handlePlayMusicLater(index)\n        break\n      case 'search':\n        handleSearch(index)\n        break\n      case 'addTo':\n        handleShowMusicAddModal(index)\n        break\n      case 'sourceDetail':\n        handleOpenMusicDetail(index)\n        break\n      case 'dislike':\n        handleDislikeMusic(index)\n        break\n    }\n  }\n\n  return {\n    menus,\n    menuLocation,\n    isShowItemMenu,\n    showMenu,\n    menuClick,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/useMusicActions.js",
    "content": "import { useRouter } from '@common/utils/vueRouter'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { openUrl } from '@common/utils/electron'\nimport { toOldMusicInfo } from '@renderer/utils'\nimport { addDislikeInfo, hasDislike } from '@renderer/core/dislikeList'\nimport { playNext } from '@renderer/core/player'\nimport { playMusicInfo } from '@renderer/store/player/state'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { useI18n } from '@renderer/plugins/i18n'\n\n\nexport default ({ props }) => {\n  const router = useRouter()\n  const t = useI18n()\n\n  const handleSearch = index => {\n    const info = props.list[index]\n    router.push({\n      path: '/search',\n      query: {\n        text: `${info.name} ${info.singer}`,\n      },\n    })\n  }\n\n  const handleOpenMusicDetail = index => {\n    const minfo = props.list[index]\n    const url = musicSdk[minfo.source]?.getMusicDetailPageUrl?.(toOldMusicInfo(minfo))\n    if (!url) return\n    openUrl(url)\n  }\n\n  const handleDislikeMusic = async(index) => {\n    const minfo = props.list[index]\n    const confirm = await dialog.confirm({\n      message: minfo.singer ? t('lists__dislike_music_singer_tip', { name: minfo.name, singer: minfo.singer }) : t('lists__dislike_music_tip', { name: minfo.name }),\n      cancelButtonText: t('cancel_button_text_2'),\n      confirmButtonText: t('confirm_button_text'),\n    })\n    if (!confirm) return\n    await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }])\n    if (hasDislike(playMusicInfo.musicInfo)) {\n      playNext(true)\n    }\n  }\n\n\n  return {\n    handleSearch,\n    handleOpenMusicDetail,\n    handleDislikeMusic,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/useMusicAdd.js",
    "content": "import { ref, nextTick } from '@common/utils/vueTools'\n\nexport default ({ selectedList, props }) => {\n  const isShowListAdd = ref(false)\n  const isShowListAddMultiple = ref(false)\n  const selectedAddMusicInfo = ref(null)\n\n  const handleShowMusicAddModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isShowListAddMultiple.value = true\n    } else {\n      selectedAddMusicInfo.value = props.list[index]\n      nextTick(() => {\n        isShowListAdd.value = true\n      })\n    }\n  }\n\n  return {\n    isShowListAdd,\n    isShowListAddMultiple,\n    selectedAddMusicInfo,\n    handleShowMusicAddModal,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/useMusicDownload.js",
    "content": "import { ref, nextTick } from '@common/utils/vueTools'\n\nexport default ({ selectedList, props }) => {\n  const isShowDownload = ref(false)\n  const isShowDownloadMultiple = ref(false)\n  const musicInfo = ref(null)\n\n  const handleShowDownloadModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isShowDownloadMultiple.value = true\n    } else {\n      musicInfo.value = props.list[index]\n      nextTick(() => {\n        isShowDownload.value = true\n      })\n    }\n  }\n\n  return {\n    isShowDownload,\n    isShowDownloadMultiple,\n    selectedDownloadMusicInfo: musicInfo,\n    handleShowDownloadModal,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/OnlineList/usePlay.ts",
    "content": "// import { useCommit } from '@common/utils/vueTools'\nimport { defaultList } from '@renderer/store/list/state'\nimport { getListMusics, addListMusics } from '@renderer/store/list/action'\nimport { addTempPlayList } from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { type Ref } from '@common/utils/vueTools'\nimport { playList } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\n\nexport default ({ selectedList, props, removeAllSelect, emit }: {\n  selectedList: Ref<LX.Music.MusicInfoOnline[]>\n  props: {\n    list: LX.Music.MusicInfoOnline[]\n  }\n  removeAllSelect: () => void\n  emit: (event: 'show-menu' | 'play-list' | 'togglePage', ...args: any[]) => void\n}) => {\n  let clickTime = 0\n  let clickIndex = -1\n\n  const handlePlayMusic = async(index: number, single: boolean) => {\n    let targetSong = props.list[index]\n    const defaultListMusics = await getListMusics(defaultList.id)\n    if (selectedList.value.length && !single) {\n      await addListMusics(defaultList.id, [...selectedList.value])\n      removeAllSelect()\n    } else {\n      await addListMusics(defaultList.id, [targetSong])\n    }\n    let targetIndex = defaultListMusics.findIndex(s => s.id === targetSong.id)\n    if (targetIndex > -1) {\n      playList(defaultList.id, targetIndex)\n    }\n  }\n\n  const handlePlayMusicLater = (index: number, single: boolean) => {\n    if (selectedList.value.length && !single) {\n      addTempPlayList(selectedList.value.map(s => ({ listId: LIST_IDS.PLAY_LATER, musicInfo: s })))\n      removeAllSelect()\n    } else {\n      addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo: props.list[index] }])\n    }\n  }\n\n  const doubleClickPlay = (index: number) => {\n    if (\n      window.performance.now() - clickTime > 400 ||\n      clickIndex !== index\n    ) {\n      clickTime = window.performance.now()\n      clickIndex = index\n      return\n    }\n    if (appSetting['list.isClickPlayList']) {\n      emit('play-list', index)\n    } else {\n      void handlePlayMusic(index, true)\n    }\n    clickTime = 0\n    clickIndex = -1\n  }\n\n  return {\n    handlePlayMusic,\n    handlePlayMusicLater,\n    doubleClickPlay,\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/material/Pagination.vue",
    "content": "<template>\n  <div v-if=\"maxPage > 1\" :class=\"$style.pagination\">\n    <ul>\n      <li v-if=\"page == 1\" :class=\"$style.disabled\">\n        <span>\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-left\" />\n          </svg>\n        </span>\n      </li>\n      <li v-else>\n        <button type=\"button\" :aria-label=\"$t('pagination__prev')\" @click=\"handleClick(page - 1)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-left\" />\n          </svg>\n        </button>\n      </li>\n      <li v-if=\"maxPage > btnLength && page > pageEvg+1\" :class=\"$style.first\">\n        <button type=\"button\" :aria-label=\"$t('pagination__page', { num: 1 })\" @click=\"handleClick(1)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-first\" />\n          </svg>\n        </button>\n      </li>\n      <li v-for=\"p in pages\" :key=\"p\" :class=\"{[$style.active] : p == page}\">\n        <span v-if=\"p === page\" v-text=\"page\" />\n        <button v-else type=\"button\" :aria-label=\"$t('pagination__page', { num: p })\" @click=\"handleClick(p)\" v-text=\"p\" />\n      </li>\n      <li v-if=\"maxPage > btnLength && maxPage - page > pageEvg\" :class=\"$style.last\">\n        <button type=\"button\" :aria-label=\"$t('pagination__page', { num: maxPage })\" @click=\"handleClick(maxPage)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-last\" />\n          </svg>\n        </button>\n      </li>\n      <li v-if=\"page == maxPage\" :class=\"$style.disabled\">\n        <span>\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-right\" />\n          </svg></span>\n      </li>\n      <li v-else>\n        <button type=\"button\" :aria-label=\"$t('pagination__next')\" @click=\"handleClick(page + 1)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-right\" />\n          </svg>\n        </button>\n      </li>\n    </ul>\n  </div>\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\n\nexport default {\n  props: {\n    count: {\n      type: Number,\n      default: 0,\n    },\n    limit: {\n      type: Number,\n      default: 10,\n    },\n    page: {\n      type: Number,\n      default: 1,\n    },\n    btnLength: {\n      type: Number,\n      default: 7,\n    },\n  },\n  emits: ['btn-click'],\n  setup(props, { emit }) {\n    const maxPage = computed(() => {\n      return Math.ceil(props.count / props.limit) || 1\n    })\n    const pageEvg = computed(() => {\n      return Math.floor(props.btnLength / 2)\n    })\n    const pages = computed(() => {\n      if (maxPage.value <= props.btnLength) return Array.from({ length: maxPage.value }, (_, i) => i + 1)\n      let start = props.page - pageEvg.value > 1\n        // eslint-disable-next-line @typescript-eslint/restrict-plus-operands\n        ? maxPage.value - props.page < pageEvg.value + 1\n          ? maxPage.value - (props.btnLength - 1)\n          : props.page - pageEvg.value\n        : 1\n      return Array.from({ length: props.btnLength }, (_, i) => start + i)\n    })\n\n    const handleClick = (page) => {\n      emit('btn-click', page)\n    }\n\n    return {\n      maxPage,\n      pageEvg,\n      pages,\n      handleClick,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.pagination {\n  display: inline-block;\n  background-color: var(--color-button-background);\n  // border-top-left-radius: 8px;\n  border-radius: @radius-border;\n  ul {\n    display: flex;\n    flex-flow: row nowrap;\n    // border: .0625rem solid @theme_color2;\n    // border-radius: .3125rem;\n    li {\n      // margin-right: @padding;\n      // color: var(--color-button-font);\n      // border: .0625rem solid @theme_line;\n      // border-radius: .3125rem;\n      transition: 0.4s ease;\n      transition-property: all;\n      line-height: 1.2;\n      display: flex;\n      // border-right: none;\n      svg {\n        height: 1em;\n      }\n      span,\n      button {\n        display: block;\n        padding: 7px 12px;\n        line-height: 1.2;\n        color: var(--color-button-font);\n        font-size: 13px;\n      }\n      &.active {\n        span {\n          background-color: var(--color-button-background-selected);\n        }\n      }\n      button {\n        background-color: transparent;\n        border: none;\n        cursor: pointer;\n        outline: none;\n        transition: background-color .3s ease;\n        &:hover {\n          background-color: var(--color-button-background-hover);\n        }\n        &:active {\n          background-color: var(--color-button-background-active);\n        }\n      }\n      &.disabled {\n        span {\n          opacity: .3;\n        }\n      }\n      &:first-child {\n        span, button {\n          border-top-left-radius: @radius-border;\n          border-bottom-left-radius: @radius-border;\n        }\n        // border-right: .0625rem solid @theme_line;\n      }\n      &:last-child {\n        span, button {\n          border-top-right-radius: @radius-border;\n          border-bottom-right-radius: @radius-border;\n        }\n        // border-right: .0625rem solid @theme_line;\n      }\n      &:first-child, &:last-child, &.first, &.last {\n        span,\n        button {\n          line-height: 0;\n        }\n      }\n    }\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/PopupBtn.vue",
    "content": "<template>\n  <div ref=\"dom_btn\" :class=\"$style.content\" @click=\"handleShowPopup\" @mouseenter=\"handlMsEnter\" @mouseleave=\"handlMsLeave\">\n    <slot />\n    <base-popup v-model:visible=\"visible\" :btn-el=\"dom_btn\" @mouseenter=\"handlMsEnter\" @mouseleave=\"handlMsLeave\">\n      <slot name=\"content\" />\n    </base-popup>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from '@common/utils/vueTools'\n\nconst visible = ref(false)\nconst dom_btn = ref<HTMLElement | null>(null)\n\nconst handleShowPopup = (evt: MouseEvent) => {\n  if (visible.value) {\n    evt.stopPropagation()\n    handlMsLeave()\n  } else handlMsEnter()\n  // setTimeout(() => {\n  //   // if (!)\n  //   visible.value = !visible.value\n  // }, 50)\n}\n\nlet timeout: number | null = null\nconst handlMsEnter = () => {\n  if (timeout) {\n    clearTimeout(timeout)\n    timeout = null\n  }\n  if (visible.value) return\n  timeout = setTimeout(() => {\n    visible.value = true\n  }, 100) as unknown as number\n}\nconst handlMsLeave = () => {\n  if (timeout) {\n    clearTimeout(timeout)\n    timeout = null\n  }\n  if (!visible.value) return\n  timeout = setTimeout(() => {\n    timeout = null\n    visible.value = false\n  }, 100) as unknown as number\n}\n\ndefineExpose({\n  hide() {\n    visible.value = false\n  },\n})\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.content {\n  position: relative;\n  display: inline-block;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/SearchInput.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <div :class=\"[$style.search, {[$style.active]: focus}, {[$style.big]: big}, {[$style.small]: small}]\">\n      <div :class=\"$style.form\">\n        <input\n          ref=\"dom_input\"\n          v-model.trim=\"text\"\n          :placeholder=\"placeholder\"\n          @focus=\"handleFocus\"\n          @blur=\"handleBlur\"\n          @input=\"$emit('update:modelValue', text)\"\n          @change=\"sendEvent('change')\"\n          @keyup.enter=\"handleSearch\"\n          @keydown.arrow-down.arrow-up.prevent\n          @keyup.arrow-down.prevent=\"handleKeyDown\"\n          @keyup.arrow-up.prevent=\"handleKeyUp\"\n          @contextmenu=\"handleContextMenu\"\n        >\n        <transition enter-active-class=\"animated zoomIn\" leave-active-class=\"animated zoomOut\">\n          <button v-show=\"text\" type=\"button\" @click=\"handleClearList\">\n            <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n              <use xlink:href=\"#icon-window-close\" />\n            </svg>\n          </button>\n        </transition>\n        <button type=\"button\" @click=\"handleSearch\">\n          <slot>\n            <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 30.239 30.239\" space=\"preserve\">\n              <use xlink:href=\"#icon-search\" />\n            </svg>\n          </slot>\n        </button>\n      </div>\n      <div v-if=\"list\" :class=\"$style.list\" :style=\"listStyle\">\n        <ul ref=\"dom_list\" @mouseleave=\"selectIndex = -1\">\n          <li\n            v-for=\"(item, index) in list\"\n            :key=\"item\"\n            :class=\"{[$style.select]: selectIndex === index }\"\n            @mouseenter=\"selectIndex = index\"\n            @click=\"handleTemplistClick(index)\"\n          >\n            <span>{{ item }}</span>\n          </li>\n        </ul>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { clipboardReadText } from '@common/utils/electron'\nimport { HOTKEY_COMMON } from '@common/hotKey'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default {\n  props: {\n    placeholder: {\n      type: String,\n      default: 'Search for something...',\n    },\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    visibleList: {\n      type: Boolean,\n      default: false,\n    },\n    modelValue: {\n      type: String,\n      default: '',\n    },\n    big: {\n      type: Boolean,\n      default: false,\n    },\n    small: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue', 'event'],\n  data() {\n    return {\n      isShow: false,\n      text: '',\n      selectIndex: -1,\n      focus: false,\n      listStyle: {\n        height: 0,\n      },\n    }\n  },\n  watch: {\n    list(n) {\n      if (!this.visibleList) return\n      if (this.selectIndex > -1) this.selectIndex = -1\n      this.$nextTick(() => {\n        this.listStyle.height = this.$refs.dom_list.scrollHeight + 'px'\n      })\n    },\n    modelValue(n) {\n      this.text = n\n    },\n    visibleList(n) {\n      n ? this.showList() : this.hideList()\n    },\n  },\n  mounted() {\n    if (appSetting['search.isFocusSearchBox']) this.handleFocusInput()\n    this.handleRegisterEvent('on')\n  },\n  beforeUnmount() {\n    this.handleRegisterEvent('off')\n  },\n  methods: {\n    handleRegisterEvent(action) {\n      let eventHub = window.key_event\n      let name = action == 'on' ? 'on' : 'off'\n      // eslint-disable-next-line @typescript-eslint/unbound-method\n      eventHub[name](HOTKEY_COMMON.focusSearchInput.action, this.handleFocusInput)\n    },\n    handleFocusInput() {\n      this.$refs.dom_input.focus()\n    },\n    handleTemplistClick(index) {\n      console.log(index)\n      this.sendEvent('listClick', index)\n    },\n    handleFocus() {\n      this.focus = true\n      this.sendEvent('focus')\n    },\n    handleBlur() {\n      setTimeout(() => {\n        this.focus = false\n        this.sendEvent('blur')\n      }, 80)\n    },\n    handleSearch() {\n      this.hideList()\n      if (this.selectIndex < 0) {\n        this.sendEvent('submit')\n        return\n      }\n      this.sendEvent('listClick', this.selectIndex)\n    },\n    showList() {\n      this.isShow = true\n      this.listStyle.height = this.$refs.dom_list.scrollHeight + 'px'\n    },\n    hideList() {\n      this.isShow = false\n      this.listStyle.height = 0\n      this.$nextTick(() => {\n        this.selectIndex = -1\n      })\n    },\n    sendEvent(action, data) {\n      this.$emit('event', {\n        action,\n        data,\n      })\n    },\n    handleKeyDown() {\n      if (this.list.length) {\n        this.selectIndex = this.selectIndex + 1 < this.list.length ? this.selectIndex + 1 : 0\n      } else if (this.selectIndex > -1) {\n        this.selectIndex = -1\n      }\n    },\n    handleKeyUp() {\n      if (this.list.length) {\n        this.selectIndex = this.selectIndex - 1 < -1 ? this.list.length - 1 : this.selectIndex - 1\n      } else if (this.selectIndex > -1) {\n        this.selectIndex = -1\n      }\n    },\n    handleContextMenu() {\n      let str = clipboardReadText()\n      str = str.trim()\n      str = str.replace(/\\t|\\r\\n|\\n|\\r/g, ' ')\n      str = str.replace(/\\s+/g, ' ')\n      let dom_input = this.$refs.dom_input\n      this.text = this.text.substring(0, dom_input.selectionStart) + str + this.text.substring(dom_input.selectionEnd, this.text.length)\n      this.$emit('update:modelValue', this.text)\n    },\n    handleClearList() {\n      this.text = ''\n      this.$emit('update:modelValue', this.text)\n      this.sendEvent('submit')\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  position: relative;\n  width: 35%;\n  height: @height-toolbar * 0.52;\n  -webkit-app-region: no-drag;\n}\n\n.search {\n  position: absolute;\n  width: 100%;\n  border-radius: @form-radius;\n  transition: box-shadow .4s ease, background-color @transition-normal;\n  display: flex;\n  flex-flow: column nowrap;\n  background-color: var(--color-primary-light-300-alpha-700);\n\n  &.active {\n    background-color: var(--color-primary-light-600-alpha-100);\n    box-shadow: 0 1px 5px 0 rgba(0,0,0,.2);\n    .form {\n      input {\n        border-bottom-left-radius: 0;\n\n      }\n      button {\n        border-bottom-right-radius: 0;\n      }\n    }\n  }\n  .form {\n    display: flex;\n    height: @height-toolbar * 0.52;\n    position: relative;\n    input {\n      flex: auto;\n      // border: 1px solid;\n      border-top-left-radius: 3px;\n      border-bottom-left-radius: 3px;\n      background-color: transparent;\n      // border-bottom: 2px solid var(--color-primary);\n      // border-color: var(--color-primary);\n      border: none;\n      min-width: 0;\n\n      outline: none;\n      // height: @height-toolbar * .7;\n      padding: 0 5px;\n      overflow: hidden;\n      font-size: 13.5px;\n      line-height: @height-toolbar * 0.52 + 5px;\n      &::placeholder {\n        color: var(--color-button-font);\n        font-size: .98em;\n      }\n    }\n    button {\n      flex: none;\n      border: none;\n      // background-color: @color-search-form-background;\n      background-color: transparent;\n      outline: none;\n      cursor: pointer;\n      height: 100%;\n      padding: 6px 7px;\n      color: var(--color-button-font);\n      transition: background-color .2s ease;\n\n      &:last-child {\n        border-top-right-radius: 3px;\n        border-bottom-right-radius: 3px;\n      }\n\n      &:hover {\n        background-color: var(--color-button-background-hover);\n      }\n      &:active {\n        background-color: var(--color-button-background-active);\n      }\n    }\n  }\n  .list {\n    // background-color: @color-search-form-background;\n    font-size: 13px;\n    transition: .3s ease;\n    height: 0;\n    transition-property: height;\n    overflow: hidden;\n    li {\n      cursor: pointer;\n      padding: 8px 5px;\n      transition: background-color .2s ease;\n      line-height: 1.3;\n      span {\n        .mixin-ellipsis-2();\n      }\n\n      &.select {\n        background-color: var(--color-primary-dark-100-alpha-700);\n      }\n      &:last-child {\n        border-bottom-left-radius: 3px;\n        border-bottom-right-radius: 3px;\n      }\n    }\n  }\n}\n\n.big {\n  width: 100%;\n  // input {\n  //   line-height: 30px;\n  // }\n  .form {\n    height: 30px;\n    button {\n      padding: 6px 10px;\n    }\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/components/material/SongList.vue",
    "content": "<template>\n  <div :class=\"$style.container\" />\n</template>\n\n<script>\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/core/apiSource.ts",
    "content": "import { apiSource, qualityList, userApi } from '@renderer/store'\nimport { appSetting, setApiSource } from '@renderer/store/setting'\nimport { setUserApi as setUserApiAction } from '@renderer/utils/ipc'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'\n\nlet prevId = ''\nexport const setUserApi = async(apiId: string) => {\n  if (prevId == apiId) return\n  prevId = apiId\n  if (window.lx.apiInitPromise[1]) {\n    window.lx.apiInitPromise[0] = new Promise<boolean>(resolve => {\n      window.lx.apiInitPromise[1] = false\n      window.lx.apiInitPromise[2] = (result: boolean) => {\n        window.lx.apiInitPromise[1] = true\n        resolve(result)\n      }\n    })\n  }\n\n  if (/^user_api/.test(apiId)) {\n    qualityList.value = {}\n    userApi.status = false\n    userApi.message = 'initing'\n\n    await setUserApiAction(apiId).then(() => {\n      if (prevId != apiId) return\n      apiSource.value = apiId\n    }).catch(err => {\n      if (prevId != apiId) return\n      if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](false)\n      console.log(err)\n      let api = apiSourceInfo.find(api => !api.disabled)\n      if (!api) return\n      apiSource.value = api.id\n      if (api.id != appSetting['common.apiSource']) setApiSource(api.id)\n    })\n  } else {\n    // @ts-expect-error\n    qualityList.value = musicSdk.supportQuality[apiId] ?? {}\n    apiSource.value = apiId\n    void setUserApiAction(apiId)\n    if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](true)\n  }\n\n  if (prevId != apiId) return\n  if (apiId != appSetting['common.apiSource']) setApiSource(apiId)\n}\n"
  },
  {
    "path": "src/renderer/core/dislikeList.ts",
    "content": "// import { toRaw } from '@common/utils/vueTools'\nimport { DISLIKE_EVENT_NAME } from '@common/ipcNames'\nimport { rendererInvoke, rendererOff, rendererOn } from '@common/rendererIpc'\nimport { action } from '@renderer/store/dislikeList'\n\n\nexport const initDislikeInfo = async() => {\n  action.initDislikeInfo(await rendererInvoke<LX.Dislike.DislikeInfo>(DISLIKE_EVENT_NAME.get_dislike_music_infos))\n}\n\nexport const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem | null) => {\n  if (!info) return false\n  return action.hasDislike(info)\n}\n\nexport const addDislikeInfo = async(infos: LX.Dislike.DislikeMusicInfo[]) => {\n  await rendererInvoke<LX.Dislike.DislikeMusicInfo[]>(DISLIKE_EVENT_NAME.add_dislike_music_infos, infos)\n}\n\nexport const overwirteDislikeInfo = async(rules: string) => {\n  await rendererInvoke<string>(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, rules)\n}\n\nexport const clearDislikeInfo = async() => {\n  await rendererInvoke(DISLIKE_EVENT_NAME.clear_dislike_music_infos)\n}\n\n\nconst noop = () => {}\n\nexport const registerRemoteDislikeAction = (onListChanged: (listIds: string[]) => void = noop) => {\n  const add_dislike_music_infos = ({ params: datas }: LX.IpcRendererEventParams<LX.Dislike.DislikeMusicInfo[]>) => {\n    action.addDislikeInfo(datas)\n  }\n  const overwrite_dislike_music_infos = ({ params: datas }: LX.IpcRendererEventParams<LX.Dislike.DislikeRules>) => {\n    action.overwirteDislikeInfo(datas)\n  }\n  const clear_dislike_music_infos = () => {\n    return action.clearDislikeInfo()\n  }\n\n  rendererOn(DISLIKE_EVENT_NAME.add_dislike_music_infos, add_dislike_music_infos)\n  rendererOn(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, overwrite_dislike_music_infos)\n  rendererOn(DISLIKE_EVENT_NAME.clear_dislike_music_infos, clear_dislike_music_infos)\n\n  return () => {\n    rendererOff(DISLIKE_EVENT_NAME.add_dislike_music_infos, add_dislike_music_infos)\n    rendererOff(DISLIKE_EVENT_NAME.overwrite_dislike_music_infos, overwrite_dislike_music_infos)\n    rendererOff(DISLIKE_EVENT_NAME.clear_dislike_music_infos, clear_dislike_music_infos)\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/globalData.ts",
    "content": "// import defaultSetting from '@common/defaultSetting'\nimport createWorkers from '@renderer/worker'\n\nwindow.lx = {\n  // appSetting: defaultSetting,\n  isEditingHotKey: false,\n  isPlayedStop: false,\n  appHotKeyConfig: {\n    local: {\n      enable: false,\n      keys: {},\n    },\n    global: {\n      enable: false,\n      keys: {},\n    },\n  },\n  songListInfo: {\n    fromName: '',\n    searchKey: '',\n    searchPosition: 0,\n    songlistKey: '',\n    songlistPosition: 0,\n  },\n  restorePlayInfo: null,\n  worker: createWorkers(),\n  isProd: process.env.NODE_ENV == 'production',\n  rootOffset: window.dt ? 0 : 8,\n  apiInitPromise: [Promise.resolve(false), true, () => {}],\n}\n\nwindow.lxData = {}\n\nwindow.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS\n"
  },
  {
    "path": "src/renderer/core/lyric.ts",
    "content": "import Lyric from '@common/utils/lyric-font-player'\nimport { getAnalyser, getCurrentTime as getPlayerCurrentTime } from '@renderer/plugins/player'\nimport { lyric, setLines, setOffset, setTempOffset, setText } from '@renderer/store/player/lyric'\nimport { isPlay, musicInfo } from '@renderer/store/player/state'\nimport { setStatusText } from '@renderer/store/player/action'\nimport { markRawList } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\nimport { onNewDesktopLyricProcess } from '@renderer/utils/ipc'\n\nconst getCurrentTime = () => {\n  return getPlayerCurrentTime() * 1000\n}\n\nlet lrc: Lyric\nlet desktopLyricPort: Electron.IpcRendererEvent['ports'][0] | null = null\nconst analyserTools: {\n  dataArray: Uint8Array\n  bufferLength: number\n  analyser: AnalyserNode | null\n  sendDataArray: () => void\n} = {\n  dataArray: new Uint8Array(),\n  bufferLength: 0,\n  analyser: null,\n  sendDataArray() {\n    if (this.analyser == null) {\n      this.analyser = getAnalyser()\n      // console.log(this.analyser)\n      if (!this.analyser) return\n      this.bufferLength = this.analyser.frequencyBinCount\n    }\n    const dataArray = new Uint8Array(this.bufferLength)\n    this.analyser.getByteFrequencyData(dataArray)\n    sendDesktopLyricInfo({\n      action: 'send_analyser_data_array',\n      data: dataArray,\n    }, [dataArray.buffer])\n  },\n}\n\nexport const sendDesktopLyricInfo = (info: LX.DesktopLyric.LyricActions, transferList?: Transferable[]) => {\n  if (desktopLyricPort == null) return\n  if (transferList) desktopLyricPort.postMessage(info, transferList)\n  else desktopLyricPort.postMessage(info)\n}\nconst handleDesktopLyricMessage = (action: LX.DesktopLyric.WinMainActions) => {\n  switch (action) {\n    case 'get_info':\n      sendDesktopLyricInfo({\n        action: 'set_info',\n        data: {\n          id: musicInfo.id,\n          singer: musicInfo.singer,\n          name: musicInfo.name,\n          album: musicInfo.album,\n          lrc: musicInfo.lrc,\n          tlrc: musicInfo.tlrc,\n          rlrc: musicInfo.rlrc,\n          lxlrc: musicInfo.lxlrc,\n          // pic: musicInfo.pic,\n          isPlay: isPlay.value,\n          line: lyric.line,\n          played_time: getCurrentTime(),\n        },\n      })\n      break\n    case 'get_status':\n      sendDesktopLyricInfo({\n        action: 'set_status',\n        data: {\n          isPlay: isPlay.value,\n          line: lyric.line,\n          played_time: getCurrentTime(),\n        },\n      })\n      break\n    case 'get_analyser_data_array':\n      analyserTools.sendDataArray()\n      break\n    default:\n      break\n  }\n}\nexport const init = () => {\n  lrc = new Lyric({\n    shadowContent: false,\n    onPlay(line, text) {\n      setText(text, Math.max(line, 0))\n      setStatusText(text)\n      window.app_event.lyricLinePlay(text, line)\n      // console.log(line, text)\n    },\n    onSetLyric(lines, offset) { // listening lyrics seting event\n      // console.log(lines) // lines is array of all lyric text\n      setLines(markRawList([...lines]))\n      setText(lines[0] ?? '', 0)\n      setOffset(offset) // 歌词延迟\n      setTempOffset(0) // 重置临时延迟\n    },\n    onUpdateLyric(lines) {\n      setLines(markRawList([...lines]))\n      setText(lines[0] ?? '', 0)\n    },\n    rate: appSetting['player.playbackRate'],\n    // offset: 80,\n  })\n\n  onNewDesktopLyricProcess(({ event }) => {\n    console.log('onNewDesktopLyricProcess')\n    const [port] = event.ports\n    desktopLyricPort = port\n\n    port.onmessage = ({ data }) => {\n      handleDesktopLyricMessage(data.action)\n      // The event data can be any serializable object (and the event could even\n      // carry other MessagePorts with it!)\n      // const result = doWork(event.data)\n      // port.postMessage(result)\n    }\n\n    port.onmessageerror = (event) => {\n      console.log('onmessageerror', event)\n    }\n  })\n}\n\nexport const setLyricOffset = (offset: number) => {\n  const tempOffset = offset - lyric.offset\n  setTempOffset(tempOffset)\n  lrc.setOffset(tempOffset)\n  sendDesktopLyricInfo({\n    action: 'set_offset',\n    data: tempOffset,\n  })\n\n  if (isPlay.value) {\n    setTimeout(() => {\n      const time = getCurrentTime()\n      sendDesktopLyricInfo({\n        action: 'set_play',\n        data: time,\n      })\n      lrc.play(time)\n    })\n  }\n}\n\nexport const setPlaybackRate = (rate: number) => {\n  lrc.setPlaybackRate(rate)\n\n  if (isPlay.value) {\n    setTimeout(() => {\n      const time = getCurrentTime()\n      lrc.play(time)\n    })\n  }\n}\n\nexport const setLyric = () => {\n  if (!musicInfo.id) return\n  if (musicInfo.lrc) {\n    const extendedLyrics = []\n    if (appSetting['player.isShowLyricRoma'] && musicInfo.rlrc) extendedLyrics.push(musicInfo.rlrc)\n    if (appSetting['player.isShowLyricTranslation'] && musicInfo.tlrc) extendedLyrics.push(musicInfo.tlrc)\n    if (appSetting['player.isSwapLyricTranslationAndRoma']) extendedLyrics.reverse()\n\n    lrc.setLyric(\n      appSetting['player.isPlayLxlrc'] && musicInfo.lxlrc ? musicInfo.lxlrc : musicInfo.lrc,\n      extendedLyrics,\n    )\n    sendDesktopLyricInfo({\n      action: 'set_lyric',\n      data: {\n        lrc: musicInfo.lrc,\n        tlrc: musicInfo.tlrc,\n        rlrc: musicInfo.rlrc,\n        lxlrc: musicInfo.lxlrc,\n      },\n    })\n  }\n\n  if (isPlay.value) {\n    setTimeout(() => {\n      const time = getCurrentTime()\n      sendDesktopLyricInfo({ action: 'set_play', data: time })\n      lrc.play(time)\n    })\n  }\n}\n\nexport const setDisabledAutoPause = (disabledAutoPause: boolean) => {\n  lrc.setDisabledAutoPause(disabledAutoPause)\n}\n\nlet sources = new Map<string, boolean>()\nlet prevDisabled = false\nexport const setDisableAutoPauseBySource = (disabled: boolean, source: string) => {\n  sources.set(source, disabled)\n  const currentDisabled = Array.from(sources.values()).some(e => e)\n  if (prevDisabled == currentDisabled) return\n  prevDisabled = currentDisabled\n  setDisabledAutoPause(currentDisabled)\n}\n\n\nexport const play = () => {\n  // if (!musicInfo.lrc) return\n  const currentTime = getCurrentTime()\n  lrc.play(currentTime)\n  sendDesktopLyricInfo({ action: 'set_play', data: currentTime })\n}\n\nexport const pause = () => {\n  lrc.pause()\n  sendDesktopLyricInfo({ action: 'set_pause' })\n}\n\nexport const stop = () => {\n  lrc.setLyric('')\n  sendDesktopLyricInfo({ action: 'set_stop' })\n  // setLines([])\n  setText('', 0)\n}\n\nexport const sendInfo = () => {\n  sendDesktopLyricInfo({\n    action: 'set_info',\n    data: {\n      id: musicInfo.id,\n      singer: musicInfo.singer,\n      name: musicInfo.name,\n      album: musicInfo.album,\n      lrc: musicInfo.lrc,\n      tlrc: musicInfo.tlrc,\n      rlrc: musicInfo.rlrc,\n      lxlrc: musicInfo.lxlrc,\n      // pic: musicInfo.pic,\n      isPlay: isPlay.value,\n      line: lyric.line,\n      played_time: getCurrentTime(),\n    },\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/music/download.ts",
    "content": "import { getDownloadFilePath } from '@renderer/utils/music'\n\nimport {\n  getMusicUrl as getOnlineMusicUrl,\n  getPicUrl as getOnlinePicUrl,\n  getLyricInfo as getOnlineLyricInfo,\n} from './online'\nimport { buildLyricInfo, getCachedLyricInfo } from './utils'\nimport { buildSavePath } from '@renderer/store/download/utils'\n\nexport const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {\n  musicInfo: LX.Download.ListItem\n  isRefresh: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  allowToggleSource?: boolean\n}): Promise<string> => {\n  if (!isRefresh) {\n    const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))\n    if (path) return path\n  }\n\n  return getOnlineMusicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource, allowToggleSource })\n}\n\nexport const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource = () => {} }: {\n  musicInfo: LX.Download.ListItem\n  isRefresh: boolean\n  listId?: string | null\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if (!isRefresh) {\n    const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))\n    if (path) {\n      const pic = await window.lx.worker.main.getMusicFilePic(path)\n      if (pic) return pic\n    }\n\n    const onlineMusicInfo = musicInfo.metadata.musicInfo\n    if (onlineMusicInfo.meta.picUrl) return onlineMusicInfo.meta.picUrl\n  }\n\n  return getOnlinePicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource }).then((url) => {\n    // TODO: when listId required save url (update downloadInfo)\n\n    return url\n  })\n}\n\nexport const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {\n  musicInfo: LX.Download.ListItem\n  isRefresh: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<LX.Player.LyricInfo> => {\n  if (!isRefresh) {\n    const lyricInfo = await getCachedLyricInfo(musicInfo.metadata.musicInfo)\n    if (lyricInfo) return buildLyricInfo(lyricInfo)\n  }\n\n  return getOnlineLyricInfo({\n    musicInfo: musicInfo.metadata.musicInfo,\n    isRefresh,\n    onToggleSource,\n  }).catch(async() => {\n    // 尝试读取文件内歌词\n    const path = await getDownloadFilePath(musicInfo, buildSavePath(musicInfo))\n    if (path) {\n      const rawlrcInfo = await window.lx.worker.main.getMusicFileLyric(path)\n      if (rawlrcInfo) return buildLyricInfo(rawlrcInfo)\n    }\n\n    throw new Error('failed')\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/music/index.ts",
    "content": "// if (targetSong.key) { // 如果是已下载的歌曲\n//   const filePath = path.join(appSetting['download.savePath'], targetSong.metadata.fileName)\n//   // console.log(filePath)\n\nimport {\n  getMusicUrl as getOnlineMusicUrl,\n  getPicUrl as getOnlinePicUrl,\n  getLyricInfo as getOnlineLyricInfo,\n} from './online'\nimport {\n  getMusicUrl as getDownloadMusicUrl,\n  getPicUrl as getDownloadPicUrl,\n  getLyricInfo as getDownloadLyricInfo,\n} from './download'\nimport {\n  getMusicUrl as getLocalMusicUrl,\n  getPicUrl as getLocalPicUrl,\n  getLyricInfo as getLocalLyricInfo,\n} from './local'\n\n\nexport const getMusicUrl = async({\n  musicInfo,\n  quality,\n  isRefresh = false,\n  onToggleSource,\n  allowToggleSource,\n}: {\n  musicInfo: LX.Music.MusicInfo | LX.Download.ListItem\n  isRefresh?: boolean\n  quality?: LX.Quality\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  allowToggleSource?: boolean\n}): Promise<string> => {\n  if ('progress' in musicInfo) {\n    return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })\n  } else if (musicInfo.source == 'local') {\n    return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })\n  } else {\n    return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource, allowToggleSource })\n  }\n}\n\nexport const getPicPath = async({\n  musicInfo,\n  isRefresh = false,\n  listId,\n  onToggleSource,\n}: {\n  musicInfo: LX.Music.MusicInfo | LX.Download.ListItem\n  listId?: string | null\n  isRefresh?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if ('progress' in musicInfo) {\n    return getDownloadPicUrl({ musicInfo, isRefresh, listId, onToggleSource })\n  } else if (musicInfo.source == 'local') {\n    return getLocalPicUrl({ musicInfo, isRefresh, listId, onToggleSource })\n  } else {\n    return getOnlinePicUrl({ musicInfo, isRefresh, listId, onToggleSource })\n  }\n}\n\nexport const getLyricInfo = async({\n  musicInfo,\n  isRefresh = false,\n  onToggleSource,\n}: {\n  musicInfo: LX.Music.MusicInfo | LX.Download.ListItem\n  isRefresh?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<LX.Player.LyricInfo> => {\n  if ('progress' in musicInfo) {\n    return getDownloadLyricInfo({ musicInfo, isRefresh, onToggleSource })\n  } else if (musicInfo.source == 'local') {\n    return getLocalLyricInfo({ musicInfo, isRefresh, onToggleSource })\n  } else {\n    return getOnlineLyricInfo({ musicInfo, isRefresh, onToggleSource })\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/music/local.ts",
    "content": "import { encodePath } from '@common/utils/common'\nimport { updateListMusics } from '@renderer/store/list/action'\nimport { saveLyric, saveMusicUrl } from '@renderer/utils/ipc'\nimport { getLocalFilePath } from '@renderer/utils/music'\n\nimport {\n  buildLyricInfo,\n  getCachedLyricInfo,\n  getOnlineOtherSourceLyricByLocal,\n  getOnlineOtherSourceLyricInfo,\n  getOnlineOtherSourceMusicUrl,\n  getOnlineOtherSourceMusicUrlByLocal,\n  getOnlineOtherSourcePicByLocal,\n  getOnlineOtherSourcePicUrl,\n  getOtherSource,\n} from './utils'\n\n\nconst getOtherSourceByLocal = async<T>(musicInfo: LX.Music.MusicInfoLocal, handler: (infos: LX.Music.MusicInfoOnline[]) => Promise<T>) => {\n  let result: LX.Music.MusicInfoOnline[] = []\n  result = await getOtherSource(musicInfo)\n  if (result.length) try { return await handler(result) } catch {}\n  if (musicInfo.name.includes('-')) {\n    const [name, singer] = musicInfo.name.split('-').map(val => val.trim())\n    result = await getOtherSource({\n      ...musicInfo,\n      name,\n      singer,\n    }, true)\n    if (result.length) try { return await handler(result) } catch {}\n    result = await getOtherSource({\n      ...musicInfo,\n      name: singer,\n      singer: name,\n    }, true)\n    if (result.length) try { return await handler(result) } catch {}\n  }\n  let fileName = musicInfo.meta.filePath.split(/\\/|\\\\/).at(-1)\n  if (fileName) {\n    fileName = fileName.substring(0, fileName.lastIndexOf('.'))\n    if (fileName != musicInfo.name) {\n      if (fileName.includes('-')) {\n        const [name, singer] = fileName.split('-').map(val => val.trim())\n        result = await getOtherSource({\n          ...musicInfo,\n          name,\n          singer,\n        }, true)\n        if (result.length) try { return await handler(result) } catch {}\n        result = await getOtherSource({\n          ...musicInfo,\n          name: singer,\n          singer: name,\n        }, true)\n      } else {\n        result = await getOtherSource({\n          ...musicInfo,\n          name: fileName,\n          singer: '',\n        }, true)\n      }\n      if (result.length) try { return await handler(result) } catch {}\n    }\n  }\n\n  throw new Error('source not found')\n}\n\nexport const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoLocal\n  isRefresh: boolean\n  allowToggleSource?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if (!isRefresh) {\n    const path = await getLocalFilePath(musicInfo)\n    if (path) return encodePath(path)\n  }\n\n  try {\n    return await getOnlineOtherSourceMusicUrlByLocal(musicInfo, isRefresh).then(({ url, quality, isFromCache }) => {\n      if (!isFromCache) void saveMusicUrl(musicInfo, quality, url)\n      return url\n    })\n  } catch {}\n\n  if (!allowToggleSource) throw new Error('failed')\n\n  onToggleSource()\n  return getOtherSourceByLocal(musicInfo, async(otherSource) => {\n    return getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {\n      // saveLyric(musicInfo, data.lyricInfo)\n      if (!isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url)\n\n      // TODO: save url ?\n      return url\n    })\n  })\n}\n\nexport const getPicUrl = async({ musicInfo, listId, isRefresh, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoLocal\n  listId?: string | null\n  isRefresh: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if (!isRefresh) {\n    const pic = await window.lx.worker.main.getMusicFilePic(musicInfo.meta.filePath)\n    if (pic) return pic\n\n    if (musicInfo.meta.picUrl) return musicInfo.meta.picUrl\n  }\n\n  try {\n    return await getOnlineOtherSourcePicByLocal(musicInfo).then(({ url }) => {\n      return url\n    })\n  } catch {}\n\n  onToggleSource()\n  return getOtherSourceByLocal(musicInfo, async(otherSource) => {\n    return getOnlineOtherSourcePicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => {\n      if (listId) {\n        musicInfo.meta.picUrl = url\n        void updateListMusics([{ id: listId, musicInfo }])\n      }\n\n      return url\n    })\n  })\n}\n\nexport const getLyricInfo = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoLocal\n  isRefresh: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<LX.Player.LyricInfo> => {\n  if (!isRefresh) {\n    const [lyricInfo, fileLyricInfo] = await Promise.all([getCachedLyricInfo(musicInfo), window.lx.worker.main.getMusicFileLyric(musicInfo.meta.filePath)])\n    // console.log(lyricInfo, fileLyricInfo)\n    if (lyricInfo?.lyric && lyricInfo.lyric != fileLyricInfo?.lyric) {\n      // 存在已编辑歌词\n      return buildLyricInfo({ ...lyricInfo, rawlrcInfo: fileLyricInfo ?? lyricInfo.rawlrcInfo })\n    }\n\n    if (fileLyricInfo) return buildLyricInfo(fileLyricInfo)\n    if (lyricInfo?.lyric) return buildLyricInfo(lyricInfo)\n  }\n\n  try {\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    return await getOnlineOtherSourceLyricByLocal(musicInfo, isRefresh).then(({ lyricInfo, isFromCache }) => {\n      if (!isFromCache) void saveLyric(musicInfo, lyricInfo)\n      return buildLyricInfo(lyricInfo)\n    })\n  } catch {}\n\n  onToggleSource()\n  return getOtherSourceByLocal(musicInfo, async(otherSource) => {\n    return getOnlineOtherSourceLyricInfo({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(async({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => {\n      void saveLyric(musicInfo, lyricInfo)\n\n      if (isFromCache) return buildLyricInfo(lyricInfo)\n      void saveLyric(targetMusicInfo, lyricInfo)\n\n      return buildLyricInfo(lyricInfo)\n    })\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/music/online.ts",
    "content": "import { updateListMusics } from '@renderer/store/list/action'\nimport { appSetting } from '@renderer/store/setting'\nimport {\n  saveLyric,\n  saveMusicUrl,\n  getMusicUrl as getStoreMusicUrl,\n} from '@renderer/utils/ipc'\nimport {\n  buildLyricInfo,\n  getPlayQuality,\n  handleGetOnlineLyricInfo,\n  handleGetOnlineMusicUrl,\n  handleGetOnlinePicUrl,\n  getCachedLyricInfo,\n} from './utils'\n\n/* export const setMusicUrl = ({ musicInfo, type, url }: {\n  musicInfo: LX.Music.MusicInfo\n  type: LX.Quality\n  url: string\n}) => {\n  saveMusicUrl(musicInfo, type, url)\n}\n\nexport const setPic = (datas: {\n  listId: string\n  musicInfo: LX.Music.MusicInfo\n  url: string\n}) => {\n  datas.musicInfo.img = datas.url\n  updateMusicInfo({\n    listId: datas.listId,\n    id: datas.musicInfo.songmid,\n    data: { img: datas.url },\n    musicInfo: datas.musicInfo,\n  })\n}\n */\n\n\nexport const getMusicUrl = async({ musicInfo, quality, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  quality?: LX.Quality\n  isRefresh: boolean\n  allowToggleSource?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  // if (!musicInfo._types[type]) {\n  //   // 兼容旧版酷我源搜索列表过滤128k音质的bug\n  //   if (!(musicInfo.source == 'kw' && type == '128k')) throw new Error('该歌曲没有可播放的音频')\n\n  //   // return Promise.reject(new Error('该歌曲没有可播放的音频'))\n  // }\n  const targetQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo)\n  const cachedUrl = await getStoreMusicUrl(musicInfo, targetQuality)\n  if (cachedUrl && !isRefresh) return cachedUrl\n\n  return handleGetOnlineMusicUrl({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {\n    if (targetMusicInfo.id != musicInfo.id && !isFromCache) void saveMusicUrl(targetMusicInfo, targetQuality, url)\n    void saveMusicUrl(musicInfo, targetQuality, url)\n    return url\n  })\n}\n\nexport const getPicUrl = async({ musicInfo, listId, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  listId?: string | null\n  isRefresh: boolean\n  allowToggleSource?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if (musicInfo.meta.picUrl && !isRefresh) return musicInfo.meta.picUrl\n  return handleGetOnlinePicUrl({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(({ url, musicInfo: targetMusicInfo, isFromCache }) => {\n    // picRequest = null\n    if (listId) {\n      musicInfo.meta.picUrl = url\n      void updateListMusics([{ id: listId, musicInfo }])\n    }\n    // savePic({ musicInfo, url, listId })\n    return url\n  })\n}\nexport const getLyricInfo = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  isRefresh: boolean\n  allowToggleSource?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<LX.Player.LyricInfo> => {\n  if (!isRefresh) {\n    const lyricInfo = await getCachedLyricInfo(musicInfo)\n    if (lyricInfo) return buildLyricInfo(lyricInfo)\n  }\n\n  // lrcRequest = music[musicInfo.source].getLyric(musicInfo)\n  return handleGetOnlineLyricInfo({ musicInfo, onToggleSource, isRefresh, allowToggleSource }).then(async({ lyricInfo, musicInfo: targetMusicInfo, isFromCache }) => {\n    // lrcRequest = null\n    if (isFromCache) return buildLyricInfo(lyricInfo)\n    if (targetMusicInfo.id == musicInfo.id) void saveLyric(musicInfo, lyricInfo)\n    else void saveLyric(targetMusicInfo, lyricInfo)\n\n    return buildLyricInfo(lyricInfo)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/music/utils.ts",
    "content": "import { qualityList } from '@renderer/store'\nimport { assertApiSupport } from '@renderer/store/utils'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport {\n  // getOtherSource as getOtherSourceFromStore,\n  // saveOtherSource as saveOtherSourceFromStore,\n  getMusicUrl as getStoreMusicUrl,\n  getPlayerLyric as getStoreLyric,\n} from '@renderer/utils/ipc'\nimport { appSetting } from '@renderer/store/setting'\nimport { langS2T, toNewMusicInfo, toOldMusicInfo } from '@renderer/utils'\nimport { requestMsg } from '@renderer/utils/message'\nimport { apis } from '@renderer/utils/musicSdk/api-source'\n\n\nconst getOtherSourcePromises = new Map()\nconst otherSourceCache = new Map<LX.Music.MusicInfo | LX.Download.ListItem, LX.Music.MusicInfoOnline[]>()\nexport const existTimeExp = /\\[\\d{1,2}:.*\\d{1,4}\\]/\n\nexport const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {\n  // if (!isRefresh && musicInfo.id) {\n  //   const cachedInfo = await getOtherSourceFromStore(musicInfo.id)\n  //   if (cachedInfo.length) return cachedInfo\n  // }\n  if (otherSourceCache.has(musicInfo)) return otherSourceCache.get(musicInfo)!\n  let key: string\n  let searchMusicInfo: {\n    name: string\n    singer: string\n    source: string\n    albumName: string\n    interval: string\n  }\n  if ('progress' in musicInfo) {\n    key = `local_${musicInfo.id}`\n    searchMusicInfo = {\n      name: musicInfo.metadata.musicInfo.name,\n      singer: musicInfo.metadata.musicInfo.singer,\n      source: musicInfo.metadata.musicInfo.source,\n      albumName: musicInfo.metadata.musicInfo.meta.albumName,\n      interval: musicInfo.metadata.musicInfo.interval ?? '',\n    }\n  } else {\n    key = `${musicInfo.source}_${musicInfo.id}`\n    searchMusicInfo = {\n      name: musicInfo.name,\n      singer: musicInfo.singer,\n      source: musicInfo.source,\n      albumName: musicInfo.meta.albumName,\n      interval: musicInfo.interval ?? '',\n    }\n  }\n  if (getOtherSourcePromises.has(key)) return getOtherSourcePromises.get(key)\n\n  const promise = new Promise<LX.Music.MusicInfoOnline[]>((resolve, reject) => {\n    let timeout: null | NodeJS.Timeout = setTimeout(() => {\n      timeout = null\n      reject(new Error('find music timeout'))\n    }, 15_000)\n    musicSdk.findMusic(searchMusicInfo).then((otherSource) => {\n      if (otherSourceCache.size > 10) otherSourceCache.clear()\n      const source = otherSource.map(toNewMusicInfo) as LX.Music.MusicInfoOnline[]\n      otherSourceCache.set(musicInfo, source)\n      resolve(source)\n    }).catch(reject).finally(() => {\n      if (timeout) clearTimeout(timeout)\n    })\n  }).then((otherSource) => {\n    // if (otherSource.length) void saveOtherSourceFromStore(musicInfo.id, otherSource)\n    return otherSource\n  }).finally(() => {\n    if (getOtherSourcePromises.has(key)) getOtherSourcePromises.delete(key)\n  })\n  getOtherSourcePromises.set(key, promise)\n  return promise\n}\n\n\nexport const buildLyricInfo = async(lyricInfo: MakeOptional<LX.Player.LyricInfo, 'rawlrcInfo'>): Promise<LX.Player.LyricInfo> => {\n  if (!appSetting['player.isS2t']) {\n    // @ts-expect-error\n    if (lyricInfo.rawlrcInfo) return lyricInfo\n    return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }\n  }\n\n  if (appSetting['player.isS2t']) {\n    const tasks = [\n      lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve(''),\n      lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''),\n      lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''),\n      lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''),\n    ]\n    if (lyricInfo.rawlrcInfo) {\n      tasks.push(lyricInfo.lyric ? langS2T(lyricInfo.lyric) : Promise.resolve(''))\n      tasks.push(lyricInfo.tlyric ? langS2T(lyricInfo.tlyric) : Promise.resolve(''))\n      tasks.push(lyricInfo.rlyric ? langS2T(lyricInfo.rlyric) : Promise.resolve(''))\n      tasks.push(lyricInfo.lxlyric ? langS2T(lyricInfo.lxlyric) : Promise.resolve(''))\n    }\n    return Promise.all(tasks).then(([lyric, tlyric, rlyric, lxlyric, lyric_raw, tlyric_raw, rlyric_raw, lxlyric_raw]) => {\n      const rawlrcInfo = lyric_raw ? {\n        lyric: lyric_raw,\n        tlyric: tlyric_raw,\n        rlyric: rlyric_raw,\n        lxlyric: lxlyric_raw,\n      } : {\n        lyric,\n        tlyric,\n        rlyric,\n        lxlyric,\n      }\n      return {\n        lyric,\n        tlyric,\n        rlyric,\n        lxlyric,\n        rawlrcInfo,\n      }\n    })\n  }\n\n  // @ts-expect-error\n  return lyricInfo.rawlrcInfo ? lyricInfo : { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }\n}\n\nexport const getCachedLyricInfo = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Player.LyricInfo | null> => {\n  let lrcInfo = await getStoreLyric(musicInfo)\n  // lrcInfo = {} as unknown as LX.Player.LyricInfo\n  if (existTimeExp.test(lrcInfo.lyric)) {\n    if (lrcInfo.tlyric != null) {\n      // if (musicInfo.lrc.startsWith('\\ufeff[id:$00000000]')) {\n      //   let str = musicInfo.lrc.replace('\\ufeff[id:$00000000]\\n', '')\n      //   commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })\n      // } else if (musicInfo.lrc.startsWith('[id:$00000000]')) {\n      //   let str = musicInfo.lrc.replace('[id:$00000000]\\n', '')\n      //   commit('setLrc', { musicInfo, lyric: str, tlyric: musicInfo.tlrc, lxlyric: musicInfo.tlrc })\n      // }\n\n      if (lrcInfo.lxlyric == null) {\n        switch (musicInfo.source) { // 以下源支持lxlyric 重新获取\n          case 'kg':\n          case 'kw':\n          case 'mg':\n          case 'wy':\n          case 'tx':\n            break\n          default:\n            return lrcInfo\n        }\n      } else if (lrcInfo.rlyric == null) {\n        // 以下源支持 rlyric 重新获取\n        if (!['wy', 'kg', 'tx'].includes(musicInfo.source)) return lrcInfo\n      } else return lrcInfo\n    }\n    if (musicInfo.source == 'local') return lrcInfo\n  }\n  return null\n}\n\nexport const getOnlineOtherSourceMusicUrlByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{\n  url: string\n  quality: LX.Quality\n  isFromCache: boolean\n}> => {\n  if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed')\n\n  const quality = '128k'\n\n  const cachedUrl = await getStoreMusicUrl(musicInfo, quality)\n  if (cachedUrl && !isRefresh) return { url: cachedUrl, quality, isFromCache: true }\n\n  let reqPromise\n  try {\n    reqPromise = apis('local').getMusicUrl(toOldMusicInfo(musicInfo), null).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n\n  return reqPromise.then(({ url }: { url: string }) => {\n    return { url, quality, isFromCache: false }\n  })\n}\n\nexport const getOnlineOtherSourceLyricByLocal = async(musicInfo: LX.Music.MusicInfoLocal, isRefresh: boolean): Promise<{\n  lyricInfo: LX.Music.LyricInfo\n  isFromCache: boolean\n}> => {\n  if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed')\n\n  const lyricInfo = await getCachedLyricInfo(musicInfo)\n  if (lyricInfo && !isRefresh) return { lyricInfo, isFromCache: true }\n\n  let reqPromise\n  try {\n    reqPromise = apis('local').getLyric(toOldMusicInfo(musicInfo)).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n\n  return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {\n    return { lyricInfo, isFromCache: false }\n  })\n}\n\nexport const getOnlineOtherSourcePicByLocal = async(musicInfo: LX.Music.MusicInfoLocal): Promise<{\n  url: string\n}> => {\n  if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed')\n\n  let reqPromise\n  try {\n    reqPromise = apis('local').getPic(toOldMusicInfo(musicInfo)).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n\n  return reqPromise.then((url: string) => {\n    return { url }\n  })\n}\n\nexport const TRY_QUALITYS_LIST = ['flac24bit', 'flac', '320k'] as const\ntype TryQualityType = typeof TRY_QUALITYS_LIST[number]\nexport const getPlayQuality = (highQuality: LX.Quality, musicInfo: LX.Music.MusicInfoOnline): LX.Quality => {\n  let type: LX.Quality = '128k'\n  if (TRY_QUALITYS_LIST.includes(highQuality as TryQualityType)) {\n    let list = qualityList.value[musicInfo.source]\n\n    let t = TRY_QUALITYS_LIST\n      .slice(TRY_QUALITYS_LIST.indexOf(highQuality as TryQualityType))\n      .find(q => musicInfo.meta._qualitys[q] && list?.includes(q))\n\n    if (t) type = t\n  }\n  return type\n}\n\nexport const getOnlineOtherSourceMusicUrl = async({ musicInfos, quality, onToggleSource, isRefresh, retryedSource = [] }: {\n  musicInfos: LX.Music.MusicInfoOnline[]\n  quality?: LX.Quality\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  isRefresh: boolean\n  retryedSource?: LX.OnlineSource[]\n}): Promise<{\n  url: string\n  musicInfo: LX.Music.MusicInfoOnline\n  quality: LX.Quality\n  isFromCache: boolean\n}> => {\n  if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed')\n\n  let musicInfo: LX.Music.MusicInfoOnline | null = null\n  let itemQuality: LX.Quality | null = null\n  // eslint-disable-next-line no-cond-assign\n  while (musicInfo = (musicInfos.shift()!)) {\n    if (retryedSource.includes(musicInfo.source)) continue\n    retryedSource.push(musicInfo.source)\n    if (!assertApiSupport(musicInfo.source)) continue\n    itemQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo)\n    if (!musicInfo.meta._qualitys[itemQuality]) continue\n\n    console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)\n    onToggleSource(musicInfo)\n    break\n  }\n  if (!musicInfo || !itemQuality) throw new Error(window.i18n.t('toggle_source_failed'))\n\n  const cachedUrl = await getStoreMusicUrl(musicInfo, itemQuality)\n  if (cachedUrl && !isRefresh) return { url: cachedUrl, musicInfo, quality: itemQuality, isFromCache: true }\n\n  let reqPromise\n  try {\n    reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), itemQuality).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n  // retryedSource.includes(musicInfo.source)\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {\n    return { musicInfo, url, quality: type, isFromCache: false }\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n  }).catch((err: any) => {\n    if (err.message == requestMsg.tooManyRequests) throw err\n    console.log(err)\n    return getOnlineOtherSourceMusicUrl({ musicInfos, quality, onToggleSource, isRefresh, retryedSource })\n  })\n}\n\n/**\n * 获取在线音乐URL\n */\nexport const handleGetOnlineMusicUrl = async({ musicInfo, quality, onToggleSource, isRefresh, allowToggleSource }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  quality?: LX.Quality\n  isRefresh: boolean\n  allowToggleSource: boolean\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<{\n  url: string\n  musicInfo: LX.Music.MusicInfoOnline\n  quality: LX.Quality\n  isFromCache: boolean\n}> => {\n  if (!await window.lx.apiInitPromise[0]) throw new Error('source init failed')\n  // console.log(musicInfo.source)\n  const targetQuality = quality ?? getPlayQuality(appSetting['player.playQuality'], musicInfo)\n\n  let reqPromise\n  try {\n    reqPromise = musicSdk[musicInfo.source].getMusicUrl(toOldMusicInfo(musicInfo), targetQuality).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n  return reqPromise.then(({ url, type }: { url: string, type: LX.Quality }) => {\n    return { musicInfo, url, quality: type, isFromCache: false }\n  }).catch(async(err: any) => {\n    console.log(err)\n    if (!allowToggleSource || err.message == requestMsg.tooManyRequests) throw err\n    onToggleSource()\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    return getOtherSource(musicInfo).then(otherSource => {\n      console.log('find otherSource', otherSource)\n      if (otherSource.length) {\n        return getOnlineOtherSourceMusicUrl({\n          musicInfos: [...otherSource],\n          onToggleSource,\n          quality,\n          isRefresh,\n          retryedSource: [musicInfo.source],\n        })\n      }\n      throw err\n    })\n  })\n}\n\n\nexport const getOnlineOtherSourcePicUrl = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {\n  musicInfos: LX.Music.MusicInfoOnline[]\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  isRefresh: boolean\n  retryedSource?: LX.OnlineSource[]\n}): Promise<{\n  url: string\n  musicInfo: LX.Music.MusicInfoOnline\n  isFromCache: boolean\n}> => {\n  let musicInfo: LX.Music.MusicInfoOnline | null = null\n  // eslint-disable-next-line no-cond-assign\n  while (musicInfo = (musicInfos.shift()!)) {\n    if (retryedSource.includes(musicInfo.source)) continue\n    retryedSource.push(musicInfo.source)\n    // if (!assertApiSupport(musicInfo.source)) continue\n    console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)\n    onToggleSource(musicInfo)\n    break\n  }\n  if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed'))\n\n  if (musicInfo.meta.picUrl && !isRefresh) return { musicInfo, url: musicInfo.meta.picUrl, isFromCache: true }\n\n  let reqPromise\n  try {\n    reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n  // retryedSource.includes(musicInfo.source)\n  return reqPromise.then((url: string) => {\n    return { musicInfo, url, isFromCache: false }\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n  }).catch((err: any) => {\n    console.log(err)\n    return getOnlineOtherSourcePicUrl({ musicInfos, onToggleSource, isRefresh, retryedSource })\n  })\n}\n\n/**\n * 获取在线歌曲封面\n */\nexport const handleGetOnlinePicUrl = async({ musicInfo, isRefresh, onToggleSource, allowToggleSource }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  isRefresh: boolean\n  allowToggleSource: boolean\n}): Promise<{\n  url: string\n  musicInfo: LX.Music.MusicInfoOnline\n  isFromCache: boolean\n}> => {\n  // console.log(musicInfo.source)\n  let reqPromise\n  try {\n    reqPromise = musicSdk[musicInfo.source].getPic(toOldMusicInfo(musicInfo))\n  } catch (err) {\n    reqPromise = Promise.reject(err)\n  }\n  return reqPromise.then((url: string) => {\n    return { musicInfo, url, isFromCache: false }\n  }).catch(async(err: any) => {\n    console.log(err)\n    if (!allowToggleSource) throw err\n    onToggleSource()\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    return getOtherSource(musicInfo).then(otherSource => {\n      console.log('find otherSource', otherSource)\n      if (otherSource.length) {\n        return getOnlineOtherSourcePicUrl({\n          musicInfos: [...otherSource],\n          onToggleSource,\n          isRefresh,\n          retryedSource: [musicInfo.source],\n        })\n      }\n      throw err\n    })\n  })\n}\n\n\nexport const getOnlineOtherSourceLyricInfo = async({ musicInfos, onToggleSource, isRefresh, retryedSource = [] }: {\n  musicInfos: LX.Music.MusicInfoOnline[]\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  isRefresh: boolean\n  retryedSource?: LX.OnlineSource[]\n}): Promise<{\n  lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo\n  musicInfo: LX.Music.MusicInfoOnline\n  isFromCache: boolean\n}> => {\n  let musicInfo: LX.Music.MusicInfoOnline | null = null\n  // eslint-disable-next-line no-cond-assign\n  while (musicInfo = (musicInfos.shift()!)) {\n    if (retryedSource.includes(musicInfo.source)) continue\n    retryedSource.push(musicInfo.source)\n    // if (!assertApiSupport(musicInfo.source)) continue\n    console.log('try toggle to: ', musicInfo.source, musicInfo.name, musicInfo.singer, musicInfo.interval)\n    onToggleSource(musicInfo)\n    break\n  }\n  if (!musicInfo) throw new Error(window.i18n.t('toggle_source_failed'))\n\n  if (!isRefresh) {\n    const lyricInfo = await getCachedLyricInfo(musicInfo)\n    if (lyricInfo) return { musicInfo, lyricInfo, isFromCache: true }\n  }\n\n  let reqPromise\n  try {\n    // TODO: remove any type\n    reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise\n  } catch (err: any) {\n    reqPromise = Promise.reject(err)\n  }\n  // retryedSource.includes(musicInfo.source)\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {\n    return existTimeExp.test(lyricInfo.lyric) ? {\n      lyricInfo,\n      musicInfo,\n      isFromCache: false,\n    } : Promise.reject(new Error('failed'))\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n  }).catch((err: any) => {\n    console.log(err)\n    return getOnlineOtherSourceLyricInfo({ musicInfos, onToggleSource, isRefresh, retryedSource })\n  })\n}\n\n/**\n * 获取在线歌词信息\n */\nexport const handleGetOnlineLyricInfo = async({ musicInfo, onToggleSource, isRefresh, allowToggleSource }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  onToggleSource: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  isRefresh: boolean\n  allowToggleSource: boolean\n}): Promise<{\n  musicInfo: LX.Music.MusicInfoOnline\n  lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo\n  isFromCache: boolean\n}> => {\n  // console.log(musicInfo.source)\n  let reqPromise\n  try {\n    // TODO: remove any type\n    reqPromise = (musicSdk[musicInfo.source].getLyric(toOldMusicInfo(musicInfo)) as any).promise\n  } catch (err) {\n    reqPromise = Promise.reject(err)\n  }\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return reqPromise.then((lyricInfo: LX.Music.LyricInfo) => {\n    return existTimeExp.test(lyricInfo.lyric) ? {\n      musicInfo,\n      lyricInfo,\n      isFromCache: false,\n    } : Promise.reject(new Error('failed'))\n  }).catch(async(err: any) => {\n    console.log(err)\n    if (!allowToggleSource) throw err\n\n    onToggleSource()\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    return getOtherSource(musicInfo).then(otherSource => {\n      console.log('find otherSource', otherSource)\n      if (otherSource.length) {\n        return getOnlineOtherSourceLyricInfo({\n          musicInfos: [...otherSource],\n          onToggleSource,\n          isRefresh,\n          retryedSource: [musicInfo.source],\n        })\n      }\n      throw err\n    })\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/player/action.ts",
    "content": "import { isEmpty, setPause, setPlay, setResource, setStop } from '@renderer/plugins/player'\nimport { isPlay, playedList, playInfo, playMusicInfo, tempPlayList, musicInfo as _musicInfo } from '@renderer/store/player/state'\nimport {\n  getList,\n  clearPlayedList,\n  clearTempPlayeList,\n  setPlayMusicInfo,\n  addPlayedList,\n  setMusicInfo,\n  setAllStatus,\n  removeTempPlayList,\n  setPlayListId,\n  removePlayedList,\n} from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { getMusicUrl, getPicPath, getLyricInfo } from '../music/index'\nimport { filterList } from './utils'\nimport { requestMsg } from '@renderer/utils/message'\nimport { getRandom } from '@renderer/utils/index'\nimport { addListMusics, removeListMusics } from '@renderer/store/list/action'\nimport { loveList } from '@renderer/store/list/state'\nimport { addDislikeInfo } from '@renderer/core/dislikeList'\n// import { checkMusicFileAvailable } from '@renderer/utils/music'\n\nlet gettingUrlId = ''\nconst createGettingUrlId = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem) => {\n  const tInfo = 'progress' in musicInfo ? musicInfo.metadata.musicInfo.meta.toggleMusicInfo : musicInfo.meta.toggleMusicInfo\n  return `${musicInfo.id}_${tInfo?.id ?? ''}`\n}\nconst createDelayNextTimeout = (delay: number) => {\n  let timeout: NodeJS.Timeout | null\n  const clearDelayNextTimeout = () => {\n    // console.log(this.timeout)\n    if (timeout) {\n      clearTimeout(timeout)\n      timeout = null\n    }\n  }\n\n  const addDelayNextTimeout = () => {\n    clearDelayNextTimeout()\n    timeout = setTimeout(() => {\n      timeout = null\n      if (window.lx.isPlayedStop) return\n      console.warn('delay next timeout timeout', delay)\n      void playNext(true)\n    }, delay)\n  }\n\n  return {\n    clearDelayNextTimeout,\n    addDelayNextTimeout,\n  }\n}\nconst { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)\nconst { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000)\n\n/**\n * 检查音乐信息是否已更改\n */\nconst diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {\n  // return curMusicInfo !== playMusicInfo.musicInfo || isPlay.value\n  return gettingUrlId != createGettingUrlId(curMusicInfo) || curMusicInfo.id != playMusicInfo.musicInfo?.id || isPlay.value\n}\n\nlet cancelDelayRetry: (() => void) | null = null\nconst delayRetry = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<string | null> => {\n  // if (cancelDelayRetry) cancelDelayRetry()\n  return new Promise<string | null>((resolve, reject) => {\n    const time = getRandom(2, 6)\n    setAllStatus(window.i18n.t('player__getting_url_delay_retry', { time }))\n    const tiemout = setTimeout(() => {\n      getMusicPlayUrl(musicInfo, isRefresh, true).then((result) => {\n        cancelDelayRetry = null\n        resolve(result)\n      }).catch(async(err: any) => {\n        cancelDelayRetry = null\n        reject(err)\n      })\n    }, time * 1000)\n    cancelDelayRetry = () => {\n      clearTimeout(tiemout)\n      cancelDelayRetry = null\n      resolve(null)\n    }\n  })\n}\nconst getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false, isRetryed = false): Promise<string | null> => {\n  // this.musicInfo.url = await getMusicPlayUrl(targetSong, type)\n  setAllStatus(window.i18n.t('player__getting_url'))\n  if (appSetting['player.autoSkipOnError']) addLoadTimeout()\n\n  // const type = getPlayType(appSetting['player.highQuality'], musicInfo)\n  let toggleMusicInfo = ('progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo).meta.toggleMusicInfo\n\n  return (toggleMusicInfo ? getMusicUrl({\n    musicInfo: toggleMusicInfo,\n    isRefresh,\n    allowToggleSource: false,\n  }) : Promise.reject(new Error('not found'))).catch(async() => {\n    return getMusicUrl({\n      musicInfo,\n      isRefresh,\n      onToggleSource(mInfo) {\n        if (diffCurrentMusicInfo(musicInfo)) return\n        setAllStatus(window.i18n.t('toggle_source_try'))\n      },\n    })\n  }).then(url => {\n    if (window.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null\n\n    return url\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  }).catch(err => {\n    // console.log('err', err.message)\n    if (window.lx.isPlayedStop ||\n      diffCurrentMusicInfo(musicInfo) ||\n      err.message == requestMsg.cancelRequest) return null\n\n    if (err.message == requestMsg.tooManyRequests) return delayRetry(musicInfo, isRefresh)\n\n    if (!isRetryed) return getMusicPlayUrl(musicInfo, isRefresh, true)\n\n    throw err\n  })\n}\n\nexport const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh?: boolean) => {\n  // if (appSetting['player.autoSkipOnError']) addLoadTimeout()\n  if (!diffCurrentMusicInfo(musicInfo)) return\n  if (cancelDelayRetry) cancelDelayRetry()\n  gettingUrlId = createGettingUrlId(musicInfo)\n  void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {\n    if (!url) return\n    setResource(url)\n  }).catch((err: any) => {\n    console.log(err)\n    setAllStatus(err.message)\n    window.app_event.error()\n    if (appSetting['player.autoSkipOnError']) addDelayNextTimeout()\n  }).finally(() => {\n    if (musicInfo === playMusicInfo.musicInfo) {\n      gettingUrlId = ''\n      clearLoadTimeout()\n    }\n  })\n}\n\n// 恢复上次播放的状态\nconst handleRestorePlay = async(restorePlayInfo: LX.Player.SavedPlayInfo) => {\n  const musicInfo = playMusicInfo.musicInfo\n  if (!musicInfo) return\n\n  setImmediate(() => {\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    window.app_event.setProgress(appSetting['player.isSavePlayTime'] ? restorePlayInfo.time : 0, restorePlayInfo.maxTime)\n    window.app_event.pause()\n  })\n\n\n  void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => {\n    if (musicInfo.id != playMusicInfo.musicInfo?.id || url == _musicInfo.pic) return\n    setMusicInfo({ pic: url })\n    window.app_event.picUpdated()\n  }).catch(_ => _)\n\n  void getLyricInfo({ musicInfo }).then((lyricInfo) => {\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    setMusicInfo({\n      lrc: lyricInfo.lyric,\n      tlrc: lyricInfo.tlyric,\n      lxlrc: lyricInfo.lxlyric,\n      rlrc: lyricInfo.rlyric,\n      rawlrc: lyricInfo.rawlrcInfo.lyric,\n    })\n    window.app_event.lyricUpdated()\n  }).catch((err) => {\n    console.log(err)\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    setAllStatus(window.i18n.t('lyric__load_error'))\n  })\n\n  if (appSetting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList({ ...playMusicInfo as LX.Player.PlayMusicInfo })\n}\n\n\n// 处理音乐播放\nconst handlePlay = () => {\n  window.lx.isPlayedStop &&= false\n\n  resetRandomNextMusicInfo()\n  if (window.lx.restorePlayInfo) {\n    void handleRestorePlay(window.lx.restorePlayInfo)\n    window.lx.restorePlayInfo = null\n    return\n  }\n  const musicInfo = playMusicInfo.musicInfo\n\n  if (!musicInfo) return\n\n  setStop()\n  window.app_event.pause()\n\n  clearDelayNextTimeout()\n  clearLoadTimeout()\n\n\n  if (appSetting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) })\n\n  setMusicUrl(musicInfo)\n\n  void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => {\n    if (musicInfo.id != playMusicInfo.musicInfo?.id || url == _musicInfo.pic) return\n    setMusicInfo({ pic: url })\n    window.app_event.picUpdated()\n  }).catch(_ => _)\n\n  void getLyricInfo({ musicInfo }).then((lyricInfo) => {\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    setMusicInfo({\n      lrc: lyricInfo.lyric,\n      tlrc: lyricInfo.tlyric,\n      lxlrc: lyricInfo.lxlyric,\n      rlrc: lyricInfo.rlyric,\n      rawlrc: lyricInfo.rawlrcInfo.lyric,\n    })\n    window.app_event.lyricUpdated()\n  }).catch((err) => {\n    console.log(err)\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    setAllStatus(window.i18n.t('lyric__load_error'))\n  })\n}\n\n/**\n * 播放列表内歌曲\n * @param listId 列表id\n * @param id 歌曲id\n */\nexport const playListById = (listId: string, id: string) => {\n  const prevListId = playInfo.playerListId\n  setPlayListId(listId)\n  // pause()\n  const musicInfo = getList(listId).find(m => m.id == id)\n  if (!musicInfo) return\n  setPlayMusicInfo(listId, musicInfo)\n  if (appSetting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList()\n  clearTempPlayeList()\n  handlePlay()\n}\n\n/**\n * 播放列表内歌曲\n * @param listId 列表id\n * @param index 播放的歌曲位置\n */\nexport const playList = (listId: string, index: number) => {\n  const prevListId = playInfo.playerListId\n  setPlayListId(listId)\n  // pause()\n  setPlayMusicInfo(listId, getList(listId)[index])\n  if (appSetting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList()\n  clearTempPlayeList()\n  handlePlay()\n}\n\nconst handleToggleStop = () => {\n  stop()\n  setTimeout(() => {\n    setPlayMusicInfo(null, null)\n  })\n}\n\nconst randomNextMusicInfo = {\n  info: null as LX.Player.PlayMusicInfo | null,\n  // index: -1,\n}\nexport const resetRandomNextMusicInfo = () => {\n  if (randomNextMusicInfo.info) {\n    randomNextMusicInfo.info = null\n    // randomNextMusicInfo.index = -1\n  }\n}\n\nexport const getNextPlayMusicInfo = async(): Promise<LX.Player.PlayMusicInfo | null> => {\n  if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲\n    const playMusicInfo = tempPlayList[0]\n    return playMusicInfo\n  }\n\n  if (playMusicInfo.musicInfo == null) return null\n\n  if (randomNextMusicInfo.info) return randomNextMusicInfo.info\n\n  // console.log(playInfo.playerListId)\n  const currentListId = playInfo.playerListId\n  if (!currentListId) return null\n  const currentList = getList(currentListId)\n\n  if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲\n    let currentId: string\n    if (playMusicInfo.isTempPlay) {\n      const musicInfo = currentList[playInfo.playerPlayIndex]\n      if (musicInfo) currentId = musicInfo.id\n    } else {\n      currentId = playMusicInfo.musicInfo.id\n    }\n    // 从已播放列表移除播放列表已删除的歌曲\n    let index\n    for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) {\n      const playMusicInfo = playedList[index]\n      const currentId = playMusicInfo.musicInfo.id\n      if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {\n        removePlayedList(index)\n        continue\n      }\n      break\n    }\n\n    if (index < playedList.length) return playedList[index]\n  }\n  // const isCheckFile = findNum > 2 // 针对下载列表，如果超过两次都碰到无效歌曲，则过滤整个列表内的无效歌曲\n  let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲\n    listId: currentListId,\n    list: currentList,\n    playedList,\n    playerMusicInfo: currentList[playInfo.playerPlayIndex],\n    isNext: true,\n  })\n\n  if (!filteredList.length) return null\n  // let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex])\n  if (playerIndex == -1 && filteredList.length) playerIndex = 0\n  let nextIndex = playerIndex\n\n  let togglePlayMethod = appSetting['player.togglePlayMethod']\n  switch (togglePlayMethod) {\n    case 'listLoop':\n      nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1\n      break\n    case 'random':\n      nextIndex = getRandom(0, filteredList.length)\n      break\n    case 'list':\n      nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1\n      break\n    case 'singleLoop':\n      break\n    default:\n      return null\n  }\n  if (nextIndex < 0) return null\n\n  const nextPlayMusicInfo = {\n    musicInfo: filteredList[nextIndex],\n    listId: currentListId,\n    isTempPlay: false,\n  }\n\n  if (togglePlayMethod == 'random') {\n    randomNextMusicInfo.info = nextPlayMusicInfo\n    // randomNextMusicInfo.index = nextIndex\n  }\n  return nextPlayMusicInfo\n}\n\nconst handlePlayNext = (playMusicInfo: LX.Player.PlayMusicInfo) => {\n  // pause()\n  setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)\n  handlePlay()\n}\n/**\n * 下一曲\n * @param isAutoToggle 是否自动切换\n * @returns\n */\nexport const playNext = async(isAutoToggle = false): Promise<void> => {\n  console.log('skip next', isAutoToggle)\n  if (tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲\n    const playMusicInfo = tempPlayList[0]\n    removeTempPlayList(0)\n    handlePlayNext(playMusicInfo)\n    console.log('play temp list')\n    return\n  }\n\n  if (playMusicInfo.musicInfo == null) {\n    handleToggleStop()\n    console.log('musicInfo empty')\n    return\n  }\n\n  // console.log(playInfo.playerListId)\n  const currentListId = playInfo.playerListId\n  if (!currentListId) {\n    handleToggleStop()\n    console.log('currentListId empty')\n    return\n  }\n  const currentList = getList(currentListId)\n\n  if (playedList.length) { // 移除已播放列表内不存在原列表的歌曲\n    let currentId: string\n    if (playMusicInfo.isTempPlay) {\n      const musicInfo = currentList[playInfo.playerPlayIndex]\n      if (musicInfo) currentId = musicInfo.id\n    } else {\n      currentId = playMusicInfo.musicInfo.id\n    }\n    // 从已播放列表移除播放列表已删除的歌曲\n    let index\n    for (index = playedList.findIndex(m => m.musicInfo.id === currentId) + 1; index < playedList.length; index++) {\n      const playMusicInfo = playedList[index]\n      const currentId = playMusicInfo.musicInfo.id\n      if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {\n        removePlayedList(index)\n        continue\n      }\n      break\n    }\n\n    if (index < playedList.length) {\n      handlePlayNext(playedList[index])\n      console.log('play played list')\n      return\n    }\n  }\n  if (randomNextMusicInfo.info) {\n    handlePlayNext(randomNextMusicInfo.info)\n    return\n  }\n  // const isCheckFile = findNum > 2 // 针对下载列表，如果超过两次都碰到无效歌曲，则过滤整个列表内的无效歌曲\n  let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲\n    listId: currentListId,\n    list: currentList,\n    playedList,\n    playerMusicInfo: currentList[playInfo.playerPlayIndex],\n    isNext: true,\n  })\n\n  if (!filteredList.length) {\n    handleToggleStop()\n    console.log('filtered list empty')\n    return\n  }\n  // let currentIndex: number = filteredList.indexOf(currentList[playInfo.playerPlayIndex])\n  if (playerIndex == -1 && filteredList.length) playerIndex = 0\n  let nextIndex = playerIndex\n\n  let togglePlayMethod = appSetting['player.togglePlayMethod']\n  if (!isAutoToggle) {\n    switch (togglePlayMethod) {\n      case 'list':\n      case 'singleLoop':\n      case 'none':\n        togglePlayMethod = 'listLoop'\n    }\n  }\n  switch (togglePlayMethod) {\n    case 'listLoop':\n      nextIndex = playerIndex === filteredList.length - 1 ? 0 : playerIndex + 1\n      break\n    case 'random':\n      nextIndex = getRandom(0, filteredList.length)\n      break\n    case 'list':\n      nextIndex = playerIndex === filteredList.length - 1 ? -1 : playerIndex + 1\n      break\n    case 'singleLoop':\n      break\n    default:\n      nextIndex = -1\n      console.log('stop toggle play', togglePlayMethod, isAutoToggle)\n      return\n  }\n  if (nextIndex < 0) {\n    console.log('next index empty')\n    return\n  }\n\n  handlePlayNext({\n    musicInfo: filteredList[nextIndex],\n    listId: currentListId,\n    isTempPlay: false,\n  })\n}\n\n/**\n * 上一曲\n */\nexport const playPrev = async(isAutoToggle = false): Promise<void> => {\n  if (playMusicInfo.musicInfo == null) {\n    handleToggleStop()\n    return\n  }\n\n  const currentListId = playInfo.playerListId\n  if (!currentListId) {\n    handleToggleStop()\n    return\n  }\n  const currentList = getList(currentListId)\n\n  if (playedList.length) {\n    let currentId: string\n    if (playMusicInfo.isTempPlay) {\n      const musicInfo = currentList[playInfo.playerPlayIndex]\n      if (musicInfo) currentId = musicInfo.id\n    } else {\n      currentId = playMusicInfo.musicInfo.id\n    }\n    // 从已播放列表移除播放列表已删除的歌曲\n    let index\n    for (index = playedList.findIndex(m => m.musicInfo.id === currentId) - 1; index > -1; index--) {\n      const playMusicInfo = playedList[index]\n      const currentId = playMusicInfo.musicInfo.id\n      if (playMusicInfo.listId == currentListId && !currentList.some(m => m.id === currentId)) {\n        removePlayedList(index)\n        continue\n      }\n      break\n    }\n\n    if (index > -1) {\n      handlePlayNext(playedList[index])\n      return\n    }\n  }\n\n  // const isCheckFile = findNum > 2\n  let { filteredList, playerIndex } = await filterList({ // 过滤已播放歌曲\n    listId: currentListId,\n    list: currentList,\n    playedList,\n    playerMusicInfo: currentList[playInfo.playerPlayIndex],\n    isNext: false,\n  })\n  if (!filteredList.length) {\n    handleToggleStop()\n    return\n  }\n\n  // let currentIndex = filteredList.indexOf(currentList[playInfo.playerPlayIndex])\n  if (playerIndex == -1 && filteredList.length) playerIndex = 0\n  let nextIndex = playerIndex\n  if (!playMusicInfo.isTempPlay) {\n    let togglePlayMethod = appSetting['player.togglePlayMethod']\n    if (!isAutoToggle) {\n      switch (togglePlayMethod) {\n        case 'list':\n        case 'singleLoop':\n        case 'none':\n          togglePlayMethod = 'listLoop'\n      }\n    }\n    switch (togglePlayMethod) {\n      case 'random':\n        nextIndex = getRandom(0, filteredList.length)\n        break\n      case 'listLoop':\n      case 'list':\n        nextIndex = playerIndex === 0 ? filteredList.length - 1 : playerIndex - 1\n        break\n      case 'singleLoop':\n        break\n      default:\n        nextIndex = -1\n        return\n    }\n    if (nextIndex < 0) return\n  }\n\n  handlePlayNext({\n    musicInfo: filteredList[nextIndex],\n    listId: currentListId,\n    isTempPlay: false,\n  })\n}\n\n/**\n * 恢复播放\n */\nexport const play = () => {\n  window.lx.isPlayedStop &&= false\n  if (playMusicInfo.musicInfo == null) return\n  if (isEmpty()) {\n    if (createGettingUrlId(playMusicInfo.musicInfo) != gettingUrlId) setMusicUrl(playMusicInfo.musicInfo)\n    return\n  }\n  setPlay()\n}\n\n/**\n * 暂停播放\n */\nexport const pause = () => {\n  setPause()\n}\n\n/**\n * 停止播放\n */\nexport const stop = () => {\n  setStop()\n  setTimeout(() => {\n    window.app_event.stop()\n  })\n}\n\n/**\n * 播放、暂停播放切换\n */\nexport const togglePlay = () => {\n  window.lx.isPlayedStop &&= false\n  if (isPlay.value) {\n    pause()\n  } else {\n    play()\n  }\n}\n\n/**\n * 收藏当前播放的歌曲\n */\nexport const collectMusic = () => {\n  if (!playMusicInfo.musicInfo) return\n  void addListMusics(loveList.id, ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo])\n}\n\n/**\n * 取消收藏当前播放的歌曲\n */\nexport const uncollectMusic = () => {\n  if (!playMusicInfo.musicInfo) return\n  void removeListMusics({ listId: loveList.id, ids: ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo.id : playMusicInfo.musicInfo.id] })\n}\n\n/**\n * 不喜欢当前播放的歌曲\n */\nexport const dislikeMusic = async() => {\n  if (!playMusicInfo.musicInfo) return\n  const minfo = 'progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo\n  await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }])\n  await playNext(true)\n}\n"
  },
  {
    "path": "src/renderer/core/player/index.ts",
    "content": "export * from './action'\nexport * from './timeoutStop'\n"
  },
  {
    "path": "src/renderer/core/player/timeoutStop.ts",
    "content": "import { ref, computed, type ComputedRef } from '@common/utils/vueTools'\nimport { isPlay } from '@renderer/store/player/state'\nimport { appSetting } from '@renderer/store/setting'\n// import { interval, intervalCancel } from '@renderer/utils/ipc'\nimport { pause } from './action'\n\nconst time = ref(-1)\n\n\nconst timeoutTools: {\n  isRunning: boolean\n  // time: number\n  interval: null | number\n  timeout: NodeJS.Timeout | null\n  endTime: number\n  exit: () => void\n  clearTimeout: () => void\n  start: (_time: number) => void\n} = {\n  isRunning: false,\n  timeout: null,\n  // time: -1,\n  endTime: 0,\n  interval: null,\n  exit() {\n    window.lx.isPlayedStop = true\n    if (!appSetting['player.waitPlayEndStop'] && isPlay.value) {\n      pause()\n    }\n  },\n  clearTimeout() {\n    if (this.interval) {\n      window.clearInterval(this.interval)\n      this.interval = null\n    }\n    if (this.timeout) {\n      clearTimeout(this.timeout)\n      this.timeout = null\n    }\n\n    if (!this.isRunning) return\n    // this.time = -1\n    time.value = -1\n    this.isRunning = false\n  },\n  start(_time: number) {\n    this.clearTimeout()\n    // this.time = _time\n    time.value = _time\n    this.isRunning = true\n    this.endTime = performance.now() + _time * 1000\n\n    this.interval = window.setInterval(() => {\n      // this.endTime = performance.now()\n      // if (this.time > 0) {\n      //   this.time--\n      // }\n      time.value = Math.max(0, Math.round((this.endTime - performance.now()) / 1000))\n    }, 1000)\n    this.timeout = setTimeout(() => {\n      this.timeout = null\n      time.value = -1\n      this.clearTimeout()\n      this.exit()\n    }, _time * 1000)\n  },\n}\n\nexport const startTimeoutStop = (time: number) => {\n  window.lx.isPlayedStop &&= false\n  timeoutTools.start(time)\n}\nexport const stopTimeoutStop = () => {\n  console.warn('stopTimeoutStop')\n  window.lx.isPlayedStop &&= false\n  timeoutTools.clearTimeout()\n}\n\nconst formatTime = (time: number): string => {\n  // let d = parseInt(time / 86400)\n  // d = d ? d.toString() + ':' : ''\n  // time = time % 86400\n  let h: number | string = Math.trunc(time / 3600)\n  h = h ? h.toString() + ':' : ''\n  time = time % 3600\n  const m = Math.trunc(time / 60).toString().padStart(2, '0')\n  const s = Math.trunc(time % 60).toString().padStart(2, '0')\n  return `${h}${m}:${s}`\n}\nexport const useTimeout = () => {\n  const timeLabel: ComputedRef<string> = computed(() => {\n    return time.value > 0 ? formatTime(time.value) : ''\n  })\n\n  return {\n    time,\n    timeLabel,\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/player/utils.ts",
    "content": "import { toRaw, markRawList } from '@common/utils/vueTools'\n// import { qualityList } from '@renderer/store'\nimport { clearPlayedList } from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { dislikeInfo } from '@renderer/store/dislikeList'\nimport { setPowerSaveBlocker as setPowerSaveBlockerRemote } from '@renderer/utils/ipc'\n\n// export const getPlayType = (highQuality: boolean, musicInfo: LX.Music.MusicInfo | LX.Download.ListItem): LX.Quality | null => {\n//   if ('progress' in musicInfo || musicInfo.source == 'local') return null\n//   let type: LX.Quality = '128k'\n//   let list = qualityList.value[musicInfo.source]\n//   if (highQuality && musicInfo.meta._qualitys['320k'] && list?.includes('320k')) type = '320k'\n//   return type\n// }\n\n/**\n * 过滤列表中已播放的歌曲\n */\nexport const filterList = async({ playedList, listId, list, playerMusicInfo, isNext }: {\n  playedList: LX.Player.PlayMusicInfo[]\n  listId: string\n  list: Array<LX.Music.MusicInfo | LX.Download.ListItem>\n  playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem\n  isNext: boolean\n}) => {\n  // if (this.list.listName === null) return\n  // console.log(isCheckFile)\n  let { filteredList, canPlayList, playerIndex } = await window.lx.worker.main.filterMusicList({\n    listId,\n    list: list.map(m => toRaw(m)),\n    playedList: toRaw(playedList),\n    // savePath: appSetting['download.savePath'],\n    playerMusicInfo: toRaw(playerMusicInfo),\n    dislikeInfo: { names: toRaw(dislikeInfo.names), musicNames: toRaw(dislikeInfo.musicNames), singerNames: toRaw(dislikeInfo.singerNames) },\n    isNext,\n  })\n\n  if (!filteredList.length && playedList.length) {\n    clearPlayedList()\n    return { filteredList: markRawList(canPlayList), playerIndex }\n  }\n  return { filteredList: markRawList(filteredList), playerIndex }\n}\n\nlet timeout: NodeJS.Timeout | null = null\nconst clearTimer = () => {\n  if (!timeout) return\n  clearTimeout(timeout)\n  timeout = null\n}\nexport const setPowerSaveBlocker = (enabled: boolean, force = false) => {\n  if (enabled) {\n    clearTimer()\n    if (!force && !appSetting['player.powerSaveBlocker']) return\n    setPowerSaveBlockerRemote(true)\n  } else if (force) {\n    clearTimer()\n    setPowerSaveBlockerRemote(false)\n  } else {\n    if (timeout) return\n    timeout = setTimeout(() => {\n      setPowerSaveBlockerRemote(false)\n    }, 60_000 * 1.5)\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/compositions/usePlaySonglist.ts",
    "content": "import { playList } from '@renderer/core/player'\nimport { setTempList } from '@renderer/store/list/action'\nimport { tempList, tempListMeta } from '@renderer/store/list/state'\nimport { getListDetail, getListDetailAll } from '@renderer/store/songList/action'\n\nconst getListPlayIndex = (list: LX.Music.MusicInfoOnline[], index?: number) => {\n  if (index == null) {\n    index = 1\n  } else {\n    if (index < 1) index = 1\n    else if (index > list.length) index = list.length\n  }\n  return index - 1\n}\n\nexport default () => {\n  const playSongListDetail = async(source: LX.OnlineSource, link: string, playIndex?: number) => {\n    // console.log(source, link, playIndex)\n    if (link == null) return\n    let isPlayingList = false\n    const id = decodeURIComponent(link)\n    const playListId = `${source}__${decodeURIComponent(link)}`\n    let list = (await getListDetail(id, source, 1)).list\n    if (playIndex == null || list.length > playIndex) {\n      isPlayingList = true\n      await setTempList(playListId, list)\n      playList(tempList.id, getListPlayIndex(list, playIndex))\n    }\n    list = await getListDetailAll(id, source)\n    if (isPlayingList) {\n      if (tempListMeta.id == id) await setTempList(playListId, list)\n    } else {\n      await setTempList(playListId, list)\n      playList(tempList.id, getListPlayIndex(list, playIndex))\n    }\n  }\n\n  return async(source: LX.OnlineSource, link: string, playIndex?: number) => {\n    try {\n      await playSongListDetail(source, link, playIndex)\n    } catch (err) {\n      console.error(err)\n      throw new Error('Get play list failed.')\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/index.ts",
    "content": "import { checkUpdate, getEnvParams, getViewPrevState, sendInited } from '@renderer/utils/ipc'\n\nimport { proxy, isFullscreen, themeId } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\n\nimport useSync from './useSync'\nimport useOpenAPI from './useOpenAPI'\nimport useStatusbarLyric from './useStatusbarLyric'\nimport useUpdate from './useUpdate'\nimport useDataInit from './useDataInit'\nimport useHandleEnvParams from './useHandleEnvParams'\nimport useEventListener from './useEventListener'\nimport useDeeplink from './useDeeplink'\nimport usePlayer from './usePlayer'\nimport useSettingSync from './useSettingSync'\nimport { useRouter } from '@common/utils/vueRouter'\nimport handleListAutoUpdate from './listAutoUpdate'\n\n\nexport default () => {\n  // apiSource.value = appSetting['common.apiSource']\n  proxy.enable = appSetting['network.proxy.enable']\n  proxy.host = appSetting['network.proxy.host']\n  proxy.port = appSetting['network.proxy.port']\n  isFullscreen.value = appSetting['common.startInFullscreen']\n  themeId.value = appSetting['theme.id']\n\n  const router = useRouter()\n  const initSyncService = useSync()\n  const initOpenAPI = useOpenAPI()\n  const initStatusbarLyric = useStatusbarLyric()\n  useEventListener()\n  const initPlayer = usePlayer()\n  const handleEnvParams = useHandleEnvParams()\n  const initData = useDataInit()\n  const initDeeplink = useDeeplink()\n  // const handleListAutoUpdate = useListAutoUpdate()\n\n  useUpdate()\n  useSettingSync()\n\n  void getEnvParams().then(envParams => {\n    // 移除代理相关的环境变量设置，防止请求库自动应用它们\n    // eslint-disable-next-line no-undef\n    // const processEnv = ENVIRONMENT\n    // for (const key of Object.keys(processEnv)) {\n    //   // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n    //   if (/^(?:http_proxy|https_proxy|NO_PROXY)$/i.test(key)) delete processEnv[key]\n    // }\n    const envProxy = envParams.cmdParams['proxy-server']\n    if (envProxy && typeof envProxy == 'string') {\n      const [host, port = ''] = envProxy.split(':')\n      proxy.envProxy = {\n        host,\n        port,\n      }\n    }\n\n    void getViewPrevState().then(state => {\n      void router.push({ path: state.url, query: state.query })\n    })\n\n    // 初始化我的列表、下载列表等数据\n    void initData().then(() => {\n      initPlayer()\n      handleEnvParams(envParams) // 处理传入的启动参数\n      void initDeeplink(envParams)\n      void initSyncService()\n      void initOpenAPI()\n      void initStatusbarLyric()\n      sendInited()\n\n      handleListAutoUpdate()\n      if (window.lx.isProd && appSetting['common.isAgreePact']) checkUpdate()\n    })\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/listAutoUpdate.ts",
    "content": "import { getListUpdateInfo } from '@renderer/utils/data'\nimport { userLists } from '@renderer/store/list/state'\nimport syncSourceList from '@renderer/store/list/syncSourceList'\n\nconst handleSyncSourceList = async(waitUpdateLists: LX.List.UserListInfo[]) => {\n  if (!waitUpdateLists.length) return\n  const targetListInfo = waitUpdateLists.shift()!\n  // console.log(targetListInfo)\n  try {\n    await syncSourceList(targetListInfo)\n  } catch {}\n  void handleSyncSourceList(waitUpdateLists)\n}\n\nexport default () => {\n  void getListUpdateInfo().then(listUpdateInfo => {\n    const waitUpdateLists = Object.entries(listUpdateInfo)\n      .map(([id, info]) => info.isAutoUpdate && userLists.find(l => l.id == id))\n      .filter(_ => _) as LX.List.UserListInfo[]\n    // for (let i = 2; i > 0; i--) {\n    //   void handleSyncSourceList(waitUpdateLists)\n    void handleSyncSourceList(waitUpdateLists)\n    // }\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDataInit.ts",
    "content": "import { getPlayInfo } from '@renderer/utils/ipc'\nimport music from '@renderer/utils/musicSdk'\nimport { log } from '@common/utils'\nimport { getListMusics, getUserLists, registerAction } from '@renderer/store/list/action'\n\n\nimport useInitUserApi from './useInitUserApi'\nimport { play, playList } from '@renderer/core/player'\nimport { onBeforeUnmount } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\nimport { playMusicInfo } from '@renderer/store/player/state'\nimport { initDislikeInfo, registerRemoteDislikeAction } from '@renderer/core/dislikeList'\n\nconst initPrevPlayInfo = async() => {\n  const info = await getPlayInfo()\n  window.lx.restorePlayInfo = null\n  if (!info?.listId || info.index < 0) return\n  const list = await getListMusics(info.listId)\n  if (!list[info.index]) return\n  window.lx.restorePlayInfo = info\n  playList(info.listId, info.index)\n\n  if (appSetting['player.startupAutoPlay']) {\n    const musicInfo = playMusicInfo.musicInfo\n    if (!musicInfo) return\n    setTimeout(() => {\n      if (musicInfo.id == playMusicInfo.musicInfo?.id) play()\n    })\n  }\n}\n\nexport default () => {\n  const initUserApi = useInitUserApi()\n\n  let unregister: null | (() => void) = null\n  let unregisterDislikeEvent: null | (() => void) = null\n\n  onBeforeUnmount(() => {\n    if (unregister) unregister()\n    if (unregisterDislikeEvent) unregisterDislikeEvent()\n  })\n\n  return async() => {\n    await Promise.all([\n      initUserApi(), // 自定义API\n    ]).catch(err => {\n      log.error(err)\n    })\n    void music.init() // 初始化音乐sdk\n    unregister = registerAction((ids) => {\n      window.app_event.myListUpdate(ids)\n    })\n    window.lxData.userLists = await getUserLists() // 获取用户列表\n    unregisterDislikeEvent = registerRemoteDislikeAction()\n    await initDislikeInfo() // 获取不喜欢列表\n    await initPrevPlayInfo().catch(err => {\n      log.error(err)\n    }) // 初始化上次的歌曲播放信息\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDeeplink/index.ts",
    "content": "import { onBeforeUnmount } from '@common/utils/vueTools'\nimport { clearEnvParamsDeeplink, focusWindow, onDeeplink } from '@renderer/utils/ipc'\n\nimport { useDialog } from './utils'\nimport useMusicAction from './useMusicAction'\nimport useSonglistAction from './useSonglistAction'\nimport usePlayerAction from './usePlayerAction'\n\nexport default () => {\n  let isInited = false\n\n  const showErrorDialog = useDialog()\n\n  const handleMusicAction = useMusicAction()\n  const handleSonglistAction = useSonglistAction()\n  const handlePlayerAction = usePlayerAction()\n\n\n  const handleLinkAction = async(link: string) => {\n    // console.log(link)\n    const [url, search] = link.split('?')\n    const [type, action, ...paths] = url.replace('lxmusic://', '').split('/')\n    const params: {\n      paths: string[]\n      data?: any\n      [key: string]: any\n    } = {\n      paths: [],\n    }\n    if (search) {\n      for (const param of search.split('&')) {\n        const [key, value] = param.split('=')\n        params[key] = value\n      }\n      if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))\n    }\n    params.paths = paths.map(p => decodeURIComponent(p))\n    console.log(params)\n    switch (type) {\n      case 'music':\n        await handleMusicAction(action, params)\n        break\n      case 'songlist':\n        await handleSonglistAction(action, params)\n        break\n      case 'player':\n        await handlePlayerAction(action as any)\n        break\n      default: throw new Error('Unknown type: ' + type)\n    }\n  }\n\n  const rDeeplink = onDeeplink(async({ params: link }) => {\n    console.log(link)\n    if (!isInited) return\n    clearEnvParamsDeeplink()\n    try {\n      await handleLinkAction(link)\n    } catch (err: any) {\n      showErrorDialog(err.message)\n      focusWindow()\n    }\n  })\n\n  onBeforeUnmount(() => {\n    rDeeplink()\n  })\n\n  return async(envParams: LX.EnvParams) => {\n    if (envParams.deeplink) {\n      clearEnvParamsDeeplink()\n      try {\n        await handleLinkAction(envParams.deeplink)\n      } catch (err: any) {\n        showErrorDialog(err.message)\n        focusWindow()\n      }\n    }\n    isInited = true\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDeeplink/useMusicAction.js",
    "content": "import { markRaw } from '@common/utils/vueTools'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { decodeName } from '@renderer/utils'\n// import { allList, defaultList, loveList, userLists } from '@renderer/store/list'\nimport { playMusicInfo, isShowPlayerDetail } from '@renderer/store/player/state'\nimport { setShowPlayerDetail, addTempPlayList } from '@renderer/store/player/action'\n\nimport { dataVerify, qualityFilter, sources } from './utils'\nimport { focusWindow } from '@renderer/utils/ipc'\nimport { playNext } from '@renderer/core/player/action'\nimport { toNewMusicInfo } from '@common/utils/tools'\nimport { LIST_IDS } from '@common/constants'\nimport { getOtherSource } from '@renderer/core/music/utils'\n\nconst useSearchMusic = () => {\n  const router = useRouter()\n\n  return ({ paths, data: params }) => {\n    let text\n    let source\n    if (params) {\n      text = dataVerify([\n        { key: 'keywords', types: ['string', 'number'], max: 128, required: true },\n      ], params).keywords\n      source = params.source\n    } else {\n      if (!paths.length) throw new Error('Keyword missing')\n\n      if (paths.length > 1) {\n        text = paths[1]\n        source = paths[0]\n      } else {\n        text = paths[0]\n      }\n\n      if (text.length > 128) text = text.substring(0, 128)\n    }\n\n    if (isShowPlayerDetail.value) setShowPlayerDetail(false)\n    const sourceList = [...sources, 'all']\n    source = sourceList.includes(source) ? source : null\n    setTimeout(() => {\n      router.replace({\n        path: '/search',\n        query: {\n          text,\n          source,\n        },\n      })\n    }, 500)\n    focusWindow()\n  }\n}\n\nconst usePlayMusic = () => {\n  const filterInfoByPlayMusic = musicInfo => {\n    switch (musicInfo.source) {\n      case 'kw':\n        musicInfo = dataVerify([\n          { key: 'name', types: ['string'], required: true, max: 200 },\n          { key: 'singer', types: ['string'], required: true, max: 200 },\n          { key: 'source', types: ['string'], required: true },\n          { key: 'songmid', types: ['string', 'number'], max: 64, required: true },\n          { key: 'img', types: ['string'], max: 1024 },\n          { key: 'albumId', types: ['string', 'number'], max: 64 },\n          { key: 'interval', types: ['string'], max: 64 },\n          { key: 'albumName', types: ['string'], max: 200 },\n          { key: 'types', types: ['object'], required: true },\n        ], musicInfo)\n        break\n      case 'kg':\n        musicInfo = dataVerify([\n          { key: 'name', types: ['string'], required: true, max: 200 },\n          { key: 'singer', types: ['string'], required: true, max: 200 },\n          { key: 'source', types: ['string'], required: true },\n          { key: 'songmid', types: ['string', 'number'], max: 64, required: true },\n          { key: 'img', types: ['string'], max: 1024 },\n          { key: 'albumId', types: ['string', 'number'], max: 64 },\n          { key: 'interval', types: ['string'], max: 64 },\n          { key: '_interval', types: ['number'], max: 64 },\n          { key: 'albumName', types: ['string'], max: 200 },\n          { key: 'types', types: ['object'], required: true },\n\n          { key: 'hash', types: ['string'], required: true, max: 64 },\n        ], musicInfo)\n        break\n      case 'tx':\n        musicInfo = dataVerify([\n          { key: 'name', types: ['string'], required: true, max: 200 },\n          { key: 'singer', types: ['string'], required: true, max: 200 },\n          { key: 'source', types: ['string'], required: true },\n          { key: 'songmid', types: ['string', 'number'], max: 64, required: true },\n          { key: 'img', types: ['string'], max: 1024 },\n          { key: 'albumId', types: ['string', 'number'], max: 64 },\n          { key: 'interval', types: ['string'], max: 64 },\n          { key: 'albumName', types: ['string'], max: 200 },\n          { key: 'types', types: ['object'], required: true },\n\n          { key: 'strMediaMid', types: ['string'], required: true, max: 64 },\n          { key: 'albumMid', types: ['string'], max: 64 },\n        ], musicInfo)\n        break\n      case 'wy':\n        musicInfo = dataVerify([\n          { key: 'name', types: ['string'], required: true, max: 200 },\n          { key: 'singer', types: ['string'], required: true, max: 200 },\n          { key: 'source', types: ['string'], required: true },\n          { key: 'songmid', types: ['string', 'number'], max: 64, required: true },\n          { key: 'img', types: ['string'], max: 1024 },\n          { key: 'albumId', types: ['string', 'number'], max: 64 },\n          { key: 'interval', types: ['string'], max: 64 },\n          { key: 'albumName', types: ['string'], max: 200 },\n          { key: 'types', types: ['object'], required: true },\n        ], musicInfo)\n        break\n      case 'mg':\n        musicInfo = dataVerify([\n          { key: 'name', types: ['string'], required: true, max: 200 },\n          { key: 'singer', types: ['string'], required: true, max: 200 },\n          { key: 'source', types: ['string'], required: true },\n          { key: 'songmid', types: ['string', 'number'], max: 64, required: true },\n          { key: 'img', types: ['string'], max: 1024 },\n          { key: 'albumId', types: ['string', 'number'], max: 64 },\n          { key: 'interval', types: ['string'], max: 64 },\n          { key: 'albumName', types: ['string'], max: 200 },\n          { key: 'types', types: ['object'], required: true },\n\n          { key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },\n          { key: 'lrcUrl', types: ['string'], max: 1024 },\n          { key: 'trcUrl', types: ['string'], max: 1024 },\n          { key: 'mrcUrl', types: ['string'], max: 1024 },\n        ], musicInfo)\n        break\n      default: throw new Error('Unknown source: ' + musicInfo.source)\n    }\n    musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)\n    return musicInfo\n  }\n\n  return ({ data: _musicInfo }) => {\n    _musicInfo = filterInfoByPlayMusic(_musicInfo)\n\n    let musicInfo = {\n      ..._musicInfo,\n      singer: decodeName(_musicInfo.singer),\n      name: decodeName(_musicInfo.name),\n      albumName: decodeName(_musicInfo.albumName),\n      otherSource: null,\n      _types: {},\n      typeUrl: {},\n    }\n    for (const type of musicInfo.types) {\n      musicInfo._types[type.type] = { size: type.size }\n    }\n    musicInfo = toNewMusicInfo(musicInfo)\n    markRaw(musicInfo)\n    const isPlaying = !!playMusicInfo.musicInfo\n    addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n    if (isPlaying) playNext()\n  }\n}\n\n\nconst useSearchPlayMusic = () => {\n  const verifyInfo = (info) => {\n    return dataVerify([\n      { key: 'name', types: ['string'], required: true, max: 200 },\n      { key: 'singer', types: ['string'], max: 200 },\n      { key: 'albumName', types: ['string'], max: 200 },\n      { key: 'interval', types: ['string'], max: 64 },\n      { key: 'playLater', types: ['boolean'] },\n    ], info)\n  }\n\n  const searchMusic = async(name, singer, albumName, interval) => {\n    return getOtherSource({\n      name,\n      singer,\n      interval,\n      meta: {\n        albumName,\n      },\n      source: 'local',\n      id: `sp_${name}_s${singer}_a${albumName}_i${interval ?? ''}`,\n    })\n  }\n  return async({ paths, data }) => {\n    // console.log(paths, data)\n    let info\n    if (paths.length) {\n      let name = paths[0].trim()\n      let singer = ''\n      if (name.includes('-')) [name, singer] = name.split('-').map(val => val.trim())\n      info = {\n        name,\n        singer,\n      }\n    } else info = data\n    info = verifyInfo(info)\n    if (!info.name) return\n    const musicList = await searchMusic(info.name, info.singer || '', info.albumName || '', info.interval || null)\n    if (musicList.length) {\n      console.log('find music:', musicList)\n      const musicInfo = musicList[0]\n      markRaw(musicInfo)\n      const isPlaying = !!playMusicInfo.musicInfo\n      if (info.playLater) {\n        addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo }])\n      } else {\n        addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n        if (isPlaying) playNext()\n      }\n    } else {\n      console.log('msuic not found:', info)\n    }\n  }\n}\n\nexport default () => {\n  const handleSearchMusic = useSearchMusic()\n  const handlePlayMusic = usePlayMusic()\n  const handleSearchPlayMusic = useSearchPlayMusic()\n\n\n  return async(action, info) => {\n    switch (action) {\n      case 'search':\n        handleSearchMusic(info)\n        break\n      case 'play':\n        handlePlayMusic(info)\n        break\n      case 'searchPlay':\n        await handleSearchPlayMusic(info)\n        break\n      default: throw new Error('Unknown action: ' + action)\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDeeplink/usePlayerAction.ts",
    "content": "import { collectMusic, dislikeMusic, pause, play, playNext, playPrev, togglePlay, uncollectMusic } from '@renderer/core/player'\n\ntype Action = 'play' | 'pause' | 'skipNext' | 'skipPrev' | 'togglePlay' | 'collect' | 'uncollect' | 'dislike'\n\nexport default () => {\n  return async(action: Action) => {\n    switch (action) {\n      case 'play':\n        play()\n        break\n      case 'pause':\n        pause()\n        break\n      case 'skipNext':\n        playNext()\n        break\n      case 'skipPrev':\n        playPrev()\n        break\n      case 'togglePlay':\n        togglePlay()\n        break\n      case 'collect':\n        collectMusic()\n        break\n      case 'uncollect':\n        uncollectMusic()\n        break\n      case 'dislike':\n        dislikeMusic()\n        break\n      default: throw new Error('Unknown action: ' + (action as any ?? ''))\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDeeplink/useSonglistAction.js",
    "content": "import { useRouter, useRoute } from '@common/utils/vueRouter'\nimport { isShowPlayerDetail } from '@renderer/store/player/state'\nimport { setShowPlayerDetail } from '@renderer/store/player/action'\nimport usePlaySonglist from '../compositions/usePlaySonglist'\nimport { focusWindow } from '@renderer/utils/ipc'\n\nimport { dataVerify, sourceVerify } from './utils'\n\nconst useOpenSonglist = () => {\n  const router = useRouter()\n  const route = useRoute()\n\n  const handleOpenSonglist = params => {\n    if (params.id) {\n      router[route.path == '/songList/detail' ? 'replace' : 'push']({\n        path: '/songList/detail',\n        query: {\n          source: params.source,\n          id: params.id,\n        },\n      })\n    } else if (params.url) {\n      router[route.path == '/songList/detail' ? 'replace' : 'push']({\n        path: '/songList/detail',\n        query: {\n          source: params.source,\n          id: params.url,\n        },\n      })\n    }\n  }\n\n  return ({ paths, data }) => {\n    let songlistInfo = {\n      source: null,\n      id: null,\n      url: null,\n    }\n    if (data) {\n      songlistInfo = data\n    } else {\n      songlistInfo.source = paths[0]\n      songlistInfo.url = paths[1]\n    }\n\n    sourceVerify(songlistInfo.source)\n\n    songlistInfo = dataVerify([\n      { key: 'source', types: ['string'] },\n      { key: 'id', types: ['string', 'number'], max: 64 },\n      { key: 'url', types: ['string'], max: 500 },\n    ], songlistInfo)\n\n    if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')\n    if (isShowPlayerDetail.value) setShowPlayerDetail(false)\n    handleOpenSonglist(songlistInfo)\n    focusWindow()\n  }\n}\nconst usePlaySonglistDetail = () => {\n  const playSongListDetail = usePlaySonglist()\n\n  return async({ paths, data }) => {\n    let songlistInfo = {\n      source: null,\n      id: null,\n      url: null,\n      index: null,\n    }\n    if (data) {\n      songlistInfo = data\n    } else {\n      songlistInfo.source = paths[0]\n      songlistInfo.url = paths[1]\n      songlistInfo.index = paths[2]\n      if (songlistInfo.index != null) {\n        songlistInfo.index = parseInt(songlistInfo.index)\n        if (Number.isNaN(songlistInfo.index)) delete songlistInfo.index\n      }\n    }\n\n    sourceVerify(songlistInfo.source)\n\n    songlistInfo = dataVerify([\n      { key: 'source', types: ['string'] },\n      { key: 'id', types: ['string', 'number'], max: 64 },\n      { key: 'url', types: ['string'], max: 500 },\n      { key: 'index', types: ['number'], max: 1000000 },\n    ], songlistInfo)\n\n    if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')\n\n    await playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index)\n  }\n}\n\nexport default () => {\n  const handleOpenSonglist = useOpenSonglist()\n  const handlePlaySonglist = usePlaySonglistDetail()\n\n\n  return async(action, info) => {\n    switch (action) {\n      case 'open':\n        handleOpenSonglist(info)\n        break\n      case 'play':\n        await handlePlaySonglist(info)\n        break\n      default: throw new Error('Unknown action: ' + action)\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useDeeplink/utils.js",
    "content": "import { useI18n } from '@renderer/plugins/i18n'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport const useDialog = () => {\n  const t = useI18n()\n  const errorDialog = message => {\n    dialog({\n      message: `${t('deep_link__handle_error_tip', { message })}`,\n      confirmButtonText: t('ok'),\n    })\n  }\n\n  return errorDialog\n}\n\nexport const sources = ['kw', 'kg', 'tx', 'wy', 'mg']\nexport const sourceVerify = source => {\n  if (!sources.includes(source)) throw new Error('Source no match')\n}\n\nexport const qualitys = ['128k', '320k', 'flac', 'flac24bit']\nexport const qualityFilter = (source, types) => {\n  types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => {\n    if (size != null && typeof size != 'string') throw new Error(type + ' size type no match')\n    if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match')\n    return hash == null ? { type, size } : { type, size, hash }\n  })\n  if (!types.length) throw new Error('quality no match')\n  return types\n}\n\nexport const dataVerify = (rules, data) => {\n  const newData = {}\n  for (const rule of rules) {\n    const val = data[rule.key]\n    if (rule.required && val == null) throw new Error(rule.key + ' missing')\n    if (val != null) {\n      if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match')\n      if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match')\n      if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match')\n    }\n    newData[rule.key] = val\n  }\n  return newData\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useEventListener.ts",
    "content": "import { getFontSizeWithScreen } from '@renderer/utils'\nimport {\n  minWindow,\n  onFocus,\n  onSettingChanged,\n  onThemeChange,\n  openDevTools,\n  quitApp,\n  setFullScreen,\n  showHideWindowToggle,\n} from '@renderer/utils/ipc'\nimport {\n  isFullscreen,\n  themeId,\n  themeShouldUseDarkColors,\n} from '@renderer/store'\nimport {\n  appSetting,\n  isShowAnimation,\n  mergeSetting,\n} from '@renderer/store/setting'\n\nimport {\n  onBeforeUnmount,\n  watch,\n} from '@common/utils/vueTools'\n// import { isLinux, isProd } from '@common/utils'\nimport { openUrl } from '@common/utils/electron'\nimport { HOTKEY_COMMON } from '@common/hotKey'\nimport { applyTheme, getThemes } from '@renderer/store/utils'\nimport { clearDownKeys } from '@renderer/event'\n\nconst handle_key_down = ({ event, type, key }: LX.KeyDownEevent) => {\n  // console.log(key)\n  if (key != 'escape' || !event || event.repeat || type == 'up' || window.lx.isEditingHotKey || (event.target as HTMLElement)?.classList.contains('ignore-esc') || event.lx_handled) return\n  if ((event.target as HTMLElement).tagName != 'INPUT') {\n    if (isFullscreen.value) {\n      event.lx_handled = true\n      void setFullScreen(false).then(fullscreen => {\n        isFullscreen.value = fullscreen\n      })\n    }\n    return\n  }\n  (event.target as HTMLInputElement).value = ''\n  ;(event.target as HTMLInputElement).blur()\n  event.lx_handled = true\n}\n\nconst handleBodyClick = (event: MouseEvent) => {\n  if ((event?.target as HTMLElement)?.tagName != 'A') return\n  if ((event?.target as HTMLAnchorElement).host == window.location.host) return\n  event.preventDefault()\n  if (/^https?:\\/\\//.test((event?.target as HTMLAnchorElement).href)) void openUrl((event?.target as HTMLAnchorElement).href)\n}\nconst handle_open_devtools = () => {\n  openDevTools()\n}\nconst handle_fullscreen = (event: LX.KeyDownEevent) => {\n  let fullscreen = !isFullscreen.value\n  if (typeof event == 'boolean') {\n    fullscreen = event\n  } else if (event.event?.repeat) return\n  void setFullScreen(fullscreen).then(fullscreen => {\n    isFullscreen.value = fullscreen\n  })\n}\nconst handle_selection = (event: LX.KeyDownEevent) => {\n  event.event?.preventDefault()\n}\n\nexport default () => {\n  watch(isFullscreen, val => {\n    if (val) {\n      document.documentElement.classList.remove(window.dt ? 'disableTransparent' : 'transparent')\n      document.documentElement.classList.add('fullscreen')\n      document.documentElement.style.fontSize = `${getFontSizeWithScreen(window.screen.width)}px`\n    } else {\n      document.documentElement.classList.remove('fullscreen')\n      document.documentElement.classList.add(window.dt ? 'disableTransparent' : 'transparent')\n      document.documentElement.style.fontSize = `${appSetting['common.fontSize']}px`\n    }\n  }, {\n    immediate: true,\n  })\n\n  watch(isShowAnimation, val => {\n    if (val) {\n      if (document.documentElement.classList.contains('disableAnimation')) {\n        document.documentElement.classList.remove('disableAnimation')\n      }\n    } else {\n      if (!document.documentElement.classList.contains('disableAnimation')) {\n        document.documentElement.classList.add('disableAnimation')\n      }\n    }\n  }, {\n    immediate: true,\n  })\n\n  const rSetConfig = onSettingChanged(({ params: setting }) => {\n    // console.log(config)\n    mergeSetting(setting)\n    window.app_event.configUpdate(setting)\n  })\n\n  const rFocus = onFocus(() => {\n    clearDownKeys()\n  })\n\n  const rThemeChange = onThemeChange(({ params: setting }) => {\n    // console.log(setting)\n    if (themeShouldUseDarkColors.value == setting.shouldUseDarkColors) {\n      if (themeId.value == setting.theme.id) return\n      themeId.value = setting.theme.id\n    } else {\n      themeShouldUseDarkColors.value = setting.shouldUseDarkColors\n      if (themeId.value != 'auto') return\n    }\n    getThemes(({ dataPath }) => {\n      applyTheme('auto', appSetting['theme.lightId'], appSetting['theme.darkId'], dataPath)\n    })\n  })\n\n  window.key_event.on(HOTKEY_COMMON.min.action, minWindow)\n  window.key_event.on(HOTKEY_COMMON.hide_toggle.action, showHideWindowToggle)\n  window.key_event.on(HOTKEY_COMMON.close.action, quitApp)\n\n  window.app_event.on('keyDown', handle_key_down)\n  window.key_event.on('key_mod+f12_down', handle_open_devtools)\n  window.key_event.on('key_f11_down', handle_fullscreen)\n  window.key_event.on('key_mod+a_down', handle_selection)\n  document.body.addEventListener('click', handleBodyClick, true)\n\n  onBeforeUnmount(() => {\n    window.key_event.off(HOTKEY_COMMON.min.action, minWindow)\n    window.key_event.off(HOTKEY_COMMON.hide_toggle.action, showHideWindowToggle)\n    window.key_event.off(HOTKEY_COMMON.close.action, quitApp)\n\n    window.app_event.off('keyDown', handle_key_down)\n    window.key_event.off('key_mod+f12_down', handle_open_devtools)\n    window.key_event.off('key_f11_down', handle_fullscreen)\n    window.key_event.off('key_mod+a_down', handle_selection)\n    document.body.removeEventListener('click', handleBodyClick)\n    rSetConfig()\n    rFocus()\n    rThemeChange()\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useHandleEnvParams.ts",
    "content": "import { useRouter } from '@common/utils/vueRouter'\nimport { parseUrlParams } from '@common/utils/common'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/state'\nimport { getListMusics } from '@renderer/store/list/action'\nimport usePlaySonglist from './compositions/usePlaySonglist'\nimport { playList } from '@renderer/core/player'\n\nconst getListPlayIndex = (list: LX.Music.MusicInfo[], indexStr?: string): number => {\n  let index: number\n  if (indexStr == null) {\n    index = 1\n  } else {\n    index = parseInt(indexStr)\n    if (Number.isNaN(index)) {\n      index = 1\n    } else {\n      if (index < 1) index = 1\n      else if (index > list.length) index = list.length\n    }\n  }\n  return index - 1\n}\n\nconst useInitEnvParamSearch = () => {\n  const router = useRouter()\n\n  return (search?: string) => {\n    if (search == null) return\n    setTimeout(() => {\n      void router.replace({\n        path: '/search',\n        query: {\n          text: search,\n        },\n      })\n    }, 1000)\n  }\n}\nconst useInitEnvParamPlay = () => {\n  // const setPlayList = useCommit('player', 'setList')\n\n  const playSongListDetail = usePlaySonglist()\n\n  return async(playStr?: string) => {\n    if (playStr == null || typeof playStr != 'string') return\n    // -play=\"source=kw&link=链接、ID\"\n    // -play=\"source=myList&name=名字\"\n    // -play=\"source=myList&name=名字&index=位置\"\n    const params = parseUrlParams(playStr)\n    if (params.type != 'songList') return\n    switch (params.source) {\n      case 'myList':\n        if (params.name != null) {\n          let targetList\n          const lists = [defaultList, loveList, ...userLists]\n          for (const list of lists) {\n            if (list.name === params.name) {\n              targetList = list\n              break\n            }\n          }\n          if (!targetList) return\n\n          playList(targetList.id, getListPlayIndex(await getListMusics(targetList.id), params.index))\n        }\n        break\n      case 'kw':\n      case 'kg':\n      case 'tx':\n      case 'mg':\n      case 'wy':\n        void playSongListDetail(params.source, params.link, parseInt(params.index))\n        break\n    }\n  }\n}\n\nexport default () => {\n  // 处理启动参数 search\n  const initEnvParamSearch = useInitEnvParamSearch()\n\n  // 处理启动参数 play\n  const initEnvParamPlay = useInitEnvParamPlay()\n\n  return (envParams: LX.EnvParams) => {\n    initEnvParamSearch(envParams.cmdParams.search)\n    void initEnvParamPlay(envParams.cmdParams.play)\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useInitUserApi.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { onUserApiStatus, getUserApiList, sendUserApiRequest as sendUserApiRequestRemote, userApiRequestCancel, onShowUserApiUpdateAlert } from '@renderer/utils/ipc'\nimport { openUrl } from '@common/utils/electron'\nimport { qualityList, userApi } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { setUserApi } from '@renderer/core/apiSource'\n\nconst sendUserApiRequest: typeof sendUserApiRequestRemote = async(data) => {\n  let stop: () => void\n  return new Promise<void>((resolve, reject) => {\n    stop = watch(() => appSetting['common.apiSource'], () => {\n      reject(new Error('source changed'))\n    })\n    void sendUserApiRequestRemote(data).then(resolve).catch(reject)\n  }).finally(() => {\n    stop()\n  })\n}\n\nexport default () => {\n  const t = useI18n()\n\n  const rUserApiStatus = onUserApiStatus(({ params: { status, message, apiInfo } }) => {\n    // console.log({ status, message, apiInfo })\n    userApi.status = status\n    userApi.message = message\n\n    if (!apiInfo || apiInfo.id !== appSetting['common.apiSource']) return\n    if (status) {\n      if (apiInfo.sources) {\n        let apis: any = {}\n        let qualitys: LX.QualityList = {}\n        for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(apiInfo.sources)) {\n          if (type != 'music') continue\n          apis[source as LX.Source] = {}\n          for (const action of actions) {\n            switch (action) {\n              case 'musicUrl':\n                apis[source].getMusicUrl = (songInfo: LX.Music.MusicInfo, type: LX.Quality) => {\n                  const requestKey = `request__${Math.random().toString().substring(2)}`\n                  return {\n                    canceleFn() {\n                      userApiRequestCancel(requestKey)\n                    },\n                    promise: sendUserApiRequest({\n                      requestKey,\n                      data: {\n                        source,\n                        action: 'musicUrl',\n                        info: {\n                          type,\n                          musicInfo: songInfo,\n                        },\n                      },\n                      // eslint-disable-next-line @typescript-eslint/promise-function-async\n                    }).then(res => {\n                      // console.log(res)\n                      return { type, url: res.data.url }\n                    }).catch(async err => {\n                      console.log(err.message)\n                      return Promise.reject(err)\n                    }),\n                  }\n                }\n                break\n              case 'lyric':\n                apis[source].getLyric = (songInfo: LX.Music.MusicInfo) => {\n                  const requestKey = `request__${Math.random().toString().substring(2)}`\n                  return {\n                    canceleFn() {\n                      userApiRequestCancel(requestKey)\n                    },\n                    promise: sendUserApiRequest({\n                      requestKey,\n                      data: {\n                        source,\n                        action: 'lyric',\n                        info: {\n                          type,\n                          musicInfo: songInfo,\n                        },\n                      },\n                      // eslint-disable-next-line @typescript-eslint/promise-function-async\n                    }).then(res => {\n                      // console.log(res)\n                      return res.data\n                    }).catch(async err => {\n                      console.log(err.message)\n                      return Promise.reject(err)\n                    }),\n                  }\n                }\n                break\n              case 'pic':\n                apis[source].getPic = (songInfo: LX.Music.MusicInfo) => {\n                  const requestKey = `request__${Math.random().toString().substring(2)}`\n                  return {\n                    canceleFn() {\n                      userApiRequestCancel(requestKey)\n                    },\n                    promise: sendUserApiRequest({\n                      requestKey,\n                      data: {\n                        source,\n                        action: 'pic',\n                        info: {\n                          type,\n                          musicInfo: songInfo,\n                        },\n                      },\n                      // eslint-disable-next-line @typescript-eslint/promise-function-async\n                    }).then(res => {\n                      // console.log(res)\n                      return res.data\n                    }).catch(async err => {\n                      console.log(err.message)\n                      return Promise.reject(err)\n                    }),\n                  }\n                }\n                break\n              default:\n                break\n            }\n          }\n          qualitys[source as LX.Source] = sourceQualitys\n        }\n        qualityList.value = qualitys\n        userApi.apis = apis\n      }\n    } else {\n      if (message) {\n        void dialog({\n          message: `${t('user_api__init_failed_alert', { name: apiInfo.name })}\\n${message}`,\n          selection: true,\n          confirmButtonText: t('ok'),\n        })\n      }\n    }\n    if (!window.lx.apiInitPromise[1]) window.lx.apiInitPromise[2](status)\n  })\n\n  const rUserApiShowUpdateAlert = onShowUserApiUpdateAlert(({ params: { name, log, updateUrl } }) => {\n    if (updateUrl) {\n      void dialog({\n        message: `${t('user_api__update_alert', { name })}\\n${log}`,\n        selection: true,\n        showCancel: true,\n        confirmButtonText: t('user_api__update_alert_open_url'),\n        cancelButtonText: t('close'),\n      }).then(confirm => {\n        if (!confirm) return\n        window.setTimeout(() => {\n          void openUrl(updateUrl)\n        }, 300)\n      })\n    } else {\n      void dialog({\n        message: `${t('user_api__update_alert', { name })}\\n${log}`,\n        selection: true,\n        confirmButtonText: t('ok'),\n      })\n    }\n  })\n\n  onBeforeUnmount(() => {\n    rUserApiStatus()\n    rUserApiShowUpdateAlert()\n  })\n\n  return async() => {\n    await setUserApi(appSetting['common.apiSource'])\n    void getUserApiList().then(list => {\n      // console.log(list)\n      // if (![...apiSourceInfo.map(s => s.id), ...list.map(s => s.id)].includes(appSetting['common.apiSource'])) {\n      //   console.warn('reset api')\n      //   let api = apiSourceInfo.find(api => !api.disabled)\n      //   if (api) apiSource.value = api.id\n      // }\n      userApi.list = list\n    }).catch(err => {\n      console.log(err)\n    })\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useOpenAPI.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport { appSetting } from '@renderer/store/setting'\nimport { sendOpenAPIAction } from '@renderer/utils/ipc'\nimport { openAPI } from '@renderer/store'\nimport { setDisableAutoPauseBySource } from '@renderer/core/lyric'\n\nexport default () => {\n  const handleEnable = async(enable: boolean, port: string, bindLan: boolean) => {\n    await sendOpenAPIAction({\n      action: 'enable',\n      data: {\n        enable,\n        port,\n        bindLan,\n      },\n    }).then((status) => {\n      openAPI.address = status.address\n      openAPI.message = status.message\n    }).catch((error) => {\n      openAPI.address = ''\n      openAPI.message = error.message\n    }).finally(() => {\n      setDisableAutoPauseBySource(!!openAPI.address, 'openAPI')\n    })\n  }\n  watch(() => appSetting['openAPI.enable'], enable => {\n    void handleEnable(enable, appSetting['openAPI.port'], appSetting['openAPI.bindLan'])\n  })\n\n  watch(() => appSetting['openAPI.port'], port => {\n    if (!appSetting['openAPI.enable']) return\n    void handleEnable(appSetting['openAPI.enable'], port, appSetting['openAPI.bindLan'])\n  })\n\n  watch(() => appSetting['openAPI.bindLan'], bindLan => {\n    if (!appSetting['openAPI.enable']) return\n    void handleEnable(appSetting['openAPI.enable'], appSetting['openAPI.port'], bindLan)\n  })\n\n  return async() => {\n    if (appSetting['openAPI.enable']) {\n      void handleEnable(true, appSetting['openAPI.port'], appSetting['openAPI.bindLan'])\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/index.ts",
    "content": "import {\n  createAudio,\n} from '@renderer/plugins/player'\nimport useMediaDevice from './useMediaDevice'\nimport usePlayerEvent from './usePlayerEvent'\nimport usePlayer from './usePlayer'\nimport usePlayStatus from './usePlayStatus'\n\nexport default () => {\n  createAudio()\n\n  usePlayerEvent()\n  useMediaDevice() // 初始化音频驱动输出设置\n  usePlayer()\n  const initPlayStatus = usePlayStatus()\n\n  return () => {\n    void initPlayStatus()\n  }\n}\n\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useLyric.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { debounce } from '@common/utils/common'\n// import { setDesktopLyricInfo, onGetDesktopLyricInfo } from '@renderer/utils/ipc'\n// import { musicInfo } from '@renderer/store/player/state'\nimport {\n  pause,\n  play,\n  setLyric,\n  stop,\n  init,\n  sendInfo,\n  setPlaybackRate,\n} from '@renderer/core/lyric'\nimport { appSetting } from '@renderer/store/setting'\n\nconst handleApplyPlaybackRate = debounce(setPlaybackRate, 300)\n\nexport default () => {\n  init()\n\n  const setPlayInfo = () => {\n    stop()\n    sendInfo()\n  }\n\n  watch(() => appSetting['player.isShowLyricTranslation'], setLyric)\n  watch(() => appSetting['player.isShowLyricRoma'], setLyric)\n  watch(() => appSetting['player.isSwapLyricTranslationAndRoma'], setLyric)\n  watch(() => appSetting['player.isPlayLxlrc'], setLyric)\n\n  window.app_event.on('play', play)\n  window.app_event.on('pause', pause)\n  window.app_event.on('stop', stop)\n  window.app_event.on('error', pause)\n  window.app_event.on('musicToggled', setPlayInfo)\n  window.app_event.on('lyricUpdated', setLyric)\n  window.app_event.on('setPlaybackRate', handleApplyPlaybackRate)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('play', play)\n    window.app_event.off('pause', pause)\n    window.app_event.off('stop', stop)\n    window.app_event.off('error', pause)\n    window.app_event.off('musicToggled', setPlayInfo)\n    window.app_event.off('lyricUpdated', setLyric)\n    window.app_event.off('setPlaybackRate', handleApplyPlaybackRate)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useMaxOutputChannelCount.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport { setMaxOutputChannelCount } from '@renderer/plugins/player'\n\nimport { appSetting } from '@renderer/store/setting'\n\nexport default () => {\n  // console.log(appSetting['player.soundEffect.panner.enable'])\n  setMaxOutputChannelCount(appSetting['player.isMaxOutputChannelCount'])\n\n  watch(() => appSetting['player.isMaxOutputChannelCount'], (val) => {\n    setMaxOutputChannelCount(val)\n  })\n}\n\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useMediaDevice.ts",
    "content": "import {\n  onBeforeUnmount,\n  watch,\n} from '@common/utils/vueTools'\nimport { pause } from '@renderer/core/player/action'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { setMediaDeviceId } from '@renderer/plugins/player'\nimport { isPlay } from '@renderer/store/player/state'\nimport { appSetting, saveMediaDeviceId } from '@renderer/store/setting'\n\nconst getDevices = async() => {\n  const devices = await navigator.mediaDevices.enumerateDevices()\n  return devices.filter(({ kind }) => kind == 'audiooutput')\n}\n\nlet isShowingTipAlert = false\n\nexport default () => {\n  let prevDeviceLabel: string | null = null\n  let prevDeviceId = ''\n\n  const getMediaDevice = async(deviceId: string) => {\n    const devices = await getDevices()\n    let device = devices.find(device => device.deviceId === deviceId)\n    if (!device) {\n      deviceId = 'default'\n      device = devices.find(device => device.deviceId === deviceId)\n    }\n\n    if (!device && !devices.length && !isShowingTipAlert) {\n      isShowingTipAlert = true\n      void dialog({\n        message: window.i18n.t('media_device__empty_device_tip'),\n        confirmButtonText: window.i18n.t('ok'),\n      }).finally(() => {\n        isShowingTipAlert = false\n      })\n    }\n    return device ? { label: device.label, deviceId: device.deviceId } : { label: '', deviceId: '' }\n  }\n  const setMediaDevice = async(deviceId: string, label: string) => {\n    prevDeviceLabel = label\n    // console.log(device)\n    setMediaDeviceId(deviceId).then(() => {\n      prevDeviceId = deviceId\n      saveMediaDeviceId(deviceId)\n    }).catch((err: any) => {\n      console.log(err)\n      setMediaDeviceId('default').finally(() => {\n        prevDeviceId = 'default'\n        saveMediaDeviceId('default')\n      })\n    })\n  }\n\n  const handleDeviceChange = (label: string) => {\n    // console.log(device)\n    // console.log(appSetting['player.isMediaDeviceRemovedStopPlay'], isPlay.value, label, prevDeviceLabel)\n    if (label != prevDeviceLabel) {\n      window.app_event.playerDeviceChanged()\n\n      if (appSetting['player.isMediaDeviceRemovedStopPlay'] && isPlay.value) {\n        window.lx.isPlayedStop = true\n        pause()\n      }\n    }\n  }\n\n  const handleMediaListChange = async() => {\n    const mediaDeviceId = appSetting['player.mediaDeviceId']\n    const device = await getMediaDevice(mediaDeviceId)\n\n    handleDeviceChange(device.label)\n\n    if (device.deviceId == mediaDeviceId) prevDeviceLabel = device.label\n    else void setMediaDevice(device.deviceId, device.label)\n  }\n\n  watch(() => appSetting['player.mediaDeviceId'], (id) => {\n    if (prevDeviceId == id) return\n    void getMediaDevice(id).then(async({ deviceId, label }) => setMediaDevice(deviceId, label))\n  })\n\n  void getMediaDevice(appSetting['player.mediaDeviceId']).then(async({ deviceId, label }) => setMediaDevice(deviceId, label))\n\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n  navigator.mediaDevices.addEventListener('devicechange', handleMediaListChange)\n\n  onBeforeUnmount(() => {\n    // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    navigator.mediaDevices.removeEventListener('devicechange', handleMediaListChange)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useMediaSessionInfo.ts",
    "content": "import { onBeforeUnmount } from '@common/utils/vueTools'\nimport { getDuration, getPlaybackRate, getCurrentTime } from '@renderer/plugins/player'\nimport { isPlay, musicInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { playProgress } from '@renderer/store/player/playProgress'\nimport { pause, play, playNext, playPrev, stop } from '@renderer/core/player'\n\nexport default () => {\n  // 创建一个空白音频以保持对 Media Session 的注册\n  const emptyAudio = new Audio()\n  emptyAudio.autoplay = false\n  emptyAudio.src = require('@renderer/assets/medias/Silence02s.mp3')\n  emptyAudio.controls = false\n  emptyAudio.preload = 'auto'\n  emptyAudio.onplaying = () => {\n    emptyAudio.pause()\n  }\n  void emptyAudio.play()\n  let prevPicUrl = ''\n\n  const updateMediaSessionInfo = () => {\n    if (musicInfo.id == null) {\n      navigator.mediaSession.metadata = null\n      return\n    }\n    const mediaMetadata: MediaMetadata = {\n      title: musicInfo.name,\n      artist: musicInfo.singer,\n      album: musicInfo.album,\n      artwork: [],\n    }\n    if (musicInfo.pic) {\n      const pic = new Image()\n      pic.src = prevPicUrl = musicInfo.pic\n      pic.onload = () => {\n        if (prevPicUrl == pic.src) {\n          mediaMetadata.artwork = [{ src: pic.src }]\n          // @ts-expect-error\n          navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)\n        }\n      }\n    } else prevPicUrl = ''\n\n    // @ts-expect-error\n    navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)\n  }\n\n  const updatePositionState = (state: {\n    duration?: number\n    position?: number\n    playbackRate?: number\n  } = {}) => {\n    navigator.mediaSession.setPositionState({\n      duration: state.duration ?? getDuration(),\n      playbackRate: state.playbackRate ?? getPlaybackRate(),\n      position: state.position ?? getCurrentTime(),\n    })\n  }\n\n  const setProgress = (time: number) => {\n    window.app_event.setProgress(time)\n  }\n\n  const setStop = () => {\n    stop()\n  }\n  const handlePlay = () => {\n    navigator.mediaSession.playbackState = 'playing'\n  }\n  const handlePause = () => {\n    navigator.mediaSession.playbackState = 'paused'\n  }\n  const handleStop = () => {\n    navigator.mediaSession.playbackState = 'none'\n  }\n  const handleSetPlayInfo = () => {\n    void emptyAudio.play().finally(() => {\n      updateMediaSessionInfo()\n      updatePositionState({\n        position: playProgress.nowPlayTime,\n        duration: playProgress.maxPlayTime,\n      })\n      handlePause()\n    })\n  }\n\n  // const registerMediaSessionHandler = () => {\n  navigator.mediaSession.setActionHandler('play', () => {\n    if (isPlay.value || !playMusicInfo) return\n    console.log('play')\n    play()\n  })\n  navigator.mediaSession.setActionHandler('pause', () => {\n    if (!isPlay.value || !playMusicInfo) return\n    console.log('pause')\n    pause()\n  })\n  navigator.mediaSession.setActionHandler('stop', () => {\n    console.log('stop')\n    setStop()\n  })\n  navigator.mediaSession.setActionHandler('seekbackward', details => {\n    console.log('seekbackward')\n    const seekOffset = details.seekOffset ?? 5\n    setProgress(Math.max(getCurrentTime() - seekOffset, 0))\n  })\n  navigator.mediaSession.setActionHandler('seekforward', details => {\n    console.log('seekforward')\n    const seekOffset = details.seekOffset ?? 5\n    setProgress(Math.min(getCurrentTime() + seekOffset, getDuration()))\n  })\n  navigator.mediaSession.setActionHandler('seekto', details => {\n    console.log('seekto', details.seekTime)\n    if (details.seekTime == null) return\n    let time = Math.min(details.seekTime, getDuration())\n    time = Math.max(time, 0)\n    setProgress(time)\n  })\n  navigator.mediaSession.setActionHandler('previoustrack', () => {\n    console.log('previoustrack')\n    void playPrev()\n  })\n  navigator.mediaSession.setActionHandler('nexttrack', () => {\n    console.log('nexttrack')\n    void playNext()\n  })\n  // navigator.mediaSession.setActionHandler('skipad', () => {\n  //   console.log('')\n  // })\n  // }\n\n  window.app_event.on('playerLoadeddata', updatePositionState)\n  window.app_event.on('playerPlaying', updatePositionState)\n  window.app_event.on('play', handlePlay)\n  window.app_event.on('pause', handlePause)\n  window.app_event.on('stop', handleStop)\n  window.app_event.on('error', handlePause)\n  window.app_event.on('playerEmptied', handleSetPlayInfo)\n  // window.app_event.on('playerLoadstart', handleSetPlayInfo)\n  window.app_event.on('musicToggled', handleSetPlayInfo)\n  window.app_event.on('picUpdated', updateMediaSessionInfo)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('playerLoadeddata', updatePositionState)\n    window.app_event.off('playerPlaying', updatePositionState)\n    window.app_event.off('play', handlePlay)\n    window.app_event.off('pause', handlePause)\n    window.app_event.off('stop', handleStop)\n    window.app_event.off('error', handlePause)\n    window.app_event.off('playerEmptied', handleSetPlayInfo)\n    // window.app_event.off('playerLoadstart', handleSetPlayInfo)\n    window.app_event.off('musicToggled', handleSetPlayInfo)\n    window.app_event.off('picUpdated', updateMediaSessionInfo)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlayEvent.ts",
    "content": "import { onBeforeUnmount } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { musicInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { setStop, isEmpty } from '@renderer/plugins/player'\nimport { playNext, setMusicUrl } from '@renderer/core/player'\nimport { setAllStatus } from '@renderer/store/player/action'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default () => {\n  const t = useI18n()\n  let retryNum = 0\n  let prevTimeoutId: string | null = null\n\n  let loadingTimeout: NodeJS.Timeout | null = null\n  let delayNextTimeout: NodeJS.Timeout | null = null\n  const startLoadingTimeout = () => {\n    // console.log('start load timeout')\n    clearLoadingTimeout()\n    loadingTimeout = setTimeout(() => {\n      if (window.lx.isPlayedStop) {\n        prevTimeoutId = null\n        setAllStatus('')\n        return\n      }\n\n      // 如果加载超时，则尝试刷新URL\n      if (prevTimeoutId == musicInfo.id) {\n        prevTimeoutId = null\n        void playNext(true)\n      } else {\n        prevTimeoutId = musicInfo.id\n        if (playMusicInfo.musicInfo) setMusicUrl(playMusicInfo.musicInfo, true)\n      }\n    }, 25000)\n  }\n  const clearLoadingTimeout = () => {\n    if (!loadingTimeout) return\n    // console.log('clear load timeout')\n    clearTimeout(loadingTimeout)\n    loadingTimeout = null\n  }\n\n  const clearDelayNextTimeout = () => {\n    // console.log(this.delayNextTimeout)\n    if (!delayNextTimeout) return\n    clearTimeout(delayNextTimeout)\n    delayNextTimeout = null\n  }\n  const addDelayNextTimeout = () => {\n    clearDelayNextTimeout()\n    delayNextTimeout = setTimeout(() => {\n      if (window.lx.isPlayedStop) {\n        setAllStatus('')\n        return\n      }\n      void playNext(true)\n    }, 5000)\n  }\n\n  const handleLoadstart = () => {\n    if (window.lx.isPlayedStop) return\n    if (appSetting['player.autoSkipOnError']) startLoadingTimeout()\n    setAllStatus(t('player__loading'))\n  }\n\n  const handleLoadeddata = () => {\n    setAllStatus(t('player__loading'))\n  }\n\n  const handlePlaying = () => {\n    setAllStatus('')\n    clearLoadingTimeout()\n  }\n\n  const handleEmpied = () => {\n    clearDelayNextTimeout()\n    clearLoadingTimeout()\n  }\n\n  const handleWating = () => {\n    setAllStatus(t('player__buffering'))\n  }\n\n  const handleError = (errCode?: number) => {\n    if (!musicInfo.id) return\n    clearLoadingTimeout()\n    if (window.lx.isPlayedStop) return\n    if (!isEmpty()) setStop()\n    if (playMusicInfo.musicInfo && errCode !== 1 && retryNum < 2) { // 若音频URL无效则尝试刷新2次URL\n      // console.log(this.retryNum)\n      retryNum++\n      setMusicUrl(playMusicInfo.musicInfo, true)\n      setAllStatus(t('player__refresh_url'))\n      return\n    }\n\n    if (appSetting['player.autoSkipOnError']) {\n      if (document.hidden) {\n        console.warn('error skip to next')\n        void playNext(true)\n      } else {\n        setAllStatus(t('player__error'))\n        setTimeout(addDelayNextTimeout)\n      }\n    }\n  }\n\n  const handleSetPlayInfo = () => {\n    retryNum = 0\n    prevTimeoutId = null\n    clearDelayNextTimeout()\n    clearLoadingTimeout()\n  }\n\n  // const handlePlayedStop = () => {\n  //   clearDelayNextTimeout()\n  //   clearLoadingTimeout()\n  // }\n\n\n  window.app_event.on('playerLoadstart', handleLoadstart)\n  window.app_event.on('playerLoadeddata', handleLoadeddata)\n  window.app_event.on('playerPlaying', handlePlaying)\n  window.app_event.on('playerWaiting', handleWating)\n  window.app_event.on('playerEmptied', handleEmpied)\n  window.app_event.on('playerError', handleError)\n  window.app_event.on('musicToggled', handleSetPlayInfo)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('playerLoadstart', handleLoadstart)\n    window.app_event.off('playerLoadeddata', handleLoadeddata)\n    window.app_event.off('playerPlaying', handlePlaying)\n    window.app_event.off('playerWaiting', handleWating)\n    window.app_event.off('playerEmptied', handleEmpied)\n    window.app_event.off('playerError', handleError)\n    window.app_event.off('musicToggled', handleSetPlayInfo)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlayProgress.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { formatPlayTime2, getRandom } from '@common/utils/common'\nimport { throttle } from '@common/utils'\nimport { savePlayInfo } from '@renderer/utils/ipc'\nimport { onTimeupdate, getCurrentTime, getDuration, setCurrentTime, onVisibilityChange } from '@renderer/plugins/player'\nimport { playProgress, setNowPlayTime, setMaxplayTime } from '@renderer/store/player/playProgress'\nimport { musicInfo, playMusicInfo, playInfo } from '@renderer/store/player/state'\n// import { getList } from '@renderer/store/utils'\nimport { appSetting } from '@renderer/store/setting'\nimport { playNext } from '@renderer/core/player'\nimport { updateListMusics } from '@renderer/store/list/action'\n\nconst delaySavePlayInfo = throttle(savePlayInfo, 2000)\n\nexport default () => {\n  let restorePlayTime = 0\n  const mediaBuffer: {\n    timeout: NodeJS.Timeout | null\n    playTime: number\n  } = {\n    timeout: null,\n    playTime: 0,\n  }\n\n  // const updateMusicInfo = useCommit('list', 'updateMusicInfo')\n\n  const startBuffering = () => {\n    console.log('start t')\n    if (mediaBuffer.timeout) return\n    mediaBuffer.timeout = setTimeout(() => {\n      mediaBuffer.timeout = null\n      if (window.lx.isPlayedStop) return\n      const currentTime = getCurrentTime()\n\n      mediaBuffer.playTime ||= currentTime\n      let skipTime = currentTime + getRandom(3, 6)\n      if (skipTime > playProgress.maxPlayTime) skipTime = (playProgress.maxPlayTime - currentTime) / 2\n      if (skipTime - mediaBuffer.playTime < 1 || playProgress.maxPlayTime - skipTime < 1) {\n        mediaBuffer.playTime = 0\n        if (appSetting['player.autoSkipOnError']) {\n          console.warn('buffering end')\n          void playNext(true)\n        }\n        return\n      }\n      startBuffering()\n      setCurrentTime(skipTime)\n      console.log(mediaBuffer.playTime)\n      console.log(currentTime)\n    }, 3000)\n  }\n  const clearBufferTimeout = () => {\n    console.log('clear t')\n    if (!mediaBuffer.timeout) return\n    clearTimeout(mediaBuffer.timeout)\n    mediaBuffer.timeout = null\n    mediaBuffer.playTime = 0\n  }\n\n  const setProgress = (time: number, maxTime?: number) => {\n    if (!musicInfo.id) return\n    if (maxTime != null) setMaxplayTime(maxTime)\n    console.log('setProgress', time, maxTime)\n    if (time > 0) restorePlayTime = time\n    if (mediaBuffer.playTime) {\n      clearBufferTimeout()\n      mediaBuffer.playTime = time\n      startBuffering()\n    }\n    setNowPlayTime(time)\n    setCurrentTime(time)\n\n    // if (!isPlay) audio.play()\n  }\n\n  const handlePause = () => {\n    clearBufferTimeout()\n  }\n\n  const handleStop = () => {\n    setNowPlayTime(0)\n    setMaxplayTime(0)\n  }\n\n  const handleError = () => {\n    restorePlayTime ||= getCurrentTime() // 记录出错的播放时间\n    console.log('handleError')\n  }\n\n  const handleLoadeddata = () => {\n    setMaxplayTime(getDuration())\n\n    if (playMusicInfo.musicInfo && 'source' in playMusicInfo.musicInfo && !playMusicInfo.musicInfo.interval) {\n      // console.log(formatPlayTime2(playProgress.maxPlayTime))\n\n      if (playMusicInfo.listId) {\n        void updateListMusics([{\n          id: playMusicInfo.listId,\n          musicInfo: {\n            ...playMusicInfo.musicInfo,\n            interval: formatPlayTime2(playProgress.maxPlayTime),\n          },\n        }])\n      }\n    }\n  }\n\n  const handlePlaying = () => {\n    console.log('handlePlaying', mediaBuffer.playTime, restorePlayTime)\n    clearBufferTimeout()\n    if (mediaBuffer.playTime) {\n      let playTime = mediaBuffer.playTime\n      mediaBuffer.playTime = 0\n      setCurrentTime(playTime)\n    } else if (restorePlayTime) {\n      setCurrentTime(restorePlayTime)\n      restorePlayTime = 0\n    }\n  }\n  const handleWating = () => {\n    startBuffering()\n  }\n\n  const handleEmpied = () => {\n    mediaBuffer.playTime = 0\n    clearBufferTimeout()\n  }\n\n  const handleSetPlayInfo = () => {\n    // restorePlayTime = playProgress.nowPlayTime\n    setCurrentTime(restorePlayTime = playProgress.nowPlayTime)\n    // setMaxplayTime(playProgress.maxPlayTime)\n    handlePause()\n    if (!playMusicInfo.isTempPlay && playMusicInfo.listId) {\n      delaySavePlayInfo({\n        time: playProgress.nowPlayTime,\n        maxTime: playProgress.maxPlayTime,\n        listId: playMusicInfo.listId,\n        index: playInfo.playIndex,\n      })\n    }\n  }\n\n  watch(() => playProgress.nowPlayTime, (newValue, oldValue) => {\n    if (Math.abs(newValue - oldValue) > 2) window.app_event.activePlayProgressTransition()\n    if (appSetting['player.isSavePlayTime'] && !playMusicInfo.isTempPlay) {\n      delaySavePlayInfo({\n        time: newValue,\n        maxTime: playProgress.maxPlayTime,\n        listId: playMusicInfo.listId as string,\n        index: playInfo.playIndex,\n      })\n    }\n  })\n  watch(() => playProgress.maxPlayTime, maxPlayTime => {\n    if (!playMusicInfo.isTempPlay) {\n      delaySavePlayInfo({\n        time: playProgress.nowPlayTime,\n        maxTime: maxPlayTime,\n        listId: playMusicInfo.listId as string,\n        index: playInfo.playIndex,\n      })\n    }\n  })\n\n  // window.app_event.on('play', handlePlay)\n  window.app_event.on('pause', handlePause)\n  window.app_event.on('stop', handleStop)\n  window.app_event.on('error', handleError)\n  window.app_event.on('setProgress', setProgress)\n  // window.app_event.on(eventPlayerNames.restorePlay, handleRestorePlay)\n  window.app_event.on('playerLoadeddata', handleLoadeddata)\n  window.app_event.on('playerPlaying', handlePlaying)\n  window.app_event.on('playerWaiting', handleWating)\n  window.app_event.on('playerEmptied', handleEmpied)\n  window.app_event.on('musicToggled', handleSetPlayInfo)\n\n  const rOnTimeupdate = onTimeupdate(() => {\n    setNowPlayTime(getCurrentTime())\n  })\n\n  let currentPlayTime = 0\n  const rVisibilityChange = onVisibilityChange(() => {\n    if (document.hidden) {\n      currentPlayTime = playProgress.nowPlayTime\n    } else {\n      if (Math.abs(playProgress.nowPlayTime - currentPlayTime) > 2) {\n        window.app_event.activePlayProgressTransition()\n      }\n    }\n  })\n\n  onBeforeUnmount(() => {\n    rOnTimeupdate()\n    rVisibilityChange()\n    // window.app_event.off('play', handlePlay)\n    window.app_event.off('pause', handlePause)\n    window.app_event.off('stop', handleStop)\n    window.app_event.off('error', handleError)\n    window.app_event.off('setProgress', setProgress)\n    // window.app_event.off(eventPlayerNames.restorePlay, handleRestorePlay)\n    window.app_event.off('playerLoadeddata', handleLoadeddata)\n    window.app_event.off('playerPlaying', handlePlaying)\n    window.app_event.off('playerWaiting', handleWating)\n    window.app_event.off('playerEmptied', handleEmpied)\n    window.app_event.off('musicToggled', handleSetPlayInfo)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlayStatus.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { sendPlayerStatus, onPlayerAction } from '@renderer/utils/ipc'\n// import store from '@renderer/store'\n\nimport { loveList } from '@renderer/store/list/state'\nimport { addListMusics, removeListMusics, checkListExistMusic } from '@renderer/store/list/action'\nimport { playMusicInfo, musicInfo } from '@renderer/store/player/state'\nimport { throttle } from '@common/utils'\nimport { pause, play, playNext, playPrev } from '@renderer/core/player'\nimport { playProgress } from '@renderer/store/player/playProgress'\nimport { appSetting } from '@renderer/store/setting'\nimport { lyric } from '@renderer/store/player/lyric'\n\nexport default () => {\n  // const setVisibleDesktopLyric = useCommit('setVisibleDesktopLyric')\n  // const setLockDesktopLyric = useCommit('setLockDesktopLyric')\n  let collect = false\n\n  const updateCollectStatus = async() => {\n    let status = !!playMusicInfo.musicInfo && await checkListExistMusic(loveList.id, playMusicInfo.musicInfo.id)\n    if (collect == status) return false\n    collect = status\n    return true\n  }\n\n  const handlePlay = () => {\n    sendPlayerStatus({ status: 'playing' })\n  }\n  const handlePause = () => {\n    sendPlayerStatus({ status: 'paused' })\n  }\n  const handleStop = () => {\n    if (playMusicInfo.musicInfo != null) return\n    sendPlayerStatus({ status: 'stoped' })\n  }\n  const handleError = () => {\n    sendPlayerStatus({ status: 'error' })\n  }\n  const handleSetPlayInfo = async() => {\n    await updateCollectStatus()\n    sendPlayerStatus({\n      collect,\n      name: musicInfo.name,\n      singer: musicInfo.singer,\n      albumName: musicInfo.album,\n      picUrl: musicInfo.pic ?? '',\n      lyric: musicInfo.lrc ?? '',\n      lyricLineText: '',\n      lyricLineAllText: '',\n    })\n  }\n  const handleSetLyric = () => {\n    sendPlayerStatus({\n      lyric: musicInfo.lrc ?? '',\n      tlyric: musicInfo.tlrc ?? '',\n      rlyric: musicInfo.rlrc ?? '',\n      lxlyric: musicInfo.lxlrc ?? '',\n      lyricLineText: '',\n      lyricLineAllText: '',\n    })\n  }\n  const handleSetPic = () => {\n    sendPlayerStatus({\n      picUrl: musicInfo.pic ?? '',\n    })\n  }\n  const handleSetLyricLine = (text: string, line: number) => {\n    let curLine = lyric.lines[line]?.extendedLyrics.join('\\n') ?? ''\n    sendPlayerStatus({\n      lyricLineText: text,\n      lyricLineAllText: curLine ? text + '\\n' + curLine : text,\n    })\n  }\n  // const handleSetTaskbarThumbnailClip = (clip) => {\n  //   setTaskbarThumbnailClip(clip)\n  // }\n  const throttleListChange = throttle(async listIds => {\n    if (!listIds.includes(loveList.id)) return\n    if (await updateCollectStatus()) sendPlayerStatus({ collect })\n  })\n  // const updateSetting = () => {\n  //   const setting = store.getters.setting\n  //   buttons.lrc = setting.desktopLyric.enable\n  //   buttons.lockLrc = setting.desktopLyric.isLock\n  //   setButtons()\n  // }\n  const rTaskbarThumbarClick = onPlayerAction(async({ params: { action, data } }) => {\n    switch (action) {\n      case 'play':\n        play()\n        break\n      case 'pause':\n        pause()\n        break\n      case 'prev':\n        void playPrev()\n        break\n      case 'next':\n        void playNext()\n        break\n      case 'collect':\n        if (!playMusicInfo.musicInfo) return\n        void addListMusics(loveList.id, ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo : playMusicInfo.musicInfo])\n        if (await updateCollectStatus()) sendPlayerStatus({ collect })\n        break\n      case 'unCollect':\n        if (!playMusicInfo.musicInfo) return\n        void removeListMusics({ listId: loveList.id, ids: ['progress' in playMusicInfo.musicInfo ? playMusicInfo.musicInfo.metadata.musicInfo.id : playMusicInfo.musicInfo.id] })\n        if (await updateCollectStatus()) sendPlayerStatus({ collect })\n        break\n      case 'seek': {\n        let progress = data as number\n        if (progress < 0) progress = 0\n        else if (progress > playProgress.maxPlayTime) progress = playProgress.maxPlayTime\n        window.app_event.setProgress(progress)\n        break\n      }\n      case 'mute':\n        window.app_event.setVolumeIsMute(data as boolean)\n        break\n      case 'volume':\n        window.app_event.setVolume(data as number)\n        break\n      // case 'lrc':\n      //   setVisibleDesktopLyric(true)\n      //   updateSetting()\n      //   break\n      // case 'unLrc':\n      //   setVisibleDesktopLyric(false)\n      //   updateSetting()\n      //   break\n      // case 'lockLrc':\n      //   setLockDesktopLyric(true)\n      //   updateSetting()\n      //   break\n      // case 'unlockLrc':\n      //   setLockDesktopLyric(false)\n      //   updateSetting()\n      //   break\n    }\n  })\n  watch(() => playProgress.nowPlayTime, (newValue, oldValue) => {\n    // console.log(playProgress.nowPlayTime, newValue, oldValue)\n    // if (newValue.toFixed(2) === oldValue.toFixed(2)) return\n    // console.log(playProgress.nowPlayTime)\n    sendPlayerStatus({ progress: newValue })\n  })\n  watch(() => playProgress.maxPlayTime, (newValue) => {\n    sendPlayerStatus({ duration: newValue })\n  })\n  watch(() => appSetting['player.playbackRate'], rate => {\n    sendPlayerStatus({ playbackRate: rate })\n  })\n\n  window.app_event.on('play', handlePlay)\n  window.app_event.on('pause', handlePause)\n  window.app_event.on('stop', handleStop)\n  window.app_event.on('error', handleError)\n  window.app_event.on('musicToggled', handleSetPlayInfo)\n  window.app_event.on('lyricUpdated', handleSetLyric)\n  window.app_event.on('picUpdated', handleSetPic)\n  window.app_event.on('lyricLinePlay', handleSetLyricLine)\n  // window.app_event.on(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)\n  window.app_event.on('myListUpdate', throttleListChange)\n\n  onBeforeUnmount(() => {\n    rTaskbarThumbarClick()\n    window.app_event.off('play', handlePlay)\n    window.app_event.off('pause', handlePause)\n    window.app_event.off('stop', handleStop)\n    window.app_event.off('error', handleError)\n    window.app_event.off('musicToggled', handleSetPlayInfo)\n    window.app_event.off('lyricUpdated', handleSetLyric)\n    window.app_event.off('picUpdated', handleSetPic)\n    window.app_event.off('lyricLinePlay', handleSetLyricLine)\n    // window.app_event.off(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)\n    window.app_event.off('myListUpdate', throttleListChange)\n  })\n\n  return async() => {\n    // const setting = store.getters.setting\n    // buttons.lrc = setting.desktopLyric.enable\n    // buttons.lockLrc = setting.desktopLyric.isLock\n    await updateCollectStatus()\n    if (playMusicInfo.musicInfo == null) return\n    sendPlayerStatus({\n      collect,\n      name: musicInfo.name,\n      singer: musicInfo.singer,\n      albumName: musicInfo.album,\n      playbackRate: appSetting['player.playbackRate'],\n      picUrl: musicInfo.pic ?? '',\n      lyric: musicInfo.lrc ?? '',\n      tlyric: musicInfo.tlrc ?? '',\n      rlyric: musicInfo.rlrc ?? '',\n      lxlyric: musicInfo.lxlrc ?? '',\n    })\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlaybackRate.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { setPlaybackRate as setPlayerPlaybackRate, setPreservesPitch } from '@renderer/plugins/player'\n\nimport { debounce } from '@common/utils'\n// import { HOTKEY_PLAYER } from '@common/hotKey'\nimport { playbackRate, setPlaybackRate } from '@renderer/store/player/playbackRate'\nimport { appSetting, savePlaybackRate } from '@renderer/store/setting'\n\nexport default () => {\n  const handleSavePlaybackRate = debounce(savePlaybackRate, 300)\n\n  setPlaybackRate(appSetting['player.playbackRate'])\n  setPlayerPlaybackRate(appSetting['player.playbackRate'])\n  setPreservesPitch(appSetting['player.preservesPitch'])\n\n\n  const handleSetPlaybackRate = (num: number) => {\n    const rate = num < 0.5 ? 0.5 : num > 2 ? 2 : num\n    setPlaybackRate(rate)\n  }\n\n  // const handleSetPlaybackRateUp = (step = 0.02) => {\n  //   handleSetPlaybackRate(volume.value + step)\n  // }\n  // const handleSetPlaybackRateDown = (step = 0.02) => {\n  //   handleSetPlaybackRate(volume.value - step)\n  // }\n\n  // const hotkeyVolumeUp = () => {\n  //   handleSetPlaybackRateUp()\n  // }\n  // const hotkeyVolumeDown = () => {\n  //   handleSetPlaybackRateDown()\n  // }\n\n  watch(playbackRate, rate => {\n    handleSavePlaybackRate(rate)\n    setPlayerPlaybackRate(rate)\n  })\n  watch(() => appSetting['player.playbackRate'], rate => {\n    setPlaybackRate(rate)\n  })\n\n\n  watch(() => appSetting['player.preservesPitch'], preservesPitch => {\n    setPreservesPitch(preservesPitch)\n  })\n\n\n  // window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n  // window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n  window.app_event.on('setPlaybackRate', handleSetPlaybackRate)\n\n  onBeforeUnmount(() => {\n    // window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n    // window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n    window.app_event.off('setPlaybackRate', handleSetPlaybackRate)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlayer.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { setTitle } from '@renderer/utils'\n\nimport {\n  getCurrentTime,\n  getDuration,\n  setPause, setStop,\n} from '@renderer/plugins/player'\n\nimport useMediaSessionInfo from './useMediaSessionInfo'\nimport usePlayProgress from './usePlayProgress'\nimport usePlayEvent from './usePlayEvent'\n\nimport {\n  musicInfo,\n  playMusicInfo,\n  playedList,\n} from '@renderer/store/player/state'\nimport {\n  setPlay,\n  setAllStatus,\n  addPlayedList,\n  clearPlayedList,\n  // resetPlayerMusicInfo,\n} from '@renderer/store/player/action'\n\nimport { appSetting } from '@renderer/store/setting'\n\nimport useLyric from './useLyric'\nimport useVolume from './useVolume'\nimport useWatchList from './useWatchList'\nimport { HOTKEY_PLAYER } from '@common/hotKey'\nimport { playNext, pause, playPrev, togglePlay, collectMusic, uncollectMusic, dislikeMusic } from '@renderer/core/player'\nimport usePlaybackRate from './usePlaybackRate'\nimport useSoundEffect from './useSoundEffect'\nimport useMaxOutputChannelCount from './useMaxOutputChannelCount'\nimport { setPowerSaveBlocker } from '@renderer/core/player/utils'\nimport usePreloadNextMusic from './usePreloadNextMusic'\n\n\nexport default () => {\n  const t = useI18n()\n\n  usePlayProgress()\n  useMediaSessionInfo()\n  usePlayEvent()\n  useLyric()\n  useVolume()\n  useMaxOutputChannelCount()\n  useSoundEffect()\n  usePlaybackRate()\n  useWatchList()\n  usePreloadNextMusic()\n\n  const handlePlayNext = () => {\n    void playNext()\n  }\n  const handlePlayPrev = () => {\n    void playPrev()\n  }\n\n  const addPowerSaveBlocker = () => {\n    setPowerSaveBlocker(true)\n  }\n  const removePowerSaveBlocker = () => {\n    setPowerSaveBlocker(false)\n  }\n\n  const setPlayStatus = () => {\n    setPlay(true)\n  }\n  const setPauseStatus = () => {\n    setPlay(false)\n    if (window.lx.isPlayedStop) pause()\n    removePowerSaveBlocker()\n  }\n\n  const handleUpdatePlayInfo = () => {\n    setTitle(musicInfo.id ? `${musicInfo.name} - ${musicInfo.singer}` : null)\n  }\n\n  const handleCanplay = () => {\n    if (window.lx.isPlayedStop) {\n      setPause()\n    }\n  }\n  const handleEnded = () => {\n    // setTimeout(() => {\n    setAllStatus(t('player__end'))\n    if (window.lx.isPlayedStop) {\n      console.log('played stop')\n      return\n    }\n    // resetPlayerMusicInfo()\n    // window.app_event.stop()\n    void playNext(true)\n    // })\n  }\n\n  const setProgress = (time: number) => {\n    window.app_event.setProgress(time)\n  }\n  const handleSeekforward = () => {\n    const seekOffset = 5\n    const curTime = getCurrentTime()\n    const time = Math.min(getCurrentTime() + seekOffset, getDuration())\n    if (Math.trunc(curTime) == Math.trunc(time)) return\n    setProgress(time)\n  }\n  const handleSeekbackward = () => {\n    const seekOffset = 5\n    const curTime = getCurrentTime()\n    const time = Math.max(getCurrentTime() - seekOffset, 0)\n    if (Math.trunc(curTime) == Math.trunc(time)) return\n    setProgress(time)\n  }\n\n  const setStopStatus = () => {\n    setPlay(false)\n    setTitle(null)\n    setAllStatus('')\n    setStop()\n    removePowerSaveBlocker()\n  }\n\n  watch(() => appSetting['player.togglePlayMethod'], newValue => {\n    // setLoopPlay(newValue == 'singleLoop')\n    if (playedList.length) clearPlayedList()\n    if (newValue == 'random' && playMusicInfo.musicInfo && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) })\n  })\n\n  // setLoopPlay(appSetting['player.togglePlayMethod'] == 'singleLoop')\n\n\n  window.key_event.on(HOTKEY_PLAYER.next.action, handlePlayNext)\n  window.key_event.on(HOTKEY_PLAYER.prev.action, handlePlayPrev)\n  window.key_event.on(HOTKEY_PLAYER.toggle_play.action, togglePlay)\n  window.key_event.on(HOTKEY_PLAYER.music_love.action, collectMusic)\n  window.key_event.on(HOTKEY_PLAYER.music_unlove.action, uncollectMusic)\n  window.key_event.on(HOTKEY_PLAYER.music_dislike.action, dislikeMusic)\n  window.key_event.on(HOTKEY_PLAYER.seekbackward.action, handleSeekbackward)\n  window.key_event.on(HOTKEY_PLAYER.seekforward.action, handleSeekforward)\n\n  window.app_event.on('play', setPlayStatus)\n  window.app_event.on('pause', setPauseStatus)\n  window.app_event.on('error', setPauseStatus)\n  window.app_event.on('stop', setStopStatus)\n  window.app_event.on('musicToggled', handleUpdatePlayInfo)\n  window.app_event.on('playerCanplay', handleCanplay)\n  window.app_event.on('playerPlaying', addPowerSaveBlocker)\n  window.app_event.on('playerEmptied', removePowerSaveBlocker)\n\n  window.app_event.on('playerEnded', handleEnded)\n\n\n  onBeforeUnmount(() => {\n  // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    window.key_event.off(HOTKEY_PLAYER.next.action, handlePlayNext)\n    // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    window.key_event.off(HOTKEY_PLAYER.prev.action, handlePlayPrev)\n    window.key_event.off(HOTKEY_PLAYER.toggle_play.action, togglePlay)\n    window.key_event.off(HOTKEY_PLAYER.music_love.action, collectMusic)\n    window.key_event.off(HOTKEY_PLAYER.music_unlove.action, uncollectMusic)\n    window.key_event.off(HOTKEY_PLAYER.music_dislike.action, dislikeMusic)\n    window.key_event.off(HOTKEY_PLAYER.seekbackward.action, handleSeekbackward)\n    window.key_event.off(HOTKEY_PLAYER.seekforward.action, handleSeekforward)\n\n\n    window.app_event.off('play', setPlayStatus)\n    window.app_event.off('pause', setPauseStatus)\n    window.app_event.off('error', setPauseStatus)\n    window.app_event.off('stop', setStopStatus)\n    window.app_event.off('musicToggled', handleUpdatePlayInfo)\n    window.app_event.off('playerPlaying', addPowerSaveBlocker)\n    window.app_event.off('playerEmptied', removePowerSaveBlocker)\n    window.app_event.off('playerCanplay', handleCanplay)\n\n    window.app_event.off('playerEnded', handleEnded)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePlayerEvent.ts",
    "content": "import { onBeforeUnmount } from '@common/utils/vueTools'\nimport {\n  onPlaying,\n  onPause,\n  onEnded,\n  onError,\n  onLoadeddata,\n  onLoadstart,\n  onCanplay,\n  onEmptied,\n  onWaiting,\n  getErrorCode,\n} from '@renderer/plugins/player'\n\n\nexport default () => {\n  const rOnPlaying = onPlaying(() => {\n    console.log('onPlaying')\n    window.app_event.playerPlaying()\n    window.app_event.play()\n  })\n  const rOnPause = onPause(() => {\n    console.log('onPause')\n    window.app_event.playerPause()\n    window.app_event.pause()\n  })\n  const rOnEnded = onEnded(() => {\n    console.log('onEnded')\n    window.app_event.playerEnded()\n    // window.app_event.pause()\n  })\n  const rOnError = onError(() => {\n    console.log('onError')\n    const errorCode = getErrorCode()\n    window.app_event.error(errorCode)\n    window.app_event.playerError(errorCode)\n  })\n  const rOnLoadeddata = onLoadeddata(() => {\n    console.log('onLoadeddata')\n    window.app_event.playerLoadeddata()\n  })\n  const rOnLoadstart = onLoadstart(() => {\n    console.log('onLoadstart')\n    window.app_event.playerLoadstart()\n  })\n  const rOnCanplay = onCanplay(() => {\n    console.log('onCanplay')\n    window.app_event.playerCanplay()\n  })\n  const rOnEmptied = onEmptied(() => {\n    console.log('onEmptied')\n    window.app_event.playerEmptied()\n    // window.app_event.stop()\n  })\n  const rOnWaiting = onWaiting(() => {\n    console.log('onWaiting')\n    window.app_event.pause()\n    window.app_event.playerWaiting()\n  })\n\n\n  onBeforeUnmount(() => {\n    rOnPlaying()\n    rOnPause()\n    rOnEnded()\n    rOnError()\n    rOnLoadeddata()\n    rOnLoadstart()\n    rOnCanplay()\n    rOnEmptied()\n    rOnWaiting()\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/usePreloadNextMusic.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { onTimeupdate, getCurrentTime } from '@renderer/plugins/player'\nimport { playProgress } from '@renderer/store/player/playProgress'\nimport { musicInfo } from '@renderer/store/player/state'\n// import { getList } from '@renderer/store/utils'\nimport { getNextPlayMusicInfo, resetRandomNextMusicInfo } from '@renderer/core/player'\nimport { getMusicUrl } from '@renderer/core/music'\nimport { appSetting } from '@renderer/store/setting'\n\nlet audio: HTMLAudioElement\nconst initAudio = () => {\n  if (audio) return\n  audio = new Audio()\n  audio.controls = false\n  audio.preload = 'auto'\n  audio.crossOrigin = 'anonymous'\n  audio.muted = true\n  audio.volume = 0\n  audio.autoplay = true\n  audio.addEventListener('playing', () => {\n    audio.pause()\n  })\n}\nconst checkMusicUrl = async(url: string): Promise<boolean> => {\n  initAudio()\n  return new Promise((resolve) => {\n    const clear = () => {\n      audio.removeEventListener('error', handleErr)\n      audio.removeEventListener('canplay', handlePlay)\n    }\n    const handleErr = () => {\n      clear()\n      if (audio?.error?.code !== 1) {\n        resolve(false)\n      } else {\n        resolve(true)\n      }\n    }\n    const handlePlay = () => {\n      clear()\n      resolve(true)\n    }\n    audio.addEventListener('error', handleErr)\n    audio.addEventListener('canplay', handlePlay)\n    audio.src = url\n  })\n}\n\nconst preloadMusicInfo = {\n  isLoading: false,\n  preProgress: 0,\n  info: null as LX.Player.PlayMusicInfo | null,\n}\nconst resetPreloadInfo = () => {\n  preloadMusicInfo.preProgress = 0\n  preloadMusicInfo.info = null\n  preloadMusicInfo.isLoading = false\n}\nconst preloadNextMusicUrl = async(curTime: number) => {\n  if (preloadMusicInfo.isLoading || curTime - preloadMusicInfo.preProgress < 3) return\n  preloadMusicInfo.isLoading = true\n  console.log('preload next music url')\n  const info = await getNextPlayMusicInfo()\n  if (info) {\n    preloadMusicInfo.info = info\n    const url = await getMusicUrl({ musicInfo: info.musicInfo }).catch(() => '')\n    if (url) {\n      console.log('preload url', url)\n      const result = await checkMusicUrl(url)\n      if (!result) {\n        const url = await getMusicUrl({ musicInfo: info.musicInfo, isRefresh: true }).catch(() => '')\n        void checkMusicUrl(url)\n        console.log('preload url refresh', url)\n      }\n    }\n  }\n  preloadMusicInfo.isLoading = false\n}\n\nexport default () => {\n  const setProgress = (time: number) => {\n    if (!musicInfo.id) return\n    preloadMusicInfo.preProgress = time\n  }\n\n  const handleSetPlayInfo = () => {\n    resetPreloadInfo()\n  }\n\n  watch(() => appSetting['player.togglePlayMethod'], () => {\n    if (!preloadMusicInfo.info || preloadMusicInfo.info.isTempPlay) return\n    resetRandomNextMusicInfo()\n    preloadMusicInfo.info = null\n    preloadMusicInfo.preProgress = playProgress.nowPlayTime\n  })\n\n  window.app_event.on('setProgress', setProgress)\n  window.app_event.on('musicToggled', handleSetPlayInfo)\n\n  const rOnTimeupdate = onTimeupdate(() => {\n    const time = getCurrentTime()\n    const duration = playProgress.maxPlayTime\n    if (duration > 10 && duration - time < 10 && !preloadMusicInfo.info) {\n      void preloadNextMusicUrl(time)\n    }\n  })\n\n\n  onBeforeUnmount(() => {\n    rOnTimeupdate()\n    window.app_event.off('setProgress', setProgress)\n    window.app_event.off('musicToggled', handleSetPlayInfo)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useSoundEffect.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport {\n  freqs,\n  getAudioContext,\n  getBiquadFilter,\n  setConvolver,\n  setPannerSoundR,\n  setPannerSpeed,\n  startPanner,\n  stopPanner,\n  setConvolverMainGain,\n  setConvolverSendGain,\n  setPitchShifter,\n} from '@renderer/plugins/player'\n\nimport { appSetting } from '@renderer/store/setting'\n\nconst cache = new Map<string, AudioBuffer>()\nconst loadBuffer = async(name: string) => new Promise<AudioBuffer>((resolve, reject) => {\n  // eslint-disable-next-line @typescript-eslint/no-var-requires\n  const path = require('@renderer/assets/medias/filters/' + name) as string\n  if (cache.has(path)) {\n    resolve(cache.get(path)!)\n    return\n  }\n  // Load buffer asynchronously\n  let request = new XMLHttpRequest()\n  request.open('GET', path, true)\n  request.responseType = 'arraybuffer'\n\n  request.onload = function() {\n    // Asynchronously decode the audio file data in request.response\n    void getAudioContext().decodeAudioData(request.response, (buffer) => {\n      if (!buffer) {\n        reject(new Error('error decoding file data: ' + path))\n        return\n      }\n      cache.set(path, buffer)\n      resolve(buffer)\n    },\n    function(error) {\n      reject(error)\n      console.error('decodeAudioData error', error)\n    })\n  }\n\n  request.onerror = function() {\n    reject(new Error('XHR error'))\n  }\n\n  request.send()\n})\n\nexport default () => {\n  // console.log(appSetting['player.soundEffect.panner.enable'])\n  if (appSetting['player.soundEffect.panner.enable']) startPanner()\n  setPannerSoundR(appSetting['player.soundEffect.panner.soundR'] / 10)\n  setPannerSpeed(2 * (appSetting['player.soundEffect.panner.speed'] / 10))\n  if (freqs.some(v => appSetting[`player.soundEffect.biquadFilter.hz${v}`] != 0)) {\n    const bfs = getBiquadFilter()\n    for (const item of freqs) {\n      bfs.get(`hz${item}`)!.gain.value = appSetting[`player.soundEffect.biquadFilter.hz${item}`]\n    }\n  }\n  if (appSetting['player.soundEffect.convolution.fileName']) {\n    void loadBuffer(appSetting['player.soundEffect.convolution.fileName']).then((buffer) => {\n      setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)\n    })\n  }\n  if (appSetting['player.soundEffect.pitchShifter.playbackRate'] != 1) {\n    setPitchShifter(appSetting['player.soundEffect.pitchShifter.playbackRate'])\n  }\n\n\n  watch(() => appSetting['player.soundEffect.panner.enable'], (enable) => {\n    if (enable) {\n      startPanner()\n    } else {\n      stopPanner()\n    }\n  })\n  watch(() => appSetting['player.soundEffect.panner.soundR'], (soundR) => {\n    setPannerSoundR(soundR / 10)\n  })\n  watch(() => appSetting['player.soundEffect.panner.speed'], (speed) => {\n    setPannerSpeed(2 * (speed / 10))\n  })\n  watch(() => appSetting['player.soundEffect.convolution.fileName'], (fileName) => {\n    setTimeout(() => {\n      if (fileName) {\n        void loadBuffer(fileName).then((buffer) => {\n          setConvolver(buffer, appSetting['player.soundEffect.convolution.mainGain'] / 10, appSetting['player.soundEffect.convolution.sendGain'] / 10)\n        })\n      } else {\n        setConvolver(null, 0, 0)\n      }\n    })\n  })\n  watch(() => appSetting['player.soundEffect.convolution.mainGain'], (mainGain) => {\n    if (!appSetting['player.soundEffect.convolution.fileName']) return\n    setConvolverMainGain(mainGain / 10)\n  })\n  watch(() => appSetting['player.soundEffect.convolution.sendGain'], (sendGain) => {\n    if (!appSetting['player.soundEffect.convolution.fileName']) return\n    setConvolverSendGain(sendGain / 10)\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz31'], (hz31) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz31')!.gain.value = hz31\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz62'], (hz62) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz62')!.gain.value = hz62\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz125'], (hz125) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz125')!.gain.value = hz125\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz250'], (hz250) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz250')!.gain.value = hz250\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz500'], (hz500) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz500')!.gain.value = hz500\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz1000'], (hz1000) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz1000')!.gain.value = hz1000\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz2000'], (hz2000) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz2000')!.gain.value = hz2000\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz4000'], (hz4000) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz4000')!.gain.value = hz4000\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz8000'], (hz8000) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz8000')!.gain.value = hz8000\n  })\n  watch(() => appSetting['player.soundEffect.biquadFilter.hz16000'], (hz16000) => {\n    const bfs = getBiquadFilter()\n    bfs.get('hz16000')!.gain.value = hz16000\n  })\n\n  watch(() => appSetting['player.soundEffect.pitchShifter.playbackRate'], (playbackRate) => {\n    setPitchShifter(playbackRate)\n  })\n\n\n  // window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n  // window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n  // window.app_event.on('setPlaybackRate', handleSetPlaybackRate)\n\n  // onBeforeUnmount(() => {\n  //   // window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n  //   // window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n  //   window.app_event.off('setPlaybackRate', handleSetPlaybackRate)\n  // })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useVolume.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { setVolume as setPlayerVolume, setMute as setPlayerMute } from '@renderer/plugins/player'\n\nimport { debounce } from '@common/utils'\nimport { HOTKEY_PLAYER } from '@common/hotKey'\n// import { player as eventPlayerNames } from '@renderer/event/names'\nimport { volume, isMute, setMute, setVolume } from '@renderer/store/player/volume'\nimport { appSetting, saveVolume, saveVolumeIsMute } from '@renderer/store/setting'\n\nexport default () => {\n  const handleSaveVolume = debounce(saveVolume, 300)\n\n  setVolume(appSetting['player.volume'])\n  setMute(appSetting['player.isMute'])\n  setPlayerVolume(appSetting['player.volume'])\n  setPlayerMute(appSetting['player.isMute'])\n\n  const handleToggleVolumeMute = (_isMute?: boolean) => {\n    let muteStatus = _isMute ?? !isMute.value\n    saveVolumeIsMute(muteStatus)\n    setMute(muteStatus)\n  }\n\n  const handleSetVolume = (num: number) => {\n    const _volume = num < 0 ? 0 : num > 1 ? 1 : num\n    setVolume(_volume)\n  }\n\n  const handleSetVolumeUp = (step = 0.04) => {\n    handleSetVolume(volume.value + step)\n  }\n  const handleSetVolumeDown = (step = 0.04) => {\n    handleSetVolume(volume.value - step)\n  }\n\n  const hotkeyVolumeUp = () => {\n    handleSetVolumeUp()\n  }\n  const hotkeyVolumeDown = () => {\n    handleSetVolumeDown()\n  }\n  const hotkeyVolumeMute = () => {\n    handleToggleVolumeMute()\n  }\n\n  watch(volume, _volume => {\n    handleSaveVolume(_volume)\n    setPlayerVolume(_volume)\n  })\n  watch(isMute, mute => {\n    saveVolumeIsMute(mute)\n    setPlayerMute(mute)\n  })\n  watch(() => appSetting['player.volume'], _volume => {\n    setVolume(_volume)\n  })\n  watch(() => appSetting['player.isMute'], muteStatus => {\n    setMute(muteStatus)\n  })\n\n\n  window.key_event.on(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n  window.key_event.on(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n  window.key_event.on(HOTKEY_PLAYER.volume_mute.action, hotkeyVolumeMute)\n  window.app_event.on('setVolume', handleSetVolume)\n  window.app_event.on('setVolumeIsMute', handleToggleVolumeMute)\n\n  onBeforeUnmount(() => {\n    window.key_event.off(HOTKEY_PLAYER.volume_up.action, hotkeyVolumeUp)\n    window.key_event.off(HOTKEY_PLAYER.volume_down.action, hotkeyVolumeDown)\n    window.key_event.off(HOTKEY_PLAYER.volume_mute.action, hotkeyVolumeMute)\n    window.app_event.off('setVolume', handleSetVolume)\n    window.app_event.off('setVolumeIsMute', handleToggleVolumeMute)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/usePlayer/useWatchList.ts",
    "content": "import { onBeforeUnmount } from '@common/utils/vueTools'\n\nimport { playInfo, playMusicInfo } from '@renderer/store/player/state'\nimport { setPlayMusicInfo, updatePlayIndex } from '@renderer/store/player/action'\nimport { throttle } from '@common/utils'\nimport { playNext, stop } from '@renderer/core/player'\n\nconst changedListIds = new Set<string | null>()\n\nexport default () => {\n  const throttleListChange = throttle(() => {\n    const isSkip = playMusicInfo.listId && !changedListIds.has(playInfo.playerListId) && !changedListIds.has(playMusicInfo.listId)\n    changedListIds.clear()\n    if (isSkip) return\n\n    const { playIndex } = updatePlayIndex()\n    if (playIndex < 0) { // 歌曲被移除\n      if (window.lx.isPlayedStop) {\n        stop()\n        setTimeout(() => {\n          setPlayMusicInfo(null, null)\n        })\n      } else if (!playMusicInfo.isTempPlay) {\n        console.log('current music removed')\n        void playNext(true)\n      }\n    }\n  })\n\n  const handleListChange = (listIds: string[]) => {\n    for (const id of listIds) {\n      changedListIds.add(id)\n    }\n    throttleListChange()\n  }\n\n  const handleDownloadListChange = () => {\n    handleListChange(['download'])\n  }\n\n  window.app_event.on('myListUpdate', handleListChange)\n  window.app_event.on('downloadListUpdate', handleDownloadListChange)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('myListUpdate', handleListChange)\n    window.app_event.off('downloadListUpdate', handleDownloadListChange)\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useSettingSync.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport { isFullscreen, proxy, sync, windowSizeList } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { sendSyncAction, setWindowSize } from '@renderer/utils/ipc'\nimport { setLanguage } from '@root/lang'\nimport { setUserApi } from '../apiSource'\n// import { applyTheme, getThemes } from '@renderer/store/utils'\n\n\nexport default () => {\n  watch(() => appSetting['common.windowSizeId'], (index) => {\n    const info = index == null ? windowSizeList[2] : windowSizeList[index]\n    setWindowSize(info.width, info.height)\n  })\n  watch(() => appSetting['common.fontSize'], (fontSize) => {\n    if (isFullscreen.value) return\n    document.documentElement.style.fontSize = `${fontSize}px`\n  })\n\n  watch(() => appSetting['common.langId'], (id) => {\n    if (!id) return\n    setLanguage(id)\n    window.setLang(id)\n  })\n\n  watch(() => appSetting['common.apiSource'], apiSource => {\n    void setUserApi(apiSource)\n  })\n\n  watch(() => appSetting['common.font'], (val) => {\n    document.documentElement.style.fontFamily = val\n  }, {\n    immediate: true,\n  })\n\n  watch(() => appSetting['sync.mode'], (mode) => {\n    sync.mode = mode\n  })\n\n  watch(() => appSetting['sync.enable'], enable => {\n    switch (appSetting['sync.mode']) {\n      case 'server':\n        if (appSetting['sync.server.port']) {\n          void sendSyncAction({\n            action: 'enable_server',\n            data: {\n              enable: appSetting['sync.enable'],\n              port: appSetting['sync.server.port'],\n            },\n          }).catch(err => {\n            console.log(err)\n          })\n        }\n        break\n      case 'client':\n        if (appSetting['sync.client.host']) {\n          void sendSyncAction({\n            action: 'enable_client',\n            data: {\n              enable: appSetting['sync.enable'],\n              host: appSetting['sync.client.host'],\n            },\n          }).catch(err => {\n            console.log(err)\n          })\n        }\n        break\n      default:\n        break\n    }\n    sync.enable = enable\n  })\n  watch(() => appSetting['sync.server.port'], port => {\n    if (appSetting['sync.mode'] == 'server') {\n      void sendSyncAction({\n        action: 'enable_server',\n        data: {\n          enable: appSetting['sync.enable'],\n          port: appSetting['sync.server.port'],\n        },\n      })\n    }\n    sync.server.port = port\n  })\n  watch(() => appSetting['sync.client.host'], host => {\n    if (appSetting['sync.mode'] == 'client') {\n      void sendSyncAction({\n        action: 'enable_client',\n        data: {\n          enable: appSetting['sync.enable'],\n          host: appSetting['sync.client.host'],\n        },\n      })\n    }\n    sync.client.host = host\n  })\n\n  watch(() => appSetting['network.proxy.enable'], enable => {\n    proxy.enable = enable\n  })\n  watch(() => appSetting['network.proxy.host'], host => {\n    proxy.host = host\n  })\n  watch(() => appSetting['network.proxy.port'], port => {\n    proxy.port = port\n  })\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useStatusbarLyric.ts",
    "content": "import { appSetting } from '@renderer/store/setting'\nimport { setDisableAutoPauseBySource } from '@renderer/core/lyric'\n\nexport default () => {\n  const handleEnable = (enable: boolean) => {\n    setDisableAutoPauseBySource(enable, 'statusBarLyric')\n  }\n\n  window.app_event.on('configUpdate', (setting) => {\n    if (setting['player.isShowStatusBarLyric'] != null) {\n      handleEnable(setting['player.isShowStatusBarLyric'])\n    }\n  })\n\n  return async() => {\n    if (appSetting['player.isShowStatusBarLyric']) {\n      handleEnable(true)\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useSync.ts",
    "content": "import { markRaw, onBeforeUnmount } from '@common/utils/vueTools'\nimport { onSyncAction, sendSyncAction } from '@renderer/utils/ipc'\nimport { sync } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { SYNC_CODE } from '@common/constants_sync'\n\nexport default () => {\n  const handleSyncList = (event: LX.Sync.SyncMainWindowActions) => {\n    // console.log(event)\n    switch (event.action) {\n      case 'select_mode':\n        sync.deviceName = event.data.deviceName\n        sync.type = event.data.type\n        sync.isShowSyncMode = true\n        break\n      case 'close_select_mode':\n        sync.isShowSyncMode = false\n        break\n      case 'server_status':\n        sync.server.status.status = event.data.status\n        sync.server.status.message = event.data.message\n        sync.server.status.address = markRaw(event.data.address)\n        sync.server.status.code = event.data.code\n        sync.server.status.devices = markRaw(event.data.devices)\n        break\n      case 'client_status':\n        sync.client.status.status = event.data.status\n        sync.client.status.message = event.data.message\n        sync.client.status.address = markRaw(event.data.address)\n        if (event.data.message == SYNC_CODE.missingAuthCode || event.data.message == SYNC_CODE.authFailed) {\n          if (!sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = true\n        } else if (sync.isShowAuthCodeModal) sync.isShowAuthCodeModal = false\n        break\n    }\n  }\n\n  const rSyncAction = onSyncAction(({ params }) => {\n    handleSyncList(params)\n  })\n\n  onBeforeUnmount(() => {\n    rSyncAction()\n  })\n\n  return async() => {\n    sync.enable = appSetting['sync.enable']\n    sync.mode = appSetting['sync.mode']\n    sync.server.port = appSetting['sync.server.port']\n    sync.client.host = appSetting['sync.client.host']\n    if (appSetting['sync.enable']) {\n      switch (appSetting['sync.mode']) {\n        case 'server':\n          if (appSetting['sync.server.port']) {\n            void sendSyncAction({\n              action: 'enable_server',\n              data: {\n                enable: appSetting['sync.enable'],\n                port: appSetting['sync.server.port'],\n              },\n            }).catch(err => {\n              console.log(err)\n            })\n          }\n          break\n        case 'client':\n          if (appSetting['sync.client.host']) {\n            void sendSyncAction({\n              action: 'enable_client',\n              data: {\n                enable: appSetting['sync.enable'],\n                host: appSetting['sync.client.host'],\n              },\n            }).catch(err => {\n              console.log(err)\n            })\n          }\n          break\n        default:\n          break\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/core/useApp/useUpdate.ts",
    "content": "import { nextTick, onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport {\n  onUpdateAvailable,\n  onUpdateDownloaded,\n  onUpdateError,\n  onUpdateNotAvailable,\n  onUpdateProgress,\n  getIgnoreVersion,\n  getLastStartInfo,\n  saveLastStartInfo,\n} from '@renderer/utils/ipc'\nimport { compareVer, isWin } from '@common/utils'\nimport { isShowChangeLog, versionInfo } from '@renderer/store'\nimport { getVersionInfo } from '@renderer/utils/update'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default () => {\n  let isShowedChangeLog = false\n\n  // 更新超时定时器\n  let updateTimeout: number | null = null\n  const startUpdateTimeout = () => {\n    if (window.lx.isProd && !(isWin && process.arch.includes('arm'))) {\n      updateTimeout = window.setTimeout(() => {\n        updateTimeout = null\n        void nextTick(() => {\n          showUpdateModal()\n          setTimeout(() => {\n            void dialog({\n              message: window.i18n.t('update__timeout_top'),\n              confirmButtonText: window.i18n.t('alert_button_text'),\n            })\n          }, 500)\n        })\n      }, 60 * 60 * 1000)\n    }\n  }\n\n  const clearUpdateTimeout = () => {\n    if (!updateTimeout) return\n    clearTimeout(updateTimeout)\n    updateTimeout = null\n  }\n\n  const handleShowChangeLog = () => {\n    isShowedChangeLog = true\n    void getLastStartInfo().then((version) => {\n      if (version == process.versions.app) return\n      saveLastStartInfo(process.versions.app)\n      if (!appSetting['common.showChangeLog']) return\n      if (version) {\n        if (compareVer(process.versions.app, version) < 0) {\n          void dialog({\n            message: window.i18n.t('update__downgrade_tip', { ver: `${version} → ${process.versions.app}` }),\n            confirmButtonText: window.i18n.t('update__ignore_confirm_tip_confirm'),\n          })\n          return\n        }\n\n        if (compareVer(version, versionInfo.newVersion!.version) >= 0) return\n      } else if (\n        // 如果当前版本不在已发布的版本中，则不需要显示更新日志\n        ![{ version: versionInfo.newVersion!.version, desc: '' }, ...(versionInfo.newVersion!.history ?? [])]\n          .some(i => i.version == process.versions.app)\n      ) return\n      isShowChangeLog.value = true\n    })\n  }\n\n  const handleGetVersionInfo = async(): Promise<NonNullable<typeof versionInfo['newVersion']>> => {\n    return (versionInfo.newVersion?.history && !versionInfo.reCheck\n      ? Promise.resolve(versionInfo.newVersion)\n      : getVersionInfo().then((body: any) => {\n        versionInfo.newVersion = body\n        return body\n      })\n    ).catch(() => {\n      if (versionInfo.newVersion) return versionInfo.newVersion\n      let result = {\n        version: '0.0.0',\n        desc: '',\n      }\n      versionInfo.newVersion = result\n      return result\n    })\n  }\n\n  let versionInfoPromise: null | ReturnType<typeof handleGetVersionInfo> = null\n\n  const showUpdateModal = (status?: LX.UpdateStatus) => {\n    if (versionInfoPromise) {\n      if (\n        // @ts-expect-error\n        versionInfoPromise.resolved &&\n        versionInfo.reCheck) {\n        versionInfoPromise = handleGetVersionInfo()\n      }\n    } else versionInfoPromise = handleGetVersionInfo()\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    void versionInfoPromise.then((result) => {\n      versionInfo.reCheck = false\n\n      if (result.version == '0.0.0') {\n        versionInfo.isUnknown = true\n        versionInfo.status = 'error'\n        let ignoreFailTipTime = parseInt(localStorage.getItem('update__check_failed_tip') ?? '0')\n        if (Date.now() - ignoreFailTipTime > 7 * 86400000) {\n          versionInfo.showModal = true\n        }\n        return\n      }\n      versionInfo.isUnknown = false\n      if (compareVer(versionInfo.version, result.version) != -1) {\n        versionInfo.status = 'idle'\n        versionInfo.isLatest = true\n        handleShowChangeLog()\n        return\n      }\n\n      return getIgnoreVersion().then((ignoreVersion) => {\n        versionInfo.isLatest = false\n        let preStatus = versionInfo.status\n        if (status) versionInfo.status = status\n        if (result.version === ignoreVersion) return\n        void nextTick(() => {\n          versionInfo.showModal = true\n          if (status == 'error' && preStatus == 'downloading' && !localStorage.getItem('update__download_failed_tip')) {\n            setTimeout(() => {\n              void dialog({\n                message: window.i18n.t('update__error_top'),\n                confirmButtonText: window.i18n.t('alert_button_text'),\n              }).finally(() => {\n                localStorage.setItem('update__download_failed_tip', '1')\n              })\n            }, 500)\n          }\n        })\n      })\n    }).finally(() => {\n      // @ts-expect-error\n      versionInfoPromise!.resolved = true\n    })\n  }\n\n  const rUpdateAvailable = onUpdateAvailable(({ params: info }) => {\n    // versionInfo.isDownloading = true\n    // console.log(info)\n    versionInfo.newVersion = {\n      version: info.version,\n      desc: info.releaseNotes as string,\n    }\n    versionInfo.isLatest = false\n    if (appSetting['common.tryAutoUpdate']) {\n      versionInfo.status = 'downloading'\n      startUpdateTimeout()\n    }\n    void nextTick(() => {\n      showUpdateModal()\n    })\n  })\n  const rUpdateNotAvailable = onUpdateNotAvailable(({ params: info }) => {\n    clearUpdateTimeout()\n    // versionInfo.newVersion = {\n    //   version: info.version,\n    //   desc: info.releaseNotes as string,\n    // }\n    void handleGetVersionInfo().finally(() => {\n      versionInfo.isLatest = true\n      versionInfo.isUnknown = false\n      versionInfo.status = 'idle'\n      handleShowChangeLog()\n    })\n  })\n  const rUpdateError = onUpdateError((params) => {\n    clearUpdateTimeout()\n    // versionInfo.status = 'error'\n    void nextTick(() => {\n      showUpdateModal('error')\n    })\n  })\n  const rUpdateProgress = onUpdateProgress(({ params: progress }) => {\n    versionInfo.downloadProgress = progress\n  })\n  const rUpdateDownloaded = onUpdateDownloaded(({ params: info }) => {\n    clearUpdateTimeout()\n    // versionInfo.status = 'downloaded'\n    void nextTick(() => {\n      showUpdateModal('downloaded')\n    })\n  })\n\n  watch(() => versionInfo.showModal, (visible) => {\n    if (visible || isShowedChangeLog || versionInfo.status == 'downloaded') return\n    setTimeout(() => {\n      handleShowChangeLog()\n    }, 1000)\n  })\n\n  onBeforeUnmount(() => {\n    clearUpdateTimeout()\n    rUpdateAvailable()\n    rUpdateNotAvailable()\n    rUpdateError()\n    rUpdateProgress()\n    rUpdateDownloaded()\n  })\n}\n"
  },
  {
    "path": "src/renderer/event/Event.ts",
    "content": "// import mitt from 'mitt'\n// import type { Emitter } from 'mitt'\n\nexport default class Event {\n  listeners: Map<string, Array<(...args: any[]) => any>>\n  constructor() {\n    this.listeners = new Map()\n  }\n\n  on(eventName: string, listener: (...args: any[]) => any) {\n    let targetListeners = this.listeners.get(eventName)\n    if (!targetListeners) this.listeners.set(eventName, targetListeners = [])\n    targetListeners.push(listener)\n  }\n\n  off(eventName: string, listener: (...args: any[]) => any) {\n    let targetListeners = this.listeners.get(eventName)\n    if (!targetListeners) return\n    const index = targetListeners.indexOf(listener)\n    if (index < 0) return\n    targetListeners.splice(index, 1)\n  }\n\n  emit(eventName: string, ...args: any[]) {\n    let targetListeners = this.listeners.get(eventName)\n    if (!targetListeners) return\n    for (const listener of targetListeners) {\n      listener(...args)\n    }\n  }\n\n  offAll(eventName: string) {\n    let targetListeners = this.listeners.get(eventName)\n    if (!targetListeners) return\n    this.listeners.delete(eventName)\n  }\n}\n\n// export class App_EVENT {\n//   listeners: Map<string, Array<() => void>>\n//   constructor() {\n//     this.listeners = new Map()\n//   }\n\n//   on(eventName: string, listener: () => void) {\n//     let targetListeners = this.listeners.get(eventName)\n//     if (targetListeners) this.listeners.set(eventName, targetListeners = [])\n//     targetListeners!.push(listener)\n//   }\n\n//   off(eventName: string, listener: () => void) {\n\n//   }\n// }\n\n"
  },
  {
    "path": "src/renderer/event/appEvent.ts",
    "content": "import Event from './Event'\n\n\n// {\n//   // sync: {\n//   //   send_action_list: 'send_action_list',\n//   //   handle_action_list: 'handle_action_list',\n//   //   send_sync_list: 'send_sync_list',\n//   //   handle_sync_list: 'handle_sync_list',\n//   // },\n// }\n\nexport class AppEvent extends Event {\n  configUpdate(setting: Partial<LX.AppSetting>) {\n    this.emit('configUpdate', setting)\n  }\n\n  focus() {\n    this.emit('focus')\n  }\n\n  dragStart() {\n    this.emit('dragStart')\n  }\n\n  dragEnd() {\n    this.emit('dragEnd')\n  }\n\n  /**\n   * 音乐信息切换\n   */\n  musicToggled() {\n    this.emit('musicToggled')\n  }\n\n  /**\n   * 手动改变进度\n   * @param progress 进度\n   */\n  setProgress(progress: number, maxPlayTime?: number) {\n    this.emit('setProgress', progress, maxPlayTime)\n  }\n\n  /**\n   * 设置音量大小\n   * @param volume 音量大小\n   */\n  setVolume(volume: number) {\n    this.emit('setVolume', volume)\n  }\n\n  /**\n   * 设置播放速率大小\n   * @param rate 播放速率\n   */\n  setPlaybackRate(rate: number) {\n    this.emit('setPlaybackRate', rate)\n  }\n\n  /**\n   * 设置是否静音\n   * @param isMute 是否静音\n   */\n  setVolumeIsMute(isMute: boolean) {\n    this.emit('setVolumeIsMute', isMute)\n  }\n\n  // 播放器事件\n  play() {\n    this.emit('play')\n  }\n\n  pause() {\n    this.emit('pause')\n  }\n\n  stop() {\n    this.emit('stop')\n  }\n\n  error(code?: number) {\n    this.emit('error', code)\n  }\n\n  // 播放器原始事件\n  playerPlaying() {\n    this.emit('playerPlaying')\n  }\n\n  playerPause() {\n    this.emit('playerPause')\n  }\n\n  playerStop() {\n    this.emit('playerStop')\n  }\n\n  playerEnded() {\n    this.emit('playerEnded')\n  }\n\n  playerError(code?: number) {\n    this.emit('playerError', code)\n  }\n\n  playerLoadeddata() {\n    this.emit('playerLoadeddata')\n  }\n\n  playerLoadstart() {\n    this.emit('playerLoadstart')\n  }\n\n  playerCanplay() {\n    this.emit('playerCanplay')\n  }\n\n  playerEmptied() {\n    this.emit('playerEmptied')\n  }\n\n  playerWaiting() {\n    this.emit('playerWaiting')\n  }\n\n  playerDeviceChanged() {\n    this.emit('playerDeviceChanged')\n  }\n\n  // 激活进度条动画事件\n  activePlayProgressTransition() {\n    this.emit('activePlayProgressTransition')\n  }\n\n  // 更新图片事件\n  picUpdated() {\n    this.emit('picUpdated')\n  }\n\n  // 更新歌词事件\n  lyricUpdated() {\n    this.emit('lyricUpdated')\n  }\n\n  // 更新歌词偏移\n  lyricOffsetUpdate() {\n    this.emit('lyricOffsetUpdate')\n  }\n\n  // 歌词行播放\n  lyricLinePlay(text: string, line: number) {\n    this.emit('lyricLinePlay', text, line)\n  }\n\n  // 我的列表改变事件\n  myListUpdate(ids: string[]) {\n    this.emit('myListUpdate', ids)\n  }\n\n  // 下载列表改变事件\n  downloadListUpdate() {\n    this.emit('downloadListUpdate')\n  }\n\n  // 列表里的音乐信息改变事件\n  // musicInfoUpdate(musicInfo: LX.Music.MusicInfo) {\n  //   this.emit('musicInfoUpdate', musicInfo)\n  // }\n\n  keyDown(event: LX.KeyDownEevent) {\n    this.emit('keyDown', event)\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends AppEvent {\n  on<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): any\n  off<K extends keyof EventMethods>(event: K, listener: EventMethods[K]): any\n}\n\nexport type AppEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\nexport const createAppEventHub = (): AppEventTypes => {\n  return new AppEvent()\n}\n"
  },
  {
    "path": "src/renderer/event/index.ts",
    "content": "import { getHotKeyConfig, onFocus, onKeyDown, onUpdateHotkey } from '@renderer/utils/ipc'\nimport { registerKeyEvent, createKeyEventHub } from './keyEvent'\n// import { registerRendererEvents, unregisterRendererEvents } from './rendererEvent'\nimport { createAppEventHub } from './appEvent'\n\nexport const registerEvents = () => {\n  window.lx.isEditingHotKey = false\n  window.app_event = createAppEventHub()\n  window.key_event = createKeyEventHub()\n\n  const setHotkeyConfig = ({ local, global }: LX.HotKeyConfigAll) => {\n    window.lx.appHotKeyConfig = {\n      local,\n      global,\n    }\n  }\n\n  void getHotKeyConfig().then(setHotkeyConfig)\n\n  onUpdateHotkey(({ params }) => {\n    setHotkeyConfig(params)\n  })\n\n  onKeyDown(({ params: { key } }) => {\n    const keyInfo = window.lx.appHotKeyConfig.global.keys[key]\n    if (keyInfo) window.key_event.emit(keyInfo.action)\n  })\n\n  onFocus(() => {\n    window.app_event.focus()\n  })\n\n  registerKeyEvent()\n  // registerRendererEvents()\n}\n\n// export const unregisterEvents = () => {\n//   unregisterKeyEvent()\n//   // unregisterRendererEvents()\n// }\n\nexport { clearDownKeys } from './keyEvent'\n\nexport type { AppEventTypes } from './appEvent'\nexport type { KeyEventTypes } from './keyEvent'\n\nregisterEvents()\n"
  },
  {
    "path": "src/renderer/event/keyEvent.ts",
    "content": "import keyBind from '../utils/keyBind'\nimport { HOTKEY_COMMON } from '@common/hotKey'\nimport Event from './Event'\nimport { appSetting } from '@renderer/store/setting'\n\ndeclare class keyEventTypes extends Event {\n  on(event: string, listener: (event: LX.KeyDownEevent) => any): void\n  off(event: string, listener: (event: LX.KeyDownEevent) => any): void\n}\n\nexport type KeyEventTypes = keyEventTypes\n\nexport const createKeyEventHub = (): keyEventTypes => {\n  return new Event()\n}\n\nwindow.lx.isEditingHotKey = false\n// let appHotKeyConfig: LX.HotKeyConfigAll = window.lx.appHotKeyConfig\n\nexport const registerKeyEvent = () => {\n  keyBind.bindKey((key, eventKey, type, event, keys, isEditing) => {\n    // console.log(`key_${key}_${type}`)\n    window.app_event.keyDown({ event, keys, key, eventKey, type })\n    // console.log(event, key)\n    // console.log(key, eventKey, type, event, keys)\n    if (window.lx.isEditingHotKey || (isEditing && type == 'down') || event?.lx_handled) return\n    if (event && window.lx.appHotKeyConfig.local.enable && window.lx.appHotKeyConfig.local.keys[key] && (key != 'escape' || !((event.target as HTMLElement).classList.contains('ignore-esc')))) {\n      // console.log(key, eventKey, type, keys, isEditing)\n      event.preventDefault()\n      if (type == 'up') return\n\n      // 软件内快捷键的最小化触发时\n      // 如果已启用托盘，则隐藏程序，否则最小化程序 https://github.com/lyswhut/lx-music-desktop/issues/603\n      if (window.lx.appHotKeyConfig.local.keys[key].action == HOTKEY_COMMON.min.action && appSetting['tray.enable']) {\n        window.key_event.emit(HOTKEY_COMMON.hide_toggle.action)\n        return\n      }\n\n      window.key_event.emit(window.lx.appHotKeyConfig.local.keys[key].action)\n      return\n    }\n    // console.log(`key_${key}_${type}`)\n    window.key_event.emit(`key_${key}_${type}`, { event, keys, key, eventKey, type })\n    if (key != eventKey) window.key_event.emit(`key_${eventKey}_${type}`, { event, keys, key, eventKey, type })\n  })\n}\n\nexport const unregisterKeyEvent = () => {\n  keyBind.unbindKey()\n}\n\nexport const clearDownKeys = () => {\n  keyBind.clearDownKeys()\n}\n"
  },
  {
    "path": "src/renderer/index.html",
    "content": "<!DOCTYPE html>\n<html style=\"background-color: transparent;\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>LX Music</title>\n</head>\n<body id=\"body\" style=\"background-color: transparent;\">\n  <!-- <div id=\"waiting-mask\">\n    <svg xmlns=\"http://www.w3.org/2000/svg\" xml:space=\"preserve\" height=\"50%\" viewbox=\"0 0 447.942 447.943\">\n      <path id=\"logo-path-1\" d=\"M203.806.482c-19.668-3.346-35.76 11.139-35.76 31.086v206.166c-11.642-4.271-24.165-6.725-37.281-6.725-59.905 0-108.469 48.566-108.469 108.473 0 59.903 48.564 108.461 108.469 108.461 34.141 0 64.54-15.82 84.406-40.482l-49.658-49.664c-15.116-15.112-11.708-28.901-9.542-34.14 2.166-5.233 9.514-17.4 30.883-17.4h18.082v-56.885c0-21.132 14.617-38.862 34.266-43.745.032-44.373.032-81.808.032-81.808 140.147 0 131.724 83.974 115.325 132.196-6.42 18.884-2.601 22.05 10.893 7.354C536.473 77.106 298.38 16.566 203.806.482z\"/>\n      <path id=\"logo-path-2\" d=\"M301.061 223.876h-50.994c-3.911 0-7.574.95-10.889 2.523-8.616 4.09-14.615 12.798-14.615 22.973v76.51h-37.708c-14.082 0-17.428 8.071-7.466 18.029l46.893 46.898 31.25 31.246a25.424 25.424 0 0 0 18.033 7.474c6.523 0 13.052-2.484 18.029-7.474l78.152-78.145c9.951-9.958 6.608-18.029-7.47-18.029h-37.71v-76.51c.001-14.078-11.42-25.495-25.505-25.495z\"/>\n    </svg>\n  </div> -->\n  <div id=\"root\" style=\"display: none;\">\n  </div>\n  <script>\n    const formatLang = (lang = 'en') => {\n      if (lang === 'zh-cn') return 'zh-Hans'\n      if (lang === 'zh-tw') return 'zh-Hant'\n      return lang.split('-')[0]\n    }\n    window.setLang = (lang = navigator.language.toLocaleLowerCase()) => {\n      document.documentElement.setAttribute('lang', formatLang(lang))\n    }\n    window.setLang()\n    document.documentElement.classList.add(/os=(\\w+)/.exec(window.location.search)[1])\n    window.shouldUseDarkColors = /dark=true/.test(window.location.search)\n    window.dt = /dt=true/.test(window.location.search)\n    document.documentElement.classList.add(window.dt ? 'disableTransparent' : 'transparent')\n\n    window.dom_style = document.createElement('style')\n\n    window.setTheme = colors => {\n      window.dom_style.innerText = `:root {${(Object.entries(colors)).map(([key, value]) => `${key}:${value};`).join('')}}`\n    }\n    const applyThemeColor = (theme) => {\n      theme = JSON.parse(decodeURIComponent(theme))\n      window.setTheme(theme.colors)\n      document.body.appendChild(window.dom_style)\n    }\n    if (/theme=(.+)(#|$)/.test(window.location.search)) applyThemeColor(RegExp.$1)\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "src/renderer/main.ts",
    "content": "import '@common/error'\nimport { createApp } from 'vue'\n\nimport './core/globalData'\n\nimport '@renderer/event'\n\n// Components\nimport mountComponents from './components'\n\n// Plugins\nimport initPlugins from './plugins'\nimport { i18nPlugin } from './plugins/i18n'\n\nimport App from './App.vue'\nimport router from './router'\n// import store from './store'\n\n\nimport { getSetting, updateSetting } from './utils/ipc'\nimport { langList } from '@root/lang'\nimport type { I18n } from '@root/lang/i18n'\n\nimport { initSetting } from './store/setting'\n// import { bubbleCursor } from './utils/cursor-effects/bubbleCursor'\n\nimport './worker'\nimport { saveViewPrevState } from './utils/data'\n\n// sync(store, router)\n\nrouter.afterEach((to) => {\n  if (to.path != '/songList/detail') {\n    saveViewPrevState({\n      url: to.path,\n      query: { ...to.query },\n    })\n  }\n})\n\nvoid getSetting().then(setting => {\n  // window.lx.appSetting = setting\n  // Set language automatically\n  if (!setting['common.langId'] || !window.i18n.availableLocales.includes(setting['common.langId'])) {\n    let langId: I18n['locale'] | null = null\n    const locale = window.navigator.language.toLocaleLowerCase() as I18n['locale']\n    if (window.i18n.availableLocales.includes(locale)) {\n      langId = locale\n    } else {\n      for (const lang of langList) {\n        if (lang.alternate == locale) {\n          langId = lang.locale\n          break\n        }\n      }\n      langId ??= 'en-us'\n    }\n    setting['common.langId'] = langId\n    void updateSetting({ 'common.langId': langId })\n    console.log('Set lang', setting['common.langId'])\n  }\n  window.setLang(setting['common.langId'])\n  window.i18n.setLanguage(setting['common.langId'])\n\n  if (!setting['common.startInFullscreen'] && (document.body.clientHeight > window.screen.availHeight || document.body.clientWidth > window.screen.availWidth) && setting['common.windowSizeId'] > 1) {\n    void updateSetting({ 'common.windowSizeId': 1 })\n  }\n\n  // store.commit('setSetting', setting)\n  initSetting(setting)\n\n  const app = createApp(App)\n  app\n    .use(router)\n    // .use(store)\n    .use(i18nPlugin)\n  initPlugins(app)\n  mountComponents(app)\n  app.mount('#root')\n})\n\n// bubbleCursor()\n"
  },
  {
    "path": "src/renderer/plugins/Dialog/Dialog.vue",
    "content": "<template>\n  <Modal :show=\"visible\" :close-btn=\"false\" :teleport=\"teleport\" @close=\"handleCancel\" @after-leave=\"afterLeave\">\n    <main class=\"scroll\" :class=\"[$style.main, { 'select': selection }]\">{{ message }}</main>\n    <footer :class=\"$style.footer\">\n      <Btn v-if=\"showCancel\" :class=\"$style.btn\" @click=\"handleCancel\">{{ cancelBtnText }}</Btn>\n      <Btn :class=\"$style.btn\" @click=\"handleComfirm\">{{ confirmBtnText }}</Btn>\n    </footer>\n  </Modal>\n</template>\n\n<script>\nimport Modal from '@renderer/components/material/Modal.vue'\nimport Btn from '@renderer/components/base/Btn.vue'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { computed } from '@common/utils/vueTools'\nexport default {\n  components: {\n    Modal,\n    Btn,\n  },\n  props: {\n    afterLeave: {\n      type: Function,\n      default: () => {},\n    },\n  },\n  setup() {\n    const t = useI18n()\n\n    const defaultBtnTexts = computed(() => {\n      return {\n        confirm: t('confirm_button_text'),\n        cancel: t('cancel_button_text'),\n      }\n    })\n\n    return {\n      defaultBtnTexts,\n    }\n  },\n  data() {\n    return {\n      visible: false,\n      message: '',\n      showCancel: false,\n      cancelButtonText: '',\n      confirmButtonText: '',\n      teleport: '#root',\n      selection: false,\n    }\n  },\n  computed: {\n    cancelBtnText() {\n      return this.cancelButtonText || this.defaultBtnTexts.cancel\n    },\n    confirmBtnText() {\n      return this.confirmButtonText || this.defaultBtnTexts.confirm\n    },\n  },\n  beforeUnmount() {\n    const el = this.$el\n    el.parentNode.removeChild(el)\n  },\n  methods: {\n    handleCancel() {\n    },\n    handleComfirm() {\n    },\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n\n.main {\n  flex: auto;\n  min-height: 40px;\n  padding: 15px 15px 0;\n  font-size: 14px;\n  // max-width: 320px;\n  min-width: 220px;\n  line-height: 1.5;\n  white-space: pre-line;\n}\n\n.footer {\n  flex: none;\n  padding: 15px;\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: flex-end;\n  gap: 15px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/plugins/Dialog/index.js",
    "content": "import Dialog from './Dialog.vue'\nimport { createApp } from 'vue'\n\n\nconst defaultOptions = {\n  message: '',\n  teleport: '#root',\n  showCancel: false,\n  cancelButtonText: '',\n  confirmButtonText: '',\n  selection: false,\n}\n\nexport const dialog = function(options) {\n  const { message, showCancel, cancelButtonText, confirmButtonText, teleport, selection } =\n    Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {})\n  return new Promise((resolve, reject) => {\n    let app = createApp(Dialog, {\n      afterLeave() {\n        app?.unmount()\n        app = null\n      },\n    })\n\n    let instance = app.mount(document.createElement('div'))\n\n    // 属性设置\n    instance.visible = true\n    instance.message = message\n    instance.showCancel = showCancel\n    instance.cancelButtonText = cancelButtonText\n    instance.confirmButtonText = confirmButtonText\n    instance.teleport = teleport\n    instance.selection = selection\n\n    // 挂载\n    document.getElementById('container').appendChild(instance.$el)\n\n    instance.handleCancel = () => {\n      instance.visible = false\n      resolve(false)\n    }\n\n    instance.handleComfirm = () => {\n      instance.visible = false\n      resolve(true)\n    }\n  })\n}\n\ndialog.confirm = options => dialog(\n  typeof options == 'string'\n    ? { message: options, showCancel: true }\n    : { ...options, showCancel: true },\n)\n\nconst dialogPlugin = {\n  install(Vue, options) {\n    Vue.config.globalProperties.$dialog = dialog\n  },\n}\n\nexport default dialogPlugin\n"
  },
  {
    "path": "src/renderer/plugins/SvgIcon/SvgIcon.vue",
    "content": "<template>\n  <svg class=\"svg-icon\" aria-hidden=\"true\">\n    <use :xlink:href=\"id\" />\n  </svg>\n</template>\n\n<script>\n\nexport default {\n  name: 'SvgIcon',\n  props: {\n    name: {\n      type: String,\n      required: true,\n    },\n  },\n  computed: {\n    id() {\n      return `#icon-${this.name}`\n    },\n  },\n}\n</script>\n\n<style>\n.svg-icon {\n  width: 1.2em;\n  height: 1.2em;\n  vertical-align: -0.25em;\n  fill: currentColor;\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/plugins/SvgIcon/index.js",
    "content": "import SvgIcon from './SvgIcon.vue'\n\nconst req = require.context('@renderer/assets/svgs', false, /\\.svg$/)\nconst requireAll = requireContext => requireContext.keys().map(requireContext)\nrequireAll(req)\n\nexport default app => {\n  app.component('svg-icon', SvgIcon)\n}\n"
  },
  {
    "path": "src/renderer/plugins/Tips/Tips.js",
    "content": "import Tips from './Tips.vue'\nimport { createApp } from 'vue'\n\nconst addAutoCloseTimer = (instance, time) => {\n  if (!time) return\n  if (instance.autoCloseTimer) clearTimeout(instance.autoCloseTimer)\n  instance.autoCloseTimer = setTimeout(() => {\n    instance.cancel()\n  }, time)\n}\nconst clearAutoCloseTimer = instance => {\n  if (!instance.autoCloseTimer) return\n  clearTimeout(instance.autoCloseTimer)\n  instance.autoCloseTimer = null\n}\n\nexport default ({ position, message, autoCloseTime } = {}, props) => {\n  if (!position) return\n  let app = createApp(Tips, {\n    afterLeave() {\n      app.unmount()\n      app = null\n    },\n  })\n\n  let instance = app.mount(document.createElement('div'))\n\n  // Tips实例挂载到刚创建的div\n  // 属性设置\n  instance.visible = true\n  instance.message = message\n  instance.position.top = position.top\n  instance.position.left = position.left\n\n  // 将Tips的DOM挂载到body上\n  document.body.appendChild(instance.$el)\n\n  instance.cancel = () => {\n    props.beforeClose(instance)\n    clearAutoCloseTimer(instance)\n    instance.visible = false\n    instance = null\n  }\n\n  instance.setTips = tips => {\n    addAutoCloseTimer(instance, autoCloseTime)\n    instance.message = tips\n  }\n\n  addAutoCloseTimer(instance, autoCloseTime)\n\n  return instance\n}\n\n"
  },
  {
    "path": "src/renderer/plugins/Tips/Tips.vue",
    "content": "<template>\n  <transition name=\"tips-fade\" @after-leave=\"afterLeave\">\n    <div\n      v-show=\"visible\" ref=\"dom_tips\" :style=\"{ left: position.left + 'px' , top: position.top + 'px', transform, maxWidth, }\"\n      :class=\"$style.tips\" role=\"presentation\"\n    >\n      {{ message }}\n    </div>\n  </transition>\n</template>\n\n<script>\nexport default {\n  props: {\n    afterLeave: {\n      type: Function,\n      default: () => {},\n    },\n  },\n  data() {\n    return {\n      visible: false,\n      message: '',\n      position: {\n        top: 0,\n        left: 0,\n      },\n      transform: 'translate(0, 0)',\n      maxWidth: '80%',\n      cancel: null,\n      setTips: null,\n      aotoCloseTimer: null,\n    }\n  },\n  watch: {\n    message() {\n      this.$nextTick(() => {\n        this.maxWidth = this.handleGetMaxWidth(this.position.left) + 'px'\n        this.$nextTick(() => {\n          this.transform = `translate(${this.handleGetOffsetXY(this.position.left, this.position.top)})`\n        })\n      })\n    },\n  },\n  beforeUnmount() {\n    const el = this.$el\n    el.parentNode.removeChild(el)\n  },\n  methods: {\n    handleGetMaxWidth(left) {\n      const containerWidth = document.documentElement.clientWidth\n      let maxWidth = containerWidth - left\n      return (maxWidth > left ? maxWidth : left - 12) - 30\n    },\n    handleGetOffsetXY(left, top) {\n      const tipsWidth = this.$refs.dom_tips.clientWidth\n      const tipsHeight = this.$refs.dom_tips.clientHeight\n      const dom_container = document.documentElement\n      const containerWidth = dom_container.clientWidth\n      const containerHeight = dom_container.clientHeight\n      const offsetWidth = containerWidth - left - tipsWidth\n      const offsetHeight = containerHeight - top - tipsHeight\n      let x = 0\n      let y = 0\n      if (tipsWidth < left && containerWidth > tipsWidth && offsetWidth < 5) {\n        x = -tipsWidth - 12\n      }\n      if (tipsHeight < top && containerHeight > tipsHeight && offsetHeight < 5) {\n        y = -tipsHeight - 8\n      }\n      return `${x}px, ${y}px`\n    },\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.tips {\n  position: fixed;\n  // transform: scale(1);\n  line-height: 1.2;\n  word-wrap: break-word;\n  padding: 4px 5px;\n  z-index: 10001;\n  font-size: 12px;\n  // max-width: 80%;\n  color: var(--color-font);\n  border-radius: 3px;\n  background: var(--color-content-background);\n  overflow: hidden;\n  pointer-events: none;\n  // text-align: justify;\n  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);\n  white-space: pre-wrap;\n  box-sizing: border-box;\n}\n\n:global(.tips-fade-enter-active), :global(.tips-fade-leave-active) {\n  transition: opacity .2s;\n}\n:global(.tips-fade-enter), :global(.tips-fade-leave-to) {\n  opacity: 0;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/plugins/Tips/index.js",
    "content": "import tips from './Tips'\nimport { debounce } from '@common/utils'\n\nlet instance\nlet prevTips\nlet prevX = 0\nlet prevY = 0\nlet isDraging = false\n\nconst getTipText = el => {\n  return el.getAttribute('aria-label') && el.getAttribute('ignore-tip') == null ? el.getAttribute('aria-label') : null\n}\n\nconst getTips = el =>\n  el\n    ? getTipText(el)\n      ? getTipText(el)\n      : el.parentNode === document.documentElement\n        ? null\n        : getTips(el.parentNode)\n    : null\n\nconst showTips = debounce(event => {\n  if (isDraging) return\n  let msg = getTips(event.target)?.trim()\n  if (!msg) return\n  prevTips = msg\n  instance = tips({\n    message: msg,\n    autoCloseTime: 10000,\n    position: {\n      top: event.y + 12,\n      left: event.x + 8,\n    },\n  }, {\n    beforeClose(closeInstance) {\n      if (instance !== closeInstance) return\n      prevTips = null\n      instance = null\n    },\n  })\n}, 400)\n\nconst hideTips = () => {\n  if (!instance) return\n  instance.cancel()\n}\n\nconst setTips = tips => {\n  if (!instance) return\n  instance.setTips(tips)\n}\n\nconst updateTips = event => {\n  if (isDraging) return\n  if (!instance) return showTips(event)\n  setTimeout(() => {\n    let msg = getTips(event.target)\n    if (!msg || prevTips === msg) return\n    setTips(msg)\n    prevTips = msg\n  })\n}\n\nsetTimeout(() => {\n  document.body.addEventListener('mousemove', event => {\n    if ((event.x == prevX && event.y == prevY) || isDraging) return\n    prevX = event.x\n    prevY = event.y\n    hideTips()\n    showTips(event)\n  })\n\n  document.body.addEventListener('click', updateTips)\n\n  document.body.addEventListener('contextmenu', updateTips)\n\n  window.app_event.on('focus', () => {\n    hideTips()\n  })\n  window.app_event.on('dragStart', () => {\n    isDraging = true\n    hideTips()\n  })\n  window.app_event.on('dragEnd', () => {\n    isDraging = false\n  })\n})\n"
  },
  {
    "path": "src/renderer/plugins/i18n.ts",
    "content": "import type { I18n } from '@root/lang'\nimport { createI18n, i18nPlugin, useI18n } from '@root/lang'\n\nwindow.i18n = createI18n()\n\nexport {\n  i18nPlugin,\n  useI18n,\n}\n\nexport type { I18n }\n"
  },
  {
    "path": "src/renderer/plugins/index.ts",
    "content": "// import './axios'\nimport { type App } from 'vue'\nimport dialog from './Dialog'\nimport './Tips'\nimport svgIcon from './SvgIcon'\n\nexport default (app: App) => {\n  app.use(dialog)\n\n  svgIcon(app)\n}\n"
  },
  {
    "path": "src/renderer/plugins/player/index.ts",
    "content": "interface HTMLAudioElementChrome extends HTMLAudioElement {\n  setSinkId: (id: string) => Promise<void>\n}\nlet audio: HTMLAudioElementChrome | null = null\nlet audioContext: AudioContext\nlet mediaSource: MediaElementAudioSourceNode\nlet analyser: AnalyserNode\n// https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext\n// https://benzleung.gitbooks.io/web-audio-api-mini-guide/content/chapter5-1.html\nexport const freqs = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000] as const\ntype Freqs = (typeof freqs)[number]\nlet biquads: Map<`hz${Freqs}`, BiquadFilterNode>\nexport const freqsPreset = [\n  { name: 'pop', hz31: 6, hz62: 5, hz125: -3, hz250: -2, hz500: 5, hz1000: 4, hz2000: -4, hz4000: -3, hz8000: 6, hz16000: 4 },\n  { name: 'dance', hz31: 4, hz62: 3, hz125: -4, hz250: -6, hz500: 0, hz1000: 0, hz2000: 3, hz4000: 4, hz8000: 4, hz16000: 5 },\n  { name: 'rock', hz31: 7, hz62: 6, hz125: 2, hz250: 1, hz500: -3, hz1000: -4, hz2000: 2, hz4000: 1, hz8000: 4, hz16000: 5 },\n  { name: 'classical', hz31: 6, hz62: 7, hz125: 1, hz250: 2, hz500: -1, hz1000: 1, hz2000: -4, hz4000: -6, hz8000: -7, hz16000: -8 },\n  { name: 'vocal', hz31: -5, hz62: -6, hz125: -4, hz250: -3, hz500: 3, hz1000: 4, hz2000: 5, hz4000: 4, hz8000: -3, hz16000: -3 },\n  { name: 'slow', hz31: 5, hz62: 4, hz125: 2, hz250: 0, hz500: -2, hz1000: 0, hz2000: 3, hz4000: 6, hz8000: 7, hz16000: 8 },\n  { name: 'electronic', hz31: 6, hz62: 5, hz125: 0, hz250: -5, hz500: -4, hz1000: 0, hz2000: 6, hz4000: 8, hz8000: 8, hz16000: 7 },\n  { name: 'subwoofer', hz31: 8, hz62: 7, hz125: 5, hz250: 4, hz500: 0, hz1000: 0, hz2000: 0, hz4000: 0, hz8000: 0, hz16000: 0 },\n  { name: 'soft', hz31: -5, hz62: -5, hz125: -4, hz250: -4, hz500: 3, hz1000: 2, hz2000: 4, hz4000: 4, hz8000: 0, hz16000: 0 },\n] as const\nexport const convolutions = [\n  { name: 'telephone', mainGain: 0.0, sendGain: 3.0, source: 'filter-telephone.wav' }, // 电话\n  { name: 's2_r4_bd', mainGain: 1.8, sendGain: 0.9, source: 's2_r4_bd.wav' }, // 教堂\n  { name: 'bright_hall', mainGain: 0.8, sendGain: 2.4, source: 'bright-hall.wav' },\n  { name: 'cinema_diningroom', mainGain: 0.6, sendGain: 2.3, source: 'cinema-diningroom.wav' },\n  { name: 'dining_living_true_stereo', mainGain: 0.6, sendGain: 1.8, source: 'dining-living-true-stereo.wav' },\n  { name: 'living_bedroom_leveled', mainGain: 0.6, sendGain: 2.1, source: 'living-bedroom-leveled.wav' },\n  { name: 'spreader50_65ms', mainGain: 1, sendGain: 2.5, source: 'spreader50-65ms.wav' },\n  // { name: 'spreader25_125ms', mainGain: 1, sendGain: 2.5, source: 'spreader25-125ms.wav' },\n  // { name: 'backslap', mainGain: 1.8, sendGain: 0.8, source: 'backslap1.wav' },\n  { name: 's3_r1_bd', mainGain: 1.8, sendGain: 0.8, source: 's3_r1_bd.wav' },\n  { name: 'matrix_1', mainGain: 1.5, sendGain: 0.9, source: 'matrix-reverb1.wav' },\n  { name: 'matrix_2', mainGain: 1.3, sendGain: 1, source: 'matrix-reverb2.wav' },\n  { name: 'cardiod_35_10_spread', mainGain: 1.8, sendGain: 0.6, source: 'cardiod-35-10-spread.wav' },\n  { name: 'tim_omni_35_10_magnetic', mainGain: 1, sendGain: 0.2, source: 'tim-omni-35-10-magnetic.wav' },\n  // { name: 'spatialized', mainGain: 1.8, sendGain: 0.8, source: 'spatialized8.wav' },\n  // { name: 'zing_long_stereo', mainGain: 0.8, sendGain: 1.8, source: 'zing-long-stereo.wav' },\n  { name: 'feedback_spring', mainGain: 1.8, sendGain: 0.8, source: 'feedback-spring.wav' },\n  // { name: 'tim_omni_rear_blend', mainGain: 1.8, sendGain: 0.8, source: 'tim-omni-rear-blend.wav' },\n] as const\n// 半音\n// export const semitones = [-1.5, -1, -0.5, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] as const\n\nlet convolver: ConvolverNode\nlet convolverSourceGainNode: GainNode\nlet convolverOutputGainNode: GainNode\nlet convolverDynamicsCompressor: DynamicsCompressorNode\nlet gainNode: GainNode\nlet panner: PannerNode\nlet pitchShifterNode: AudioWorkletNode\nlet pitchShifterNodePitchFactor: AudioParam | null\nlet pitchShifterNodeLoadStatus: 'none' | 'loading' | 'unconnect' | 'connected' = 'none'\nlet pitchShifterNodeTempValue = 1\nlet defaultChannelCount = 2\nexport const soundR = 0.5\n\n\nexport const createAudio = () => {\n  if (audio) return\n  audio = new window.Audio() as HTMLAudioElementChrome\n  audio.controls = false\n  audio.autoplay = true\n  audio.preload = 'auto'\n  audio.crossOrigin = 'anonymous'\n}\n\nconst initAnalyser = () => {\n  analyser = audioContext.createAnalyser()\n  analyser.fftSize = 256\n}\n\nconst initBiquadFilter = () => {\n  biquads = new Map()\n  let i\n\n  for (const item of freqs) {\n    const filter = audioContext.createBiquadFilter()\n    biquads.set(`hz${item}`, filter)\n    filter.type = 'peaking'\n    filter.frequency.value = item\n    filter.Q.value = 1.4\n    filter.gain.value = 0\n  }\n\n  for (i = 1; i < freqs.length; i++) {\n    (biquads.get(`hz${freqs[i - 1]}`)!).connect(biquads.get(`hz${freqs[i]}`)!)\n  }\n}\n\nconst initConvolver = () => {\n  convolverSourceGainNode = audioContext.createGain()\n  convolverOutputGainNode = audioContext.createGain()\n  convolverDynamicsCompressor = audioContext.createDynamicsCompressor()\n  convolver = audioContext.createConvolver()\n  convolver.connect(convolverOutputGainNode)\n  convolverSourceGainNode.connect(convolverDynamicsCompressor)\n  convolverOutputGainNode.connect(convolverDynamicsCompressor)\n}\n\nconst initPanner = () => {\n  panner = audioContext.createPanner()\n}\n\nconst initGain = () => {\n  gainNode = audioContext.createGain()\n}\n\nconst initAdvancedAudioFeatures = () => {\n  if (audioContext) return\n  if (!audio) throw new Error('audio not defined')\n  audioContext = new window.AudioContext({ latencyHint: 'playback' })\n  defaultChannelCount = audioContext.destination.channelCount\n\n  initAnalyser()\n  initBiquadFilter()\n  initConvolver()\n  initPanner()\n  initGain()\n  // source -> analyser -> biquadFilter -> pitchShifter -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain\n  mediaSource = audioContext.createMediaElementSource(audio)\n  mediaSource.connect(analyser)\n  analyser.connect(biquads.get(`hz${freqs[0]}`)!)\n  const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!)\n  lastBiquadFilter.connect(convolverSourceGainNode)\n  lastBiquadFilter.connect(convolver)\n  convolverDynamicsCompressor.connect(panner)\n  panner.connect(gainNode)\n  gainNode.connect(audioContext.destination)\n\n  // 音频输出设备改变时刷新 audio node 连接\n  window.app_event.on('playerDeviceChanged', handleMediaListChange)\n\n  // audio.addEventListener('playing', connectAudioNode)\n  // audio.addEventListener('pause', disconnectAudioNode)\n  // audio.addEventListener('waiting', disconnectAudioNode)\n  // audio.addEventListener('emptied', disconnectAudioNode)\n  // if (!audio.paused) connectAudioNode()\n}\n\nconst handleMediaListChange = () => {\n  mediaSource.disconnect()\n  mediaSource.connect(analyser)\n}\n\n// let isConnected = true\n// const connectAudioNode = () => {\n//   if (isConnected) return\n//   console.log('connect Node')\n//   mediaSource.connect(analyser)\n//   isConnected = true\n//   if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {\n//     disconnectPitchShifterNode()\n//   }\n// }\n\n// const disconnectAudioNode = () => {\n//   if (!isConnected) return\n//   console.log('disconnect Node')\n//   mediaSource.disconnect()\n//   isConnected = false\n//   if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {\n//     disconnectPitchShifterNode()\n//   }\n// }\n\nexport const getAudioContext = () => {\n  initAdvancedAudioFeatures()\n  return audioContext\n}\n\nlet unsubMediaListChangeEvent: (() => void) | null = null\nexport const setMaxOutputChannelCount = (enable: boolean) => {\n  if (enable) {\n    initAdvancedAudioFeatures()\n    audioContext.destination.channelCountMode = 'max'\n    audioContext.destination.channelCount = audioContext.destination.maxChannelCount\n    // navigator.mediaDevices.addEventListener('devicechange', handleMediaListChange)\n    if (!unsubMediaListChangeEvent) {\n      let handleMediaListChange = () => {\n        setMaxOutputChannelCount(true)\n      }\n      window.app_event.on('playerDeviceChanged', handleMediaListChange)\n      unsubMediaListChangeEvent = () => {\n        window.app_event.off('playerDeviceChanged', handleMediaListChange)\n        unsubMediaListChangeEvent = null\n      }\n    }\n  } else {\n    unsubMediaListChangeEvent?.()\n    if (audioContext && audioContext.destination.channelCountMode != 'explicit') {\n      audioContext.destination.channelCount = defaultChannelCount\n      // audioContext.destination.channelInterpretation\n      audioContext.destination.channelCountMode = 'explicit'\n    }\n  }\n}\n\nexport const getAnalyser = (): AnalyserNode | null => {\n  initAdvancedAudioFeatures()\n  return analyser\n}\n\nexport const getBiquadFilter = () => {\n  initAdvancedAudioFeatures()\n  return biquads\n}\n\n// let isConvolverConnected = false\nexport const setConvolver = (buffer: AudioBuffer | null, mainGain: number, sendGain: number) => {\n  initAdvancedAudioFeatures()\n  convolver.buffer = buffer\n  // console.log(mainGain, sendGain)\n  if (buffer) {\n    convolverSourceGainNode.gain.value = mainGain\n    convolverOutputGainNode.gain.value = sendGain\n  } else {\n    convolverSourceGainNode.gain.value = 1\n    convolverOutputGainNode.gain.value = 0\n  }\n}\n\nexport const setConvolverMainGain = (gain: number) => {\n  if (convolverSourceGainNode.gain.value == gain) return\n  // console.log(gain)\n  convolverSourceGainNode.gain.value = gain\n}\n\nexport const setConvolverSendGain = (gain: number) => {\n  if (convolverOutputGainNode.gain.value == gain) return\n  // console.log(gain)\n  convolverOutputGainNode.gain.value = gain\n}\n\nlet pannerInfo = {\n  x: 0,\n  y: 0,\n  z: 0,\n  soundR: 0.5,\n  rad: 0,\n  speed: 1,\n  intv: null as NodeJS.Timeout | null,\n}\nconst setPannerXYZ = (nx: number, ny: number, nz: number) => {\n  pannerInfo.x = nx\n  pannerInfo.y = ny\n  pannerInfo.z = nz\n  // console.log(pannerInfo)\n  panner.positionX.value = nx * pannerInfo.soundR\n  panner.positionY.value = ny * pannerInfo.soundR\n  panner.positionZ.value = nz * pannerInfo.soundR\n}\nexport const setPannerSoundR = (r: number) => {\n  pannerInfo.soundR = r\n}\n\nexport const setPannerSpeed = (speed: number) => {\n  pannerInfo.speed = speed\n  if (pannerInfo.intv) startPanner()\n}\nexport const stopPanner = () => {\n  if (pannerInfo.intv) {\n    clearInterval(pannerInfo.intv)\n    pannerInfo.intv = null\n    pannerInfo.rad = 0\n  }\n  panner.positionX.value = 0\n  panner.positionY.value = 0\n  panner.positionZ.value = 0\n}\n\nexport const startPanner = () => {\n  initAdvancedAudioFeatures()\n  if (pannerInfo.intv) {\n    clearInterval(pannerInfo.intv)\n    pannerInfo.intv = null\n    pannerInfo.rad = 0\n  }\n  pannerInfo.intv = setInterval(() => {\n    pannerInfo.rad += 1\n    if (pannerInfo.rad > 360) pannerInfo.rad -= 360\n    setPannerXYZ(Math.sin(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180), Math.cos(pannerInfo.rad * Math.PI / 180))\n  }, pannerInfo.speed * 10)\n}\n\nlet isConnected = true\nconst connectNode = () => {\n  if (isConnected) return\n  console.log('connect Node')\n  analyser?.connect(biquads.get(`hz${freqs[0]}`)!)\n  isConnected = true\n  if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {\n    disconnectPitchShifterNode()\n  }\n}\nconst disconnectNode = () => {\n  if (!isConnected) return\n  console.log('disconnect Node')\n  analyser?.disconnect()\n  isConnected = false\n  if (pitchShifterNodeTempValue == 1 && pitchShifterNodeLoadStatus == 'connected') {\n    disconnectPitchShifterNode()\n  }\n}\nconst connectPitchShifterNode = () => {\n  console.log('connect Pitch Shifter Node')\n  audio!.addEventListener('playing', connectNode)\n  audio!.addEventListener('pause', disconnectNode)\n  audio!.addEventListener('waiting', disconnectNode)\n  audio!.addEventListener('emptied', disconnectNode)\n  if (audio!.paused) disconnectNode()\n\n  const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!)\n  lastBiquadFilter.disconnect()\n  lastBiquadFilter.connect(pitchShifterNode)\n\n  pitchShifterNode.connect(convolver)\n  pitchShifterNode.connect(convolverSourceGainNode)\n  // convolverDynamicsCompressor.disconnect(panner)\n  // convolverDynamicsCompressor.connect(pitchShifterNode)\n  // pitchShifterNode.connect(panner)\n  pitchShifterNodeLoadStatus = 'connected'\n  pitchShifterNodePitchFactor!.value = pitchShifterNodeTempValue\n}\nconst disconnectPitchShifterNode = () => {\n  console.log('disconnect Pitch Shifter Node')\n  const lastBiquadFilter = (biquads.get(`hz${freqs.at(-1)!}`)!)\n  lastBiquadFilter.disconnect()\n  lastBiquadFilter.connect(convolver)\n  lastBiquadFilter.connect(convolverSourceGainNode)\n  pitchShifterNodeLoadStatus = 'unconnect'\n  pitchShifterNodePitchFactor = null\n\n  audio!.removeEventListener('playing', connectNode)\n  audio!.removeEventListener('pause', disconnectNode)\n  audio!.removeEventListener('waiting', disconnectNode)\n  audio!.removeEventListener('emptied', disconnectNode)\n  connectNode()\n}\nconst loadPitchShifterNode = () => {\n  pitchShifterNodeLoadStatus = 'loading'\n  initAdvancedAudioFeatures()\n  // source -> analyser -> biquadFilter -> audioWorklet(pitch shifter) -> [(convolver & convolverSource)->convolverDynamicsCompressor] -> panner -> gain\n  void audioContext.audioWorklet.addModule(new URL(\n    /* webpackChunkName: 'pitch_shifter.audioWorklet' */\n    './pitch-shifter/phase-vocoder.js',\n    import.meta.url,\n  )).then(() => {\n    console.log('pitch shifter audio worklet loaded')\n    // https://github.com/olvb/phaze/issues/26#issuecomment-1574629971\n    pitchShifterNode = new AudioWorkletNode(audioContext, 'phase-vocoder-processor', { outputChannelCount: [2] })\n    let pitchFactorParam = pitchShifterNode.parameters.get('pitchFactor')\n    if (!pitchFactorParam) return\n    pitchShifterNodePitchFactor = pitchFactorParam\n    pitchShifterNodeLoadStatus = 'unconnect'\n    if (pitchShifterNodeTempValue == 1) return\n\n    connectPitchShifterNode()\n  })\n}\n\nexport const setPitchShifter = (val: number) => {\n  // console.log('setPitchShifter', val)\n  pitchShifterNodeTempValue = val\n  switch (pitchShifterNodeLoadStatus) {\n    case 'loading':\n      break\n    case 'none':\n      loadPitchShifterNode()\n      break\n    case 'connected':\n      // a: 1 = 半音\n      // value = 2 ** (a / 12)\n      pitchShifterNodePitchFactor!.value = val\n      break\n    case 'unconnect':\n      connectPitchShifterNode()\n      break\n  }\n}\n\nexport const hasInitedAdvancedAudioFeatures = (): boolean => audioContext != null\n\nexport const setResource = (src: string) => {\n  if (audio) audio.src = src\n}\n\nexport const setPlay = () => {\n  void audio?.play()\n}\n\nexport const setPause = () => {\n  audio?.pause()\n}\n\nexport const setStop = () => {\n  if (audio) {\n    audio.src = ''\n    audio.removeAttribute('src')\n  }\n}\n\nexport const isEmpty = (): boolean => !audio?.src\n\nexport const setLoopPlay = (isLoop: boolean) => {\n  if (audio) audio.loop = isLoop\n}\n\nexport const getPlaybackRate = (): number => {\n  return audio?.defaultPlaybackRate ?? 1\n}\n\nexport const setPlaybackRate = (rate: number) => {\n  if (!audio) return\n  audio.defaultPlaybackRate = rate\n  audio.playbackRate = rate\n}\n\nexport const setPreservesPitch = (preservesPitch: boolean) => {\n  if (!audio) return\n  audio.preservesPitch = preservesPitch\n}\n\nexport const getMute = (): boolean => {\n  return audio?.muted ?? false\n}\n\nexport const setMute = (isMute: boolean) => {\n  if (audio) audio.muted = isMute\n}\n\nexport const getCurrentTime = () => {\n  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n  return audio?.currentTime || 0\n}\n\nexport const setCurrentTime = (time: number) => {\n  if (audio) audio.currentTime = time\n}\n\nexport const setMediaDeviceId = async(mediaDeviceId: string): Promise<void> => {\n  if (!audio) return\n  return audio.setSinkId(mediaDeviceId)\n}\n\nexport const setVolume = (volume: number) => {\n  if (audio) audio.volume = volume\n}\n\nexport const getDuration = () => {\n  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n  return audio?.duration || 0\n}\n\n// export const getPlaybackRate = () => {\n//   return audio?.playbackRate ?? 1\n// }\n\ntype Noop = () => void\n\nexport const onPlaying = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('playing', callback)\n  return () => {\n    audio?.removeEventListener('playing', callback)\n  }\n}\n\nexport const onPause = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio?.addEventListener('pause', callback)\n  return () => {\n    audio?.removeEventListener('pause', callback)\n  }\n}\n\nexport const onEnded = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('ended', callback)\n  return () => {\n    audio?.removeEventListener('ended', callback)\n  }\n}\n\nexport const onError = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('error', callback)\n  return () => {\n    audio?.removeEventListener('error', callback)\n  }\n}\n\nexport const onLoadeddata = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('loadeddata', callback)\n  return () => {\n    audio?.removeEventListener('loadeddata', callback)\n  }\n}\n\nexport const onLoadstart = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('loadstart', callback)\n  return () => {\n    audio?.removeEventListener('loadstart', callback)\n  }\n}\n\nexport const onCanplay = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('canplay', callback)\n  return () => {\n    audio?.removeEventListener('canplay', callback)\n  }\n}\n\nexport const onEmptied = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('emptied', callback)\n  return () => {\n    audio?.removeEventListener('emptied', callback)\n  }\n}\n\nexport const onTimeupdate = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('timeupdate', callback)\n  return () => {\n    audio?.removeEventListener('timeupdate', callback)\n  }\n}\n\n// 缓冲中\nexport const onWaiting = (callback: Noop) => {\n  if (!audio) throw new Error('audio not defined')\n\n  audio.addEventListener('waiting', callback)\n  return () => {\n    audio?.removeEventListener('waiting', callback)\n  }\n}\n\n// 可见性改变\nexport const onVisibilityChange = (callback: Noop) => {\n  document.addEventListener('visibilitychange', callback)\n  return () => {\n    document.removeEventListener('visibilitychange', callback)\n  }\n}\n\n\nexport const getErrorCode = () => {\n  return audio?.error?.code\n}\n"
  },
  {
    "path": "src/renderer/plugins/player/pitch-shifter/fft.js",
    "content": "// https://github.com/indutny/fft.js\n\n\nfunction FFT(size) {\n  this.size = size | 0\n  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') }\n\n  this._csize = size << 1\n\n  // NOTE: Use of `var` is intentional for old V8 versions\n  let table = new Array(this.size * 2)\n  for (let i = 0; i < table.length; i += 2) {\n    const angle = Math.PI * i / this.size\n    table[i] = Math.cos(angle)\n    table[i + 1] = -Math.sin(angle)\n  }\n  this.table = table\n\n  // Find size's power of two\n  let power = 0\n  for (let t = 1; this.size > t; t <<= 1) { power++ }\n\n  // Calculate initial step's width:\n  //   * If we are full radix-4 - it is 2x smaller to give inital len=8\n  //   * Otherwise it is the same as `power` to give len=4\n  this._width = power % 2 === 0 ? power - 1 : power\n\n  // Pre-compute bit-reversal patterns\n  this._bitrev = new Array(1 << this._width)\n  for (let j = 0; j < this._bitrev.length; j++) {\n    this._bitrev[j] = 0\n    for (let shift = 0; shift < this._width; shift += 2) {\n      let revShift = this._width - shift - 2\n      this._bitrev[j] |= ((j >>> shift) & 3) << revShift\n    }\n  }\n\n  this._out = null\n  this._data = null\n  this._inv = 0\n}\n\nFFT.prototype.fromComplexArray = function fromComplexArray(complex, storage) {\n  let res = storage || new Array(complex.length >>> 1)\n  for (let i = 0; i < complex.length; i += 2) { res[i >>> 1] = complex[i] }\n  return res\n}\n\nFFT.prototype.createComplexArray = function createComplexArray() {\n  const res = new Array(this._csize)\n  for (let i = 0; i < res.length; i++) { res[i] = 0 }\n  return res\n}\n\nFFT.prototype.toComplexArray = function toComplexArray(input, storage) {\n  let res = storage || this.createComplexArray()\n  for (let i = 0; i < res.length; i += 2) {\n    res[i] = input[i >>> 1]\n    res[i + 1] = 0\n  }\n  return res\n}\n\nFFT.prototype.completeSpectrum = function completeSpectrum(spectrum) {\n  let size = this._csize\n  let half = size >>> 1\n  for (let i = 2; i < half; i += 2) {\n    spectrum[size - i] = spectrum[i]\n    spectrum[size - i + 1] = -spectrum[i + 1]\n  }\n}\n\nFFT.prototype.transform = function transform(out, data) {\n  if (out === data) { throw new Error('Input and output buffers must be different') }\n\n  this._out = out\n  this._data = data\n  this._inv = 0\n  this._transform4()\n  this._out = null\n  this._data = null\n}\n\nFFT.prototype.realTransform = function realTransform(out, data) {\n  if (out === data) { throw new Error('Input and output buffers must be different') }\n\n  this._out = out\n  this._data = data\n  this._inv = 0\n  this._realTransform4()\n  this._out = null\n  this._data = null\n}\n\nFFT.prototype.inverseTransform = function inverseTransform(out, data) {\n  if (out === data) { throw new Error('Input and output buffers must be different') }\n\n  this._out = out\n  this._data = data\n  this._inv = 1\n  this._transform4()\n  for (let i = 0; i < out.length; i++) { out[i] /= this.size }\n  this._out = null\n  this._data = null\n}\n\n// radix-4 implementation\n//\n// NOTE: Uses of `var` are intentional for older V8 version that do not\n// support both `let compound assignments` and `const phi`\nFFT.prototype._transform4 = function _transform4() {\n  let out = this._out\n  let size = this._csize\n\n  // Initial step (permute and transform)\n  let width = this._width\n  let step = 1 << width\n  let len = (size / step) << 1\n\n  let outOff\n  let t\n  let bitrev = this._bitrev\n  if (len === 4) {\n    for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {\n      const off = bitrev[t]\n      this._singleTransform2(outOff, off, step)\n    }\n  } else {\n    // len === 8\n    for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {\n      const off = bitrev[t]\n      this._singleTransform4(outOff, off, step)\n    }\n  }\n\n  // Loop through steps in decreasing order\n  let inv = this._inv ? -1 : 1\n  let table = this.table\n  for (step >>= 2; step >= 2; step >>= 2) {\n    len = (size / step) << 1\n    let quarterLen = len >>> 2\n\n    // Loop through offsets in the data\n    for (outOff = 0; outOff < size; outOff += len) {\n      // Full case\n      let limit = outOff + quarterLen\n      for (let i = outOff, k = 0; i < limit; i += 2, k += step) {\n        const A = i\n        const B = A + quarterLen\n        const C = B + quarterLen\n        const D = C + quarterLen\n\n        // Original values\n        const Ar = out[A]\n        const Ai = out[A + 1]\n        const Br = out[B]\n        const Bi = out[B + 1]\n        const Cr = out[C]\n        const Ci = out[C + 1]\n        const Dr = out[D]\n        const Di = out[D + 1]\n\n        // Middle values\n        const MAr = Ar\n        const MAi = Ai\n\n        const tableBr = table[k]\n        const tableBi = inv * table[k + 1]\n        const MBr = Br * tableBr - Bi * tableBi\n        const MBi = Br * tableBi + Bi * tableBr\n\n        const tableCr = table[2 * k]\n        const tableCi = inv * table[2 * k + 1]\n        const MCr = Cr * tableCr - Ci * tableCi\n        const MCi = Cr * tableCi + Ci * tableCr\n\n        const tableDr = table[3 * k]\n        const tableDi = inv * table[3 * k + 1]\n        const MDr = Dr * tableDr - Di * tableDi\n        const MDi = Dr * tableDi + Di * tableDr\n\n        // Pre-Final values\n        const T0r = MAr + MCr\n        const T0i = MAi + MCi\n        const T1r = MAr - MCr\n        const T1i = MAi - MCi\n        const T2r = MBr + MDr\n        const T2i = MBi + MDi\n        const T3r = inv * (MBr - MDr)\n        const T3i = inv * (MBi - MDi)\n\n        // Final values\n        const FAr = T0r + T2r\n        const FAi = T0i + T2i\n\n        const FCr = T0r - T2r\n        const FCi = T0i - T2i\n\n        const FBr = T1r + T3i\n        const FBi = T1i - T3r\n\n        const FDr = T1r - T3i\n        const FDi = T1i + T3r\n\n        out[A] = FAr\n        out[A + 1] = FAi\n        out[B] = FBr\n        out[B + 1] = FBi\n        out[C] = FCr\n        out[C + 1] = FCi\n        out[D] = FDr\n        out[D + 1] = FDi\n      }\n    }\n  }\n}\n\n// radix-2 implementation\n//\n// NOTE: Only called for len=4\nFFT.prototype._singleTransform2 = function _singleTransform2(outOff, off,\n  step) {\n  const out = this._out\n  const data = this._data\n\n  const evenR = data[off]\n  const evenI = data[off + 1]\n  const oddR = data[off + step]\n  const oddI = data[off + step + 1]\n\n  const leftR = evenR + oddR\n  const leftI = evenI + oddI\n  const rightR = evenR - oddR\n  const rightI = evenI - oddI\n\n  out[outOff] = leftR\n  out[outOff + 1] = leftI\n  out[outOff + 2] = rightR\n  out[outOff + 3] = rightI\n}\n\n// radix-4\n//\n// NOTE: Only called for len=8\nFFT.prototype._singleTransform4 = function _singleTransform4(outOff, off,\n  step) {\n  const out = this._out\n  const data = this._data\n  const inv = this._inv ? -1 : 1\n  const step2 = step * 2\n  const step3 = step * 3\n\n  // Original values\n  const Ar = data[off]\n  const Ai = data[off + 1]\n  const Br = data[off + step]\n  const Bi = data[off + step + 1]\n  const Cr = data[off + step2]\n  const Ci = data[off + step2 + 1]\n  const Dr = data[off + step3]\n  const Di = data[off + step3 + 1]\n\n  // Pre-Final values\n  const T0r = Ar + Cr\n  const T0i = Ai + Ci\n  const T1r = Ar - Cr\n  const T1i = Ai - Ci\n  const T2r = Br + Dr\n  const T2i = Bi + Di\n  const T3r = inv * (Br - Dr)\n  const T3i = inv * (Bi - Di)\n\n  // Final values\n  const FAr = T0r + T2r\n  const FAi = T0i + T2i\n\n  const FBr = T1r + T3i\n  const FBi = T1i - T3r\n\n  const FCr = T0r - T2r\n  const FCi = T0i - T2i\n\n  const FDr = T1r - T3i\n  const FDi = T1i + T3r\n\n  out[outOff] = FAr\n  out[outOff + 1] = FAi\n  out[outOff + 2] = FBr\n  out[outOff + 3] = FBi\n  out[outOff + 4] = FCr\n  out[outOff + 5] = FCi\n  out[outOff + 6] = FDr\n  out[outOff + 7] = FDi\n}\n\n// Real input radix-4 implementation\nFFT.prototype._realTransform4 = function _realTransform4() {\n  let out = this._out\n  let size = this._csize\n\n  // Initial step (permute and transform)\n  let width = this._width\n  let step = 1 << width\n  let len = (size / step) << 1\n\n  let outOff\n  let t\n  let bitrev = this._bitrev\n  if (len === 4) {\n    for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {\n      const off = bitrev[t]\n      this._singleRealTransform2(outOff, off >>> 1, step >>> 1)\n    }\n  } else {\n    // len === 8\n    for (outOff = 0, t = 0; outOff < size; outOff += len, t++) {\n      const off = bitrev[t]\n      this._singleRealTransform4(outOff, off >>> 1, step >>> 1)\n    }\n  }\n\n  // Loop through steps in decreasing order\n  let inv = this._inv ? -1 : 1\n  let table = this.table\n  for (step >>= 2; step >= 2; step >>= 2) {\n    len = (size / step) << 1\n    let halfLen = len >>> 1\n    let quarterLen = halfLen >>> 1\n    let hquarterLen = quarterLen >>> 1\n\n    // Loop through offsets in the data\n    for (outOff = 0; outOff < size; outOff += len) {\n      for (let i = 0, k = 0; i <= hquarterLen; i += 2, k += step) {\n        let A = outOff + i\n        let B = A + quarterLen\n        let C = B + quarterLen\n        let D = C + quarterLen\n\n        // Original values\n        let Ar = out[A]\n        let Ai = out[A + 1]\n        let Br = out[B]\n        let Bi = out[B + 1]\n        let Cr = out[C]\n        let Ci = out[C + 1]\n        let Dr = out[D]\n        let Di = out[D + 1]\n\n        // Middle values\n        let MAr = Ar\n        let MAi = Ai\n\n        let tableBr = table[k]\n        let tableBi = inv * table[k + 1]\n        let MBr = Br * tableBr - Bi * tableBi\n        let MBi = Br * tableBi + Bi * tableBr\n\n        let tableCr = table[2 * k]\n        let tableCi = inv * table[2 * k + 1]\n        let MCr = Cr * tableCr - Ci * tableCi\n        let MCi = Cr * tableCi + Ci * tableCr\n\n        let tableDr = table[3 * k]\n        let tableDi = inv * table[3 * k + 1]\n        let MDr = Dr * tableDr - Di * tableDi\n        let MDi = Dr * tableDi + Di * tableDr\n\n        // Pre-Final values\n        let T0r = MAr + MCr\n        let T0i = MAi + MCi\n        let T1r = MAr - MCr\n        let T1i = MAi - MCi\n        let T2r = MBr + MDr\n        let T2i = MBi + MDi\n        let T3r = inv * (MBr - MDr)\n        let T3i = inv * (MBi - MDi)\n\n        // Final values\n        let FAr = T0r + T2r\n        let FAi = T0i + T2i\n\n        let FBr = T1r + T3i\n        let FBi = T1i - T3r\n\n        out[A] = FAr\n        out[A + 1] = FAi\n        out[B] = FBr\n        out[B + 1] = FBi\n\n        // Output final middle point\n        if (i === 0) {\n          let FCr = T0r - T2r\n          let FCi = T0i - T2i\n          out[C] = FCr\n          out[C + 1] = FCi\n          continue\n        }\n\n        // Do not overwrite ourselves\n        if (i === hquarterLen) { continue }\n\n        // In the flipped case:\n        // MAi = -MAi\n        // MBr=-MBi, MBi=-MBr\n        // MCr=-MCr\n        // MDr=MDi, MDi=MDr\n        let ST0r = T1r\n        let ST0i = -T1i\n        let ST1r = T0r\n        let ST1i = -T0i\n        let ST2r = -inv * T3i\n        let ST2i = -inv * T3r\n        let ST3r = -inv * T2i\n        let ST3i = -inv * T2r\n\n        let SFAr = ST0r + ST2r\n        let SFAi = ST0i + ST2i\n\n        let SFBr = ST1r + ST3i\n        let SFBi = ST1i - ST3r\n\n        let SA = outOff + quarterLen - i\n        let SB = outOff + halfLen - i\n\n        out[SA] = SFAr\n        out[SA + 1] = SFAi\n        out[SB] = SFBr\n        out[SB + 1] = SFBi\n      }\n    }\n  }\n}\n\n// radix-2 implementation\n//\n// NOTE: Only called for len=4\nFFT.prototype._singleRealTransform2 = function _singleRealTransform2(outOff,\n  off,\n  step) {\n  const out = this._out\n  const data = this._data\n\n  const evenR = data[off]\n  const oddR = data[off + step]\n\n  const leftR = evenR + oddR\n  const rightR = evenR - oddR\n\n  out[outOff] = leftR\n  out[outOff + 1] = 0\n  out[outOff + 2] = rightR\n  out[outOff + 3] = 0\n}\n\n// radix-4\n//\n// NOTE: Only called for len=8\nFFT.prototype._singleRealTransform4 = function _singleRealTransform4(outOff,\n  off,\n  step) {\n  const out = this._out\n  const data = this._data\n  const inv = this._inv ? -1 : 1\n  const step2 = step * 2\n  const step3 = step * 3\n\n  // Original values\n  const Ar = data[off]\n  const Br = data[off + step]\n  const Cr = data[off + step2]\n  const Dr = data[off + step3]\n\n  // Pre-Final values\n  const T0r = Ar + Cr\n  const T1r = Ar - Cr\n  const T2r = Br + Dr\n  const T3r = inv * (Br - Dr)\n\n  // Final values\n  const FAr = T0r + T2r\n\n  const FBr = T1r\n  const FBi = -T3r\n\n  const FCr = T0r - T2r\n\n  const FDr = T1r\n  const FDi = T3r\n\n  out[outOff] = FAr\n  out[outOff + 1] = 0\n  out[outOff + 2] = FBr\n  out[outOff + 3] = FBi\n  out[outOff + 4] = FCr\n  out[outOff + 5] = 0\n  out[outOff + 6] = FDr\n  out[outOff + 7] = FDi\n}\n\nexport default FFT\n"
  },
  {
    "path": "src/renderer/plugins/player/pitch-shifter/ola-processor.js",
    "content": "/* eslint-disable no-var */\n\nconst WEBAUDIO_BLOCK_SIZE = 128\n\n\n/** Overlap-Add Node */\nclass OLAProcessor extends globalThis.AudioWorkletProcessor {\n  constructor(options) {\n    super(options)\n\n    this.keepReturnTrue = true\n    this.processNow = false\n\n    this.nbInputs = options.numberOfInputs\n    this.nbOutputs = options.numberOfOutputs\n\n    this.blockSize = options.processorOptions.blockSize\n    // TODO for now, the only support hop size is the size of a web audio block\n    this.hopSize = WEBAUDIO_BLOCK_SIZE\n\n    this.nbOverlaps = this.blockSize / this.hopSize\n\n    this.lastSilencedHopCount = 0\n    this.nbOverlaps2x = this.nbOverlaps * 2\n    this.fakeEmptyInputs = [new Array(2).fill(new Float32Array(WEBAUDIO_BLOCK_SIZE))]\n\n\n    // pre-allocate input buffers (will be reallocated if needed)\n    this.inputBuffers = new Array(this.nbInputs)\n    this.inputBuffersHead = new Array(this.nbInputs)\n    this.inputBuffersToSend = new Array(this.nbInputs)\n    // assume 2 channels per input\n    for (var i = 0; i < this.nbInputs; i++) {\n      this.allocateInputChannels(i, 2)\n    }\n    // pre-allocate input buffers (will be reallocated if needed)\n    this.outputBuffers = new Array(this.nbOutputs)\n    this.outputBuffersToRetrieve = new Array(this.nbOutputs)\n    // assume 2 channels per output\n    for (i = 0; i < this.nbOutputs; i++) {\n      this.allocateOutputChannels(i, 2)\n    }\n\n    this.port.onmessage = (e) => this.keepReturnTrue = false\n  }\n\n  /** Handles dynamic reallocation of input/output channels buffer\n     (channel numbers may vary during lifecycle) **/\n  reallocateChannelsIfNeeded(inputs, outputs, force) {\n    for (var i = 0; i < this.nbInputs; i++) {\n      let nbChannels = inputs[i].length\n      if (force || (nbChannels != this.inputBuffers[i].length)) {\n        this.allocateInputChannels(i, nbChannels)\n        // console.log(\"reallocateChannelsIfNeeded\");\n      }\n    }\n\n    for (i = 0; i < this.nbOutputs; i++) {\n      let nbChannels = outputs[i].length\n      if (force || (nbChannels != this.outputBuffers[i].length)) {\n        this.allocateOutputChannels(i, nbChannels)\n        // console.log(\"reallocateChannelsIfNeeded\");\n      }\n    }\n  }\n\n  allocateInputChannels(inputIndex, nbChannels) {\n    // allocate input buffers\n    // console.log(\"allocateInputChannels\");\n\n    this.inputBuffers[inputIndex] = new Array(nbChannels)\n    for (var i = 0; i < nbChannels; i++) {\n      this.inputBuffers[inputIndex][i] = new Float32Array(this.blockSize + WEBAUDIO_BLOCK_SIZE)\n      this.inputBuffers[inputIndex][i].fill(0)\n    }\n\n    // allocate input buffers to send and head pointers to copy from\n    // (cannot directly send a pointer/subarray because input may be modified)\n    this.inputBuffersHead[inputIndex] = new Array(nbChannels)\n    this.inputBuffersToSend[inputIndex] = new Array(nbChannels)\n    for (i = 0; i < nbChannels; i++) {\n      this.inputBuffersHead[inputIndex][i] = this.inputBuffers[inputIndex][i].subarray(0, this.blockSize)\n      this.inputBuffersToSend[inputIndex][i] = new Float32Array(this.blockSize)\n    }\n  }\n\n  allocateOutputChannels(outputIndex, nbChannels) {\n    // allocate output buffers\n    this.outputBuffers[outputIndex] = new Array(nbChannels)\n    for (var i = 0; i < nbChannels; i++) {\n      this.outputBuffers[outputIndex][i] = new Float32Array(this.blockSize)\n      this.outputBuffers[outputIndex][i].fill(0)\n    }\n\n    // allocate output buffers to retrieve\n    // (cannot send a pointer/subarray because new output has to be add to existing output)\n    this.outputBuffersToRetrieve[outputIndex] = new Array(nbChannels)\n    for (i = 0; i < nbChannels; i++) {\n      this.outputBuffersToRetrieve[outputIndex][i] = new Float32Array(this.blockSize)\n      this.outputBuffersToRetrieve[outputIndex][i].fill(0)\n    }\n  }\n\n  checkForNotSilence(value) {\n    return value !== 0\n  }\n\n  /** Read next web audio block to input buffers **/\n  readInputs(inputs) {\n    // when playback is paused, we may stop receiving new samples\n    /* if (inputs[0].length && inputs[0][0].length == 0) {\n            for (var i = 0; i < this.nbInputs; i++) {\n                for (var j = 0; j < this.inputBuffers[i].length; j++) {\n                    this.inputBuffers[i][j].fill(0, this.blockSize);\n                }\n            }\n            return;\n        } */\n\n    for (let i = 0; i < this.nbInputs; i++) {\n      for (let j = 0; j < this.inputBuffers[i].length; j++) {\n        let webAudioBlock = inputs[i][j]\n        this.inputBuffers[i][j]?.set(webAudioBlock, this.blockSize)\n      }\n    }\n  }\n\n  /** Shift left content of input buffers to receive new web audio block **/\n  shiftInputBuffers() {\n    for (let i = 0; i < this.nbInputs; i++) {\n      for (let j = 0; j < this.inputBuffers[i].length; j++) {\n        this.inputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)\n      }\n    }\n  }\n\n  /** Copy contents of input buffers to buffer actually sent to process **/\n  prepareInputBuffersToSend() {\n    for (let i = 0; i < this.nbInputs; i++) {\n      for (let j = 0; j < this.inputBuffers[i].length; j++) {\n        this.inputBuffersToSend[i][j].set(this.inputBuffersHead[i][j])\n      }\n    }\n  }\n\n  /** Add contents of output buffers just processed to output buffers **/\n  handleOutputBuffersToRetrieve() {\n    for (let i = 0; i < this.nbOutputs; i++) {\n      for (let j = 0; j < this.outputBuffers[i].length; j++) {\n        for (let k = 0; k < this.blockSize; k++) {\n          this.outputBuffers[i][j][k] += this.outputBuffersToRetrieve[i][j][k] / this.nbOverlaps\n        }\n      }\n    }\n  }\n\n  /** Write next web audio block from output buffers **/\n  writeOutputs(outputs) {\n    for (let i = 0; i < this.nbInputs; i++) {\n      for (let j = 0; j < this.inputBuffers[i].length; j++) {\n        let webAudioBlock = this.outputBuffers[i][j].subarray(0, WEBAUDIO_BLOCK_SIZE)\n        outputs[i][j]?.set(webAudioBlock)\n      }\n    }\n  }\n\n  /** Shift left content of output buffers to receive new web audio block **/\n  shiftOutputBuffers() {\n    for (let i = 0; i < this.nbOutputs; i++) {\n      for (let j = 0; j < this.outputBuffers[i].length; j++) {\n        this.outputBuffers[i][j].copyWithin(0, WEBAUDIO_BLOCK_SIZE)\n        this.outputBuffers[i][j].subarray(this.blockSize - WEBAUDIO_BLOCK_SIZE).fill(0)\n      }\n    }\n  }\n\n  process(inputs, outputs, params) {\n    // console.log(inputs[0].length ? \"active\" : \"inactive\");\n    // this.reallocateChannelsIfNeeded(inputs, outputs);\n    // if (inputs[0][0].some(this.checkForNotSilence) || inputs[0][1].some(this.checkForNotSilence))\n    // console.log(inputs[0].length)\n    if (inputs[0].length < 2) {\n      // 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\n      // if (inputs[0][0].some(this.checkForNotSilence)) console.warn(\"single channel not silence exception!\");\n      if (this.lastSilencedHopCount < this.nbOverlaps2x) {\n        // ALLOW nbOverlaps2x BLOCKS OF SILENCE TO COME THROUGH TO ACCOMODATE LATENCY TAIL\n        this.lastSilencedHopCount++\n        inputs = this.fakeEmptyInputs\n        this.processNow = true\n      } else {\n        // console.warn(\"skipping processing\");\n        if (this.lastSilencedHopCount === this.nbOverlaps2x) {\n          this.lastSilencedHopCount++\n          this.reallocateChannelsIfNeeded(this.fakeEmptyInputs, outputs, true)\n          // console.warn(\"reallocateChannels\");\n        }\n        this.processNow = false // ENABLES SKIPPING UNNEEDED PROCESSING OF SILENT INPUT\n      }\n    } else {\n      if (this.lastSilencedHopCount) {\n        this.lastSilencedHopCount = 0\n        // this.reallocateChannelsIfNeeded(inputs, outputs, true);\n        // console.warn(\"reallocateChannels\");\n      }\n      this.processNow = true\n    }\n    if (this.processNow) {\n      this.readInputs(inputs)\n      this.shiftInputBuffers()\n      this.prepareInputBuffersToSend()\n      this.processOLA(this.inputBuffersToSend, this.outputBuffersToRetrieve, params)\n      this.handleOutputBuffersToRetrieve()\n      this.writeOutputs(outputs)\n      this.shiftOutputBuffers()\n    }\n    return this.keepReturnTrue\n  }\n\n  /* processOLA(inputs, outputs, params) {\n        console.assert(false, \"Not overriden\");\n    } */\n}\n\nexport default OLAProcessor\n"
  },
  {
    "path": "src/renderer/plugins/player/pitch-shifter/phase-vocoder.js",
    "content": "// https://github.com/olvb/phaze/issues/26#issuecomment-1573938170\n// https://github.com/olvb/phaze\nimport FFT from './fft'\nimport OLAProcessor from './ola-processor'\n\n\nconst DEFAULT_BUFFERED_BLOCK_SIZE = 4096\n\nfunction genHannWindow(length) {\n  let win = new Float32Array(length)\n  for (let i = 0; i < length; i++) {\n    win[i] = 0.8 * (1 - Math.cos(2 * Math.PI * i / length))\n  }\n  return win\n}\n\nclass PhaseVocoderProcessor extends OLAProcessor {\n  static get parameterDescriptors() {\n    return [{\n      name: 'pitchFactor',\n      defaultValue: 1.0,\n      automationRate: 'k-rate',\n    }, /* ,\n    {\n      name: 'pitchCents',\n      defaultValue: 0.0,\n      automationRate: 'k-rate'\n        } */]\n  }\n\n  constructor(options) {\n    (options.processorOptions ??= {}).blockSize ??= DEFAULT_BUFFERED_BLOCK_SIZE\n    super(options)\n\n    this.fftSize = this.blockSize\n    this.timeCursor = 0\n\n    this.hannWindow = genHannWindow(this.blockSize)\n\n    // prepare FFT and pre-allocate buffers\n    this.fft = new FFT(this.fftSize)\n    this.freqComplexBuffer = this.fft.createComplexArray()\n    this.freqComplexBufferShifted = this.fft.createComplexArray()\n    this.timeComplexBuffer = this.fft.createComplexArray()\n    this.magnitudes = new Float32Array(this.fftSize / 2 + 1)\n    this.peakIndexes = new Int32Array(this.magnitudes.length)\n    this.nbPeaks = 0\n  }\n\n  processOLA(inputs, outputs, parameters) {\n    // k-rate automation, param arrays only have single value\n    const pitchFactor = parameters.pitchFactor[0]/*  || Math.pow(2, (parameters.pitchCents[0]/12)) */\n\n    for (let i = 0; i < this.nbInputs; i++) {\n      for (let j = 0; j < inputs[i].length; j++) {\n        // big assumption here: output is symetric to input\n        let input = inputs[i][j]\n        let output = outputs[i][j]\n\n        this.applyHannWindow(input)\n\n        this.fft.realTransform(this.freqComplexBuffer, input)\n\n        this.computeMagnitudes()\n        this.findPeaks()\n        this.shiftPeaks(pitchFactor)\n\n        this.fft.completeSpectrum(this.freqComplexBufferShifted)\n        this.fft.inverseTransform(this.timeComplexBuffer, this.freqComplexBufferShifted)\n        this.fft.fromComplexArray(this.timeComplexBuffer, output)\n\n        this.applyHannWindow(output)\n      }\n    }\n\n    this.timeCursor += this.hopSize\n  }\n\n  /** Apply Hann window in-place */\n  applyHannWindow(input) {\n    for (let i = 0; i < this.blockSize; i++) {\n      input[i] = input[i] * this.hannWindow[i]\n    }\n  }\n\n  /** Compute squared magnitudes for peak finding **/\n  computeMagnitudes() {\n    let i = 0; let j = 0\n    while (i < this.magnitudes.length) {\n      let real = this.freqComplexBuffer[j]\n      let imag = this.freqComplexBuffer[j + 1]\n      // no need to sqrt for peak finding\n      this.magnitudes[i] = real ** 2 + imag ** 2\n      i += 1\n      j += 2\n    }\n  }\n\n  /** Find peaks in spectrum magnitudes **/\n  findPeaks() {\n    this.nbPeaks = 0\n    let i = 2\n    let end = this.magnitudes.length - 2\n\n    while (i < end) {\n      let mag = this.magnitudes[i]\n\n      if (this.magnitudes[i - 1] >= mag || this.magnitudes[i - 2] >= mag) {\n        i++\n        continue\n      }\n      if (this.magnitudes[i + 1] >= mag || this.magnitudes[i + 2] >= mag) {\n        i++\n        continue\n      }\n\n      this.peakIndexes[this.nbPeaks] = i\n      this.nbPeaks++\n      i += 2\n    }\n  }\n\n  /** Shift peaks and regions of influence by pitchFactor into new specturm */\n  shiftPeaks(pitchFactor) {\n    // zero-fill new spectrum\n    this.freqComplexBufferShifted.fill(0)\n\n    for (let i = 0; i < this.nbPeaks; i++) {\n      let peakIndex = this.peakIndexes[i]\n      let peakIndexShifted = Math.round(peakIndex * pitchFactor)\n\n      if (peakIndexShifted > this.magnitudes.length) break\n\n      // find region of influence\n      let startIndex = 0\n      let endIndex = this.fftSize\n      if (i > 0) {\n        startIndex = peakIndex - Math.floor((peakIndex - this.peakIndexes[i - 1]) / 2)\n      }\n      if (i < this.nbPeaks - 1) {\n        endIndex = peakIndex + Math.ceil((this.peakIndexes[i + 1] - peakIndex) / 2)\n      }\n\n      // shift whole region of influence around peak to shifted peak\n      let startOffset = startIndex - peakIndex\n      let endOffset = endIndex - peakIndex\n      for (let j = startOffset; j < endOffset; j++) {\n        let binIndex = peakIndex + j\n        let binIndexShifted = peakIndexShifted + j\n\n        if (binIndexShifted >= this.magnitudes.length) break\n\n        // apply phase correction\n        let omegaDelta = 2 * Math.PI * (binIndexShifted - binIndex) / this.fftSize\n        let phaseShiftReal = Math.cos(omegaDelta * this.timeCursor)\n        let phaseShiftImag = Math.sin(omegaDelta * this.timeCursor)\n\n        let indexReal = binIndex * 2\n        let indexImag = indexReal + 1\n        let valueReal = this.freqComplexBuffer[indexReal]\n        let valueImag = this.freqComplexBuffer[indexImag]\n\n        let valueShiftedReal = valueReal * phaseShiftReal - valueImag * phaseShiftImag\n        let valueShiftedImag = valueReal * phaseShiftImag + valueImag * phaseShiftReal\n\n        let indexShiftedReal = binIndexShifted * 2\n        let indexShiftedImag = indexShiftedReal + 1\n        this.freqComplexBufferShifted[indexShiftedReal] += valueShiftedReal\n        this.freqComplexBufferShifted[indexShiftedImag] += valueShiftedImag\n      }\n    }\n  }\n}\n\nglobalThis.registerProcessor('phase-vocoder-processor', PhaseVocoderProcessor)\n\n"
  },
  {
    "path": "src/renderer/router.ts",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\n// import Vue from 'vue'\nimport { createRouter, createWebHashHistory } from 'vue-router'\n\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes: [\n    {\n      path: '/search',\n      name: 'Search',\n      component: require('./views/Search/index.vue').default,\n      meta: {\n        name: 'Search',\n      },\n    },\n    {\n      path: '/songList/list',\n      name: 'SongList',\n      component: require('./views/songList/List/index.vue').default,\n      meta: {\n        name: 'SongList',\n      },\n    },\n    {\n      path: '/songList/detail',\n      name: 'SongListDetail',\n      component: require('./views/songList/Detail/index.vue').default,\n      meta: {\n        name: 'SongList',\n      },\n    },\n    {\n      path: '/leaderboard',\n      name: 'Leaderboard',\n      component: require('./views/Leaderboard/index.vue').default,\n      meta: {\n        name: 'Leaderboard',\n      },\n    },\n    {\n      path: '/list',\n      name: 'List',\n      component: require('./views/List/index.vue').default,\n      meta: {\n        name: 'List',\n      },\n    },\n    {\n      path: '/download',\n      name: 'Download',\n      component: require('./views/Download/index.vue').default,\n      meta: {\n        name: 'Download',\n      },\n    },\n    {\n      path: '/setting',\n      name: 'Setting',\n      component: require('./views/Setting/index.vue').default,\n      meta: {\n        name: 'Setting',\n      },\n    },\n    { path: '/:pathMatch(.*)*', redirect: '/search' },\n  ],\n  linkActiveClass: 'active-link',\n  linkExactActiveClass: 'exact-active-link',\n})\n\n\nexport default router\n"
  },
  {
    "path": "src/renderer/store/dislikeList/action.ts",
    "content": "import { markRaw } from '@common/utils/vueTools'\n\n\nimport { dislikeInfo, dislikeRuleCount } from './state'\nimport { SPLIT_CHAR } from '@common/constants'\n\n\nexport const hasDislike = (info: LX.Music.MusicInfo | LX.Download.ListItem) => {\n  if ('progress' in info) info = info.metadata.musicInfo\n  const name = info.name?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? ''\n  const singer = info.singer?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? ''\n\n  return dislikeInfo.musicNames.has(name) || dislikeInfo.singerNames.has(singer) ||\n    dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n}\n\nexport const initDislikeInfo = ({ musicNames, rules, names, singerNames }: LX.Dislike.DislikeInfo) => {\n  dislikeInfo.names = markRaw(names)\n  dislikeInfo.singerNames = markRaw(singerNames)\n  dislikeInfo.musicNames = markRaw(musicNames)\n  dislikeInfo.rules = rules\n  dislikeRuleCount.value = dislikeInfo.musicNames.size + dislikeInfo.singerNames.size + dislikeInfo.names.size\n}\n\nconst initNameSet = () => {\n  dislikeInfo.names.clear()\n  dislikeInfo.musicNames.clear()\n  dislikeInfo.singerNames.clear()\n  const list: string[] = []\n  for (const item of dislikeInfo.rules.split('\\n')) {\n    if (!item) continue\n    let [name, singer] = item.split(SPLIT_CHAR.DISLIKE_NAME)\n    if (name) {\n      name = name.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      if (singer) {\n        singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n        const rule = `${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`\n        dislikeInfo.names.add(rule)\n        list.push(rule)\n      } else {\n        dislikeInfo.musicNames.add(name)\n        list.push(name)\n      }\n    } else if (singer) {\n      singer = singer.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim()\n      dislikeInfo.singerNames.add(singer)\n      list.push(`${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n    }\n  }\n  dislikeInfo.rules = Array.from(new Set(list)).join('\\n')\n  dislikeRuleCount.value = dislikeInfo.musicNames.size + dislikeInfo.singerNames.size + dislikeInfo.names.size\n}\n\nexport const addDislikeInfo = (infos: LX.Dislike.DislikeMusicInfo[]) => {\n  dislikeInfo.rules += '\\n' + infos.map(info => `${info.name ?? ''}${SPLIT_CHAR.DISLIKE_NAME}${info.singer ?? ''}`).join('\\n')\n  initNameSet()\n  return dislikeInfo.rules\n}\n\nexport const overwirteDislikeInfo = (rules: string) => {\n  dislikeInfo.rules = rules\n  initNameSet()\n  return dislikeInfo.rules\n}\n\nexport const clearDislikeInfo = () => {\n  dislikeInfo.rules = ''\n  initNameSet()\n  return dislikeInfo.rules\n}\n\n\n// export const updateDislikeInfo = (info: LX.Dislike.ListItem) => {\n//   const targetInfo = dislikeInfo.list.find(i => i.id == info.id)\n//   if (!targetInfo) return\n//   targetInfo.name = info.name\n//   targetInfo.singer = info.singer\n//   initNameSet()\n// }\n\n// export const removeDislikeInfo = (ids: string[]) => {\n//   for (const id of ids) {\n//     dislikeInfo.list.splice(dislikeInfo.list.findIndex(info => info.id == id), 1)\n//   }\n//   initNameSet()\n// }\n\n// export const clearDislikeInfo = () => {\n//   dislikeInfo.rules = ''\n//   initNameSet()\n// }\n\n"
  },
  {
    "path": "src/renderer/store/dislikeList/index.ts",
    "content": "\nexport * as action from './action'\nexport * from './state'\n"
  },
  {
    "path": "src/renderer/store/dislikeList/state.ts",
    "content": "import { markRaw, ref } from '@common/utils/vueTools'\n\n// import { deduplicationList } from '@common/utils/renderer'\n\n\nexport const dislikeInfo: LX.Dislike.DislikeInfo = markRaw({\n  names: markRaw(new Set()),\n  musicNames: markRaw(new Set()),\n  singerNames: markRaw(new Set()),\n  rules: '',\n})\n\nexport const dislikeRuleCount = ref(0)\n"
  },
  {
    "path": "src/renderer/store/download/action.ts",
    "content": "import {\n  downloadTasksGet,\n  // downloadListClear,\n  downloadTasksCreate,\n  downloadTasksRemove,\n  downloadTasksUpdate,\n} from '@renderer/utils/ipc'\nimport {\n  downloadList,\n} from './state'\nimport { markRaw, toRaw } from '@common/utils/vueTools'\nimport { getMusicUrl, getPicUrl, getLyricInfo } from '@renderer/core/music/online'\nimport { appSetting } from '../setting'\nimport { qualityList } from '..'\nimport { proxyCallback } from '@renderer/worker/utils'\nimport { arrPush, arrUnshift, joinPath } from '@renderer/utils'\nimport { DOWNLOAD_STATUS } from '@common/constants'\nimport { proxy } from '../index'\nimport { buildSavePath } from './utils'\n\nconst waitingUpdateTasks = new Map<string, LX.Download.ListItem>()\nlet timer: NodeJS.Timeout | null = null\nconst throttleUpdateTask = (tasks: LX.Download.ListItem[]) => {\n  for (const task of tasks) waitingUpdateTasks.set(task.id, toRaw(task))\n  if (timer) return\n  timer = setTimeout(() => {\n    timer = null\n    void downloadTasksUpdate(Array.from(waitingUpdateTasks.values()))\n    waitingUpdateTasks.clear()\n  }, 100)\n}\n\nconst runingTask = new Map<string, LX.Download.ListItem>()\n\n// const initDownloadList = (list: LX.Download.ListItem[]) => {\n//   downloadList.splice(0, downloadList.length, ...list)\n// }\n\nexport const getDownloadList = async(): Promise<LX.Download.ListItem[]> => {\n  if (!downloadList.length) {\n    const list = await downloadTasksGet()\n    for (const downloadInfo of list) {\n      markRaw(downloadInfo.metadata)\n      switch (downloadInfo.status) {\n        case DOWNLOAD_STATUS.RUN:\n        case DOWNLOAD_STATUS.WAITING:\n          downloadInfo.status = DOWNLOAD_STATUS.PAUSE\n          downloadInfo.statusText = window.i18n.t('download___status_paused')\n        default:\n          break\n      }\n    }\n    arrPush(downloadList, list)\n  }\n  return downloadList\n}\n\nconst addTasks = async(list: LX.Download.ListItem[]) => {\n  const addMusicLocationType = appSetting['list.addMusicLocationType']\n\n  await downloadTasksCreate(list.map(i => toRaw(i)), addMusicLocationType)\n\n  if (addMusicLocationType === 'top') {\n    arrUnshift(downloadList, list)\n  } else {\n    arrPush(downloadList, list)\n  }\n  window.app_event.downloadListUpdate()\n}\n\nconst setStatusText = (downloadInfo: LX.Download.ListItem, text: string) => { // 设置状态文本\n  downloadInfo.statusText = text\n  throttleUpdateTask([downloadInfo])\n}\n\nconst setUrl = (downloadInfo: LX.Download.ListItem, url: string) => {\n  downloadInfo.metadata.url = url\n  throttleUpdateTask([downloadInfo])\n}\n\nconst updateFilePath = (downloadInfo: LX.Download.ListItem, filePath: string) => {\n  downloadInfo.metadata.filePath = filePath\n  throttleUpdateTask([downloadInfo])\n}\n\nconst setProgress = (downloadInfo: LX.Download.ListItem, progress: LX.Download.ProgressInfo) => {\n  downloadInfo.total = progress.total\n  downloadInfo.downloaded = progress.downloaded\n  downloadInfo.writeQueue = progress.writeQueue\n  if (progress.progress == 100) {\n    downloadInfo.speed = ''\n    downloadInfo.progress = 99.99\n    setStatusText(downloadInfo, window.i18n.t('download_status_write_queue', { num: progress.writeQueue }))\n  } else {\n    downloadInfo.speed = progress.speed\n    downloadInfo.progress = progress.progress\n  }\n  throttleUpdateTask([downloadInfo])\n}\n\nconst setStatus = (downloadInfo: LX.Download.ListItem, status: LX.Download.DownloadTaskStatus, statusText?: string) => { // 设置状态及状态文本\n  if (statusText == null) {\n    switch (status) {\n      case DOWNLOAD_STATUS.RUN:\n        statusText = window.i18n.t('download___status_running')\n        break\n      case DOWNLOAD_STATUS.WAITING:\n        statusText = window.i18n.t('download___status_waiting')\n        break\n      case DOWNLOAD_STATUS.PAUSE:\n        statusText = window.i18n.t('download___status_paused')\n        break\n      case DOWNLOAD_STATUS.ERROR:\n        statusText = window.i18n.t('download___status_error')\n        break\n      case DOWNLOAD_STATUS.COMPLETED:\n        statusText = window.i18n.t('download___status_completed')\n        break\n      default:\n        statusText = ''\n        break\n    }\n  }\n\n  if (downloadInfo.statusText == statusText && downloadInfo.status == status) return\n\n  if (status == DOWNLOAD_STATUS.COMPLETED) downloadInfo.isComplate = true\n  downloadInfo.statusText = statusText\n  downloadInfo.status = status\n  throttleUpdateTask([downloadInfo])\n}\n\n// 修复 1.1.x版本 酷狗源歌词格式\nconst fixKgLyric = (lrc: string) => /\\[00:\\d\\d:\\d\\d.\\d+\\]/.test(lrc) ? lrc.replace(/(?:\\[00:(\\d\\d:\\d\\d.\\d+\\]))/gm, '[$1') : lrc\n\nconst getProxy = () => {\n  return proxy.enable && proxy.host ? {\n    host: proxy.host,\n    port: parseInt(proxy.port || '80'),\n  } : proxy.envProxy ? {\n    host: proxy.envProxy.host,\n    port: parseInt(proxy.envProxy.port || '80'),\n  } : undefined\n}\n/**\n * 设置歌曲meta信息\n * @param downloadInfo 下载任务信息\n */\nconst saveMeta = (downloadInfo: LX.Download.ListItem) => {\n  if (downloadInfo.metadata.quality === 'ape') return\n  const isUseOtherSource = appSetting['download.isUseOtherSource']\n  const tasks: [Promise<string | null>, Promise<LX.Player.LyricInfo | null>] = [\n    appSetting['download.isEmbedPic']\n      ? downloadInfo.metadata.musicInfo.meta.picUrl\n        ? Promise.resolve(downloadInfo.metadata.musicInfo.meta.picUrl)\n        : getPicUrl({ musicInfo: downloadInfo.metadata.musicInfo, isRefresh: false, allowToggleSource: isUseOtherSource }).catch(err => {\n          console.log(err)\n          return null\n        })\n      : Promise.resolve(null),\n    appSetting['download.isEmbedLyric']\n      ? getLyricInfo({ musicInfo: downloadInfo.metadata.musicInfo, isRefresh: false, allowToggleSource: isUseOtherSource }).catch(err => {\n        console.log(err)\n        return null\n      })\n      : Promise.resolve(null),\n  ]\n  void Promise.all(tasks).then(([imgUrl, lyrics]) => {\n    const info = {\n      filePath: downloadInfo.metadata.filePath,\n      isEmbedLyricLx: appSetting['download.isEmbedLyricLx'],\n      isEmbedLyricT: appSetting['download.isEmbedLyricT'],\n      isEmbedLyricR: appSetting['download.isEmbedLyricR'],\n      title: downloadInfo.metadata.musicInfo.name,\n      artist: downloadInfo.metadata.musicInfo.singer?.replaceAll('、', ';'),\n      album: downloadInfo.metadata.musicInfo.meta.albumName,\n      APIC: imgUrl,\n    }\n    void window.lx.worker.download.writeMeta(info, lyrics ?? { lyric: '' }, getProxy())\n  })\n}\n\n/**\n * 保存歌词文件\n * @param downloadInfo 下载任务信息\n */\nconst downloadLyric = (downloadInfo: LX.Download.ListItem) => {\n  if (!appSetting['download.isDownloadLrc']) return\n  void getLyricInfo({\n    musicInfo: downloadInfo.metadata.musicInfo,\n    isRefresh: false,\n    allowToggleSource: appSetting['download.isUseOtherSource'],\n  }).then(lrcs => {\n    if (lrcs.lyric) {\n      lrcs.lyric = fixKgLyric(lrcs.lyric)\n      const info = {\n        filePath: downloadInfo.metadata.filePath.substring(0, downloadInfo.metadata.filePath.lastIndexOf('.')) + '.lrc',\n        format: appSetting['download.lrcFormat'],\n        downloadLxlrc: appSetting['download.isDownloadLxLrc'],\n        downloadTlrc: appSetting['download.isDownloadTLrc'],\n        downloadRlrc: appSetting['download.isDownloadRLrc'],\n      }\n      void window.lx.worker.download.saveLrc(lrcs, info)\n    }\n  })\n}\n\nconst getUrl = async(downloadInfo: LX.Download.ListItem, isRefresh: boolean = false) => {\n  let toggleMusicInfo = downloadInfo.metadata.musicInfo.meta.toggleMusicInfo\n  return (toggleMusicInfo ? getMusicUrl({\n    musicInfo: toggleMusicInfo,\n    isRefresh,\n    quality: downloadInfo.metadata.quality,\n    allowToggleSource: false,\n  }) : Promise.reject(new Error('not found'))).catch(() => {\n    return getMusicUrl({\n      musicInfo: downloadInfo.metadata.musicInfo,\n      isRefresh: false,\n      quality: downloadInfo.metadata.quality,\n      allowToggleSource: appSetting['download.isUseOtherSource'],\n    })\n  }).catch(() => '')\n}\nconst handleRefreshUrl = (downloadInfo: LX.Download.ListItem) => {\n  setStatusText(downloadInfo, window.i18n.t('download_status_error_refresh_url'))\n  let toggleMusicInfo = downloadInfo.metadata.musicInfo.meta.toggleMusicInfo\n  ;(toggleMusicInfo ? getMusicUrl({\n    musicInfo: toggleMusicInfo,\n    isRefresh: true,\n    quality: downloadInfo.metadata.quality,\n    allowToggleSource: false,\n  }) : Promise.reject(new Error('not found'))).catch(() => {\n    return getMusicUrl({\n      musicInfo: downloadInfo.metadata.musicInfo,\n      isRefresh: true,\n      quality: downloadInfo.metadata.quality,\n      allowToggleSource: appSetting['download.isUseOtherSource'],\n    })\n  })\n    .catch(() => '')\n    .then(url => {\n    // commit('setStatusText', { downloadInfo, text: '链接刷新成功' })\n      setUrl(downloadInfo, url)\n      void window.lx.worker.download.updateUrl(downloadInfo.id, url)\n    })\n    .catch(err => {\n      console.log(err)\n      handleError(downloadInfo, err.message)\n    })\n}\nconst handleError = (downloadInfo: LX.Download.ListItem, message?: string) => {\n  setStatus(downloadInfo, DOWNLOAD_STATUS.ERROR, message)\n  void window.lx.worker.download.removeTask(downloadInfo.id)\n  runingTask.delete(downloadInfo.id)\n  void checkStartTask()\n}\n\nconst handleStartTask = async(downloadInfo: LX.Download.ListItem) => {\n  if (!downloadInfo.metadata.url) {\n    setStatusText(downloadInfo, window.i18n.t('download_status_url_getting'))\n    const url = await getUrl(downloadInfo)\n    if (!url) {\n      handleError(downloadInfo, window.i18n.t('download_status_error_url_failed'))\n      return\n    }\n    setUrl(downloadInfo, url)\n    if (downloadInfo.status != DOWNLOAD_STATUS.RUN) return\n  }\n\n  const savePath = buildSavePath(downloadInfo)\n  const filePath = joinPath(savePath, downloadInfo.metadata.fileName)\n  if (downloadInfo.metadata.filePath != filePath) updateFilePath(downloadInfo, filePath)\n\n  setStatusText(downloadInfo, window.i18n.t('download_status_start'))\n\n  await window.lx.worker.download.startTask(toRaw(downloadInfo), savePath, appSetting['download.skipExistFile'], proxyCallback((event: LX.Download.DownloadTaskActions) => {\n    // console.log(event)\n    switch (event.action) {\n      case 'start':\n        setStatus(downloadInfo, DOWNLOAD_STATUS.RUN)\n        break\n      case 'complete':\n        downloadInfo.progress = 100\n        saveMeta(downloadInfo)\n        downloadLyric(downloadInfo)\n        void window.lx.worker.download.removeTask(downloadInfo.id)\n        runingTask.delete(downloadInfo.id)\n        setStatus(downloadInfo, DOWNLOAD_STATUS.COMPLETED)\n        void checkStartTask()\n        break\n      case 'refreshUrl':\n        handleRefreshUrl(downloadInfo)\n        break\n      case 'statusText':\n        setStatusText(downloadInfo, event.data)\n        break\n      case 'progress':\n        setProgress(downloadInfo, event.data)\n        break\n      case 'error':\n        handleError(downloadInfo, event.data.error\n          ? window.i18n.t(event.data.error) + (event.data.message ?? '')\n          : event.data.message,\n        )\n        break\n      default:\n        break\n    }\n  }), getProxy())\n}\nconst startTask = async(downloadInfo: LX.Download.ListItem) => {\n  setStatus(downloadInfo, DOWNLOAD_STATUS.RUN)\n  runingTask.set(downloadInfo.id, downloadInfo)\n  void handleStartTask(downloadInfo)\n}\n\nconst getStartTask = (list: LX.Download.ListItem[]): LX.Download.ListItem | null => {\n  let downloadCount = 0\n  const waitList = list.filter(item => {\n    if (item.status == DOWNLOAD_STATUS.WAITING) return true\n    if (item.status == DOWNLOAD_STATUS.RUN) ++downloadCount\n    return false\n  })\n  // console.log(downloadCount, waitList)\n  return downloadCount < appSetting['download.maxDownloadNum'] ? waitList.shift() ?? null : null\n}\n\nconst checkStartTask = async() => {\n  if (runingTask.size >= appSetting['download.maxDownloadNum']) return\n  let result = getStartTask(downloadList)\n  // console.log(result)\n  while (result) {\n    await startTask(result)\n    result = getStartTask(downloadList)\n  }\n}\n\n/**\n * 过滤重复任务\n * @param list\n */\nconst filterTask = (list: LX.Download.ListItem[]) => {\n  const set = new Set<string>()\n  for (const item of downloadList) set.add(item.id)\n  return list.filter(item => {\n    if (set.has(item.id)) return false\n    markRaw(item.metadata)\n    set.add(item.id)\n    return true\n  })\n}\n/**\n * 创建下载任务\n * @param list 要下载的歌曲\n * @param quality 下载音质\n */\nexport const createDownloadTasks = async(list: LX.Music.MusicInfoOnline[], quality: LX.Quality, listId?: string) => {\n  if (!list.length) return\n  const tasks = filterTask(await window.lx.worker.download.createDownloadTasks(list, quality,\n    appSetting['download.fileName'],\n    toRaw(qualityList.value), listId),\n  )\n\n  if (tasks.length) await addTasks(tasks)\n  void checkStartTask()\n}\n\n/**\n * 开始下载任务\n * @param list\n */\nexport const startDownloadTasks = async(list: LX.Download.ListItem[]) => {\n  for (const downloadInfo of list) {\n    switch (downloadInfo.status) {\n      case DOWNLOAD_STATUS.PAUSE:\n      case DOWNLOAD_STATUS.ERROR:\n        if (runingTask.size < appSetting['download.maxDownloadNum']) void startTask(downloadInfo)\n        else setStatus(downloadInfo, DOWNLOAD_STATUS.WAITING)\n      default:\n        break\n    }\n  }\n  void checkStartTask()\n}\n\n/**\n * 暂停下载任务\n * @param list\n */\nexport const pauseDownloadTasks = async(list: LX.Download.ListItem[]) => {\n  for (const downloadInfo of list) {\n    switch (downloadInfo.status) {\n      case DOWNLOAD_STATUS.RUN:\n        void window.lx.worker.download.pauseTask(downloadInfo.id)\n        runingTask.delete(downloadInfo.id)\n      case DOWNLOAD_STATUS.WAITING:\n      case DOWNLOAD_STATUS.ERROR:\n        setStatus(downloadInfo, DOWNLOAD_STATUS.PAUSE)\n      default:\n        break\n    }\n  }\n  void checkStartTask()\n}\n\n/**\n * 移除下载任务\n * @param ids 要移除的任务Id\n */\nexport const removeDownloadTasks = async(ids: string[]) => {\n  await downloadTasksRemove(ids)\n\n  const idsSet = new Set<string>(ids)\n  const newList = downloadList.filter(task => {\n    if (runingTask.has(task.id)) {\n      void window.lx.worker.download.removeTask(task.id)\n      runingTask.delete(task.id)\n    }\n    return !idsSet.has(task.id)\n  })\n  downloadList.splice(0, downloadList.length)\n  arrPush(downloadList, newList)\n\n\n  void checkStartTask()\n  window.app_event.downloadListUpdate()\n}\n"
  },
  {
    "path": "src/renderer/store/download/state.ts",
    "content": "import { reactive, ref, markRaw } from '@common/utils/vueTools'\nimport { DOWNLOAD_STATUS } from '@common/constants'\n\nexport const isInitedList = ref(false)\n\nexport const setInited = () => {\n  isInitedList.value = true\n}\n\nexport const downloadList = reactive<LX.Download.ListItem[]>([])\n// export const downloadListMap = new Map<string, LX.Download.ListItem>()\n\nexport const downloadStatus = markRaw(DOWNLOAD_STATUS)\n"
  },
  {
    "path": "src/renderer/store/download/utils.ts",
    "content": "import { appSetting } from '@renderer/store/setting'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/listManage'\nimport { filterFileName } from '@common/utils/common'\nimport { clipFileNameLength } from '@common/utils/tools'\nimport { joinPath } from '@common/utils/nodejs'\n\nexport const buildSavePath = (musicInfo: LX.Download.ListItem) => {\n  let savePath = appSetting['download.savePath']\n  if (appSetting['download.isSavePathGroupByListName']) {\n    let dirName: string | undefined\n    const listId = musicInfo.metadata.listId\n    switch (listId) {\n      case defaultList.id:\n        dirName = window.i18n.t(defaultList.name)\n        break\n      case loveList.id:\n        dirName = window.i18n.t(loveList.name)\n        break\n      default:\n        dirName = userLists.find(list => list.id === listId)?.name\n        break\n    }\n    if (dirName) dirName = filterFileName(dirName)\n    savePath = joinPath(savePath, clipFileNameLength(dirName ?? window.i18n.t(defaultList.name)))\n  }\n  return savePath\n}\n"
  },
  {
    "path": "src/renderer/store/hotSearch.ts",
    "content": "import { reactive, markRaw } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\n\n// import { deduplicationList } from '@common/utils/renderer'\n\nexport type Source = LX.OnlineSource | 'all'\n\ninterface SourceLists extends Partial<Record<LX.OnlineSource, string[]>> {\n  'all': string[]\n}\n\nexport const sources: Source[] = markRaw([])\n\nexport const sourceList: SourceLists = markRaw({\n  all: reactive<string[]>([]),\n})\n\n\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.hotSearch) continue\n  sources.push(source.id as LX.OnlineSource)\n  sourceList[source.id as LX.OnlineSource] = reactive<string[]>([])\n}\nsources.push('all')\n\n\nconst setList = (source: LX.OnlineSource, list: string[]): string[] => {\n  return sourceList[source] = list.slice(0, 20)\n}\n\nconst setLists = (lists: Array<{ source: LX.OnlineSource, list: string[] }>): string[] => {\n  let wordsMap = new Map<string, number>()\n  for (const { source, list } of lists) {\n    if (!sourceList[source]?.length) sourceList[source] = list.slice(0, 20)\n    for (let item of list) {\n      item = item.trim()\n      wordsMap.set(item, (wordsMap.get(item) ?? 0) + 1)\n    }\n  }\n  const wordsMapArr = Array.from(wordsMap)\n  wordsMapArr.sort((a, b) => a[0].localeCompare(b[0]))\n  wordsMapArr.sort((a, b) => b[1] - a[1])\n  const words = wordsMapArr.map(item => item[0])\n  return sourceList.all = words.slice(0, sources.length * 10)\n}\n\nexport const getList = async(source: Source): Promise<string[]> => {\n  if (source == 'all') {\n    let task = []\n    for (const source of sources) {\n      if (source == 'all') continue\n      task.push(\n        sourceList[source]?.length\n          ? Promise.resolve({ source, list: sourceList[source] })\n          : (music[source]?.hotSearch.getList() ?? Promise.reject(new Error('source not found: ' + source))).catch((err: any) => {\n              console.log(err)\n              return { source, list: [] }\n            }),\n      )\n    }\n    return Promise.all(task).then((results: any[]) => {\n      return setLists(results)\n    })\n  } else {\n    if (sourceList[source]?.length) return Promise.resolve(sourceList[source])\n    if (!music[source]?.hotSearch) {\n      setList(source, [])\n      return Promise.resolve([])\n    }\n    return music[source]?.hotSearch.getList().then(data => setList(source, data.list))\n  }\n}\n\n\nexport const clearList = (source: Source) => {\n  sourceList[source] = []\n}\n"
  },
  {
    "path": "src/renderer/store/index.ts",
    "content": "import { ref, reactive, shallowRef, markRaw, computed, watch } from '@common/utils/vueTools'\nimport { windowSizeList as configWindowSizeList } from '@common/config'\nimport { appSetting } from './setting'\nimport pkg from '../../../package.json'\nimport { type ProgressInfo } from 'electron-updater'\nimport music from '@renderer/utils/musicSdk'\nprocess.versions.app = pkg.version\n\nexport const apiSource = ref<string | null>(null)\nexport const proxy: {\n  enable: boolean\n  host: string\n  port: string\n\n  envProxy?: {\n    host: string\n    port: string\n  }\n} = {\n  enable: false,\n  host: '',\n  port: '',\n}\nexport const sync: {\n  enable: boolean\n  mode: LX.AppSetting['sync.mode']\n  isShowSyncMode: boolean\n  isShowAuthCodeModal: boolean\n  deviceName: string\n  type: keyof LX.Sync.ModeTypes\n  server: {\n    port: string\n    status: {\n      status: boolean\n      message: string\n      address: string[]\n      code: string\n      devices: LX.Sync.ServerKeyInfo[]\n    }\n  }\n  client: {\n    host: string\n    status: {\n      status: boolean\n      message: string\n      address: string[]\n    }\n  }\n} = reactive({\n  enable: false,\n  mode: 'server',\n  isShowSyncMode: false,\n  isShowAuthCodeModal: false,\n  deviceName: '',\n  type: 'list',\n  server: {\n    port: '',\n    status: {\n      status: false,\n      message: '',\n      address: [],\n      code: '',\n      devices: [],\n    },\n  },\n  client: {\n    host: '',\n    status: {\n      status: false,\n      message: '',\n      address: [],\n    },\n  },\n})\n\nexport const openAPI = reactive({\n  address: '',\n  message: '',\n})\n\n\nexport const windowSizeActive = computed(() => {\n  return windowSizeList.find(i => i.id === appSetting['common.windowSizeId']) ?? windowSizeList[0]\n})\n\nexport const getSourceI18nPrefix = () => {\n  return appSetting['common.sourceNameType'] == 'real' ? 'source_' : 'source_alias_'\n}\n\nexport const sourceNames = computed(() => {\n  const prefix = getSourceI18nPrefix()\n  const sourceNames: Record<LX.OnlineSource | 'all', string> = {\n    kw: 'kw',\n    tx: 'tx',\n    kg: 'kg',\n    mg: 'mg',\n    wy: 'wy',\n    all: window.i18n.t(prefix + 'all' as any),\n  }\n  for (const { id } of music.sources) {\n    sourceNames[id as LX.OnlineSource] = window.i18n.t(prefix + id as any)\n  }\n\n  return sourceNames\n})\n\nexport const windowSizeList = markRaw(configWindowSizeList)\n\nexport const isShowPact = ref(false)\n\nexport const versionInfo = window.lxData.versionInfo = reactive<{\n  version: string\n  newVersion: {\n    version: string\n    desc: string\n    history?: LX.VersionInfo[]\n  } | null\n  showModal: boolean\n  isUnknown: boolean\n  isLatest: boolean\n  reCheck: boolean\n  status: LX.UpdateStatus\n  downloadProgress: ProgressInfo | null\n}>({\n  version: pkg.version,\n  newVersion: null,\n  showModal: false,\n  reCheck: false,\n  isUnknown: false,\n  isLatest: false,\n  status: 'checking',\n  downloadProgress: null,\n})\nexport const userApi = reactive<{\n  list: LX.UserApi.UserApiInfo[]\n  status: boolean\n  message?: string\n  apis: Partial<LX.UserApi.UserApiSources>\n}>({\n  list: [],\n  status: false,\n  message: 'initing',\n  apis: {},\n})\n\nexport const isShowChangeLog = ref(false)\n\n\nexport const isFullscreen = ref(false)\nwatch(isFullscreen, isFullscreen => {\n  window.lx.rootOffset = window.dt || isFullscreen ? 0 : 8\n}, { immediate: true })\n\nexport const themeShouldUseDarkColors = ref(window.shouldUseDarkColors)\n\n\nexport const qualityList = shallowRef<LX.QualityList>({})\nexport const setQualityList = (_qualityList: LX.QualityList) => {\n  qualityList.value = _qualityList\n}\n\nexport const themeId = ref('green')\nexport const themeInfo: LX.ThemeInfo = {\n  themes: [],\n  userThemes: [],\n  dataPath: '',\n}\n"
  },
  {
    "path": "src/renderer/store/leaderboard/action.ts",
    "content": "// import { getLeaderboardSetting } from '@renderer/utils/data'\nimport { deduplicationList, toNewMusicInfo } from '@renderer/utils'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { markRaw, markRawList } from '@common/utils/vueTools'\nimport { boards, type Board, listDetailInfo, type ListDetailInfo } from './state'\n\nconst cache = new Map<string, any>()\n\nexport const setBoard = (board: Board, source: LX.OnlineSource) => {\n  boards[source] = markRaw(board)\n}\n\nexport const setListDetail = (result: ListDetailInfo, id: string, page: number) => {\n  listDetailInfo.list = markRaw([...result.list])\n  listDetailInfo.id = id\n  listDetailInfo.source = result.source\n  if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total\n  else listDetailInfo.total = result.limit * page\n  listDetailInfo.limit = result.limit\n  listDetailInfo.page = page\n\n  if (result.list.length) listDetailInfo.noItemLabel = ''\n  else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item')\n}\nexport const clearListDetail = () => {\n  listDetailInfo.list = []\n  listDetailInfo.id = ''\n  listDetailInfo.source = null\n  listDetailInfo.total = 0\n  listDetailInfo.limit = 30\n  listDetailInfo.page = 1\n  listDetailInfo.key = null\n  listDetailInfo.noItemLabel = ''\n}\n\nexport const getBoardsList = async(source: LX.OnlineSource) => {\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  return musicSdk[source]?.leaderboard.getBoards() as Promise<Board>\n}\n\n/**\n * 获取排行榜内单页歌曲\n * @param id 排行榜id  {souce}__{id}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetail = async(id: string, page: number, isRefresh = false): Promise<ListDetailInfo> => {\n  // let [source, bangId] = tabId.split('__')\n  // if (!bangId) return\n  let key = `${id}__${page}`\n\n  if (!isRefresh && cache.has(key)) return cache.get(key)\n\n  const [source, bangId] = id.split('__') as [LX.OnlineSource, string]\n\n  return musicSdk[source]?.leaderboard?.getList(bangId, page).then((result: ListDetailInfo) => {\n    result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[]))\n    cache.set(key, result)\n    return result\n  })\n}\n\n\n/**\n * 获取排行榜内全部歌曲\n * @param id 排行榜id  {souce}__{id}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetailAll = async(id: string, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {\n  const [source, bangId] = id.split('__') as [LX.OnlineSource, string]\n  // console.log(source, id)\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  const loadData = async(id: string, page: number): Promise<ListDetailInfo> => {\n    let key = `${source}__${id}__${page}`\n    if (!isRefresh && cache.has(key)) return cache.get(key)\n\n    return musicSdk[source]?.leaderboard.getList(id, page).then((result: ListDetailInfo) => {\n      result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[]))\n      cache.set(key, result)\n      return result\n    }) ?? Promise.reject(new Error('source not found' + source))\n  }\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return loadData(bangId, 1).then((result: ListDetailInfo) => {\n    if (result.total <= result.limit) return result.list\n\n    let maxPage = Math.ceil(result.total / result.limit)\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    const loadDetail = (loadPage = 2): Promise<ListDetailInfo['list']> => {\n      return loadPage == maxPage\n        ? loadData(bangId, loadPage).then((result: ListDetailInfo) => result.list)\n        // eslint-disable-next-line @typescript-eslint/promise-function-async\n        : loadData(bangId, loadPage).then((result1: ListDetailInfo) => loadDetail(++loadPage).then((result2: ListDetailInfo['list']) => [...result1.list, ...result2]))\n    }\n    return loadDetail().then(result2 => [...result.list, ...result2])\n  }).then((list: ListDetailInfo['list']) => deduplicationList(list))\n}\n\n\n/**\n * 获取并设置排行榜内单页歌曲\n * @param id 排行榜id  {souce}__{id}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getAndSetListDetail = async(id: string, page: number, isRefresh = false) => {\n  // let [source, bangId] = tabId.split('__')\n  // if (!bangId) return\n  let key = `${id}__${page}`\n\n  if (!isRefresh && listDetailInfo.key == key && listDetailInfo.list.length) return\n\n  listDetailInfo.key = key\n  listDetailInfo.noItemLabel = window.i18n.t('list__loading')\n\n  return getListDetail(id, page, isRefresh).then((result: ListDetailInfo) => {\n    if (key != listDetailInfo.key) return\n    setListDetail(result, id, page)\n  }).catch((error: any) => {\n    clearListDetail()\n    listDetailInfo.noItemLabel = window.i18n.t('list__load_failed')\n    console.log(error)\n    throw error\n  })\n}\n"
  },
  {
    "path": "src/renderer/store/leaderboard/state.ts",
    "content": "import { reactive, markRaw, shallowReactive } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\n\nexport type Source = LX.OnlineSource\n\nexport const sources: LX.OnlineSource[] = markRaw([])\n\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.leaderboard?.getBoards) continue\n  sources.push(source.id as LX.OnlineSource)\n}\n\nexport interface BoardItem {\n  id: string\n  name: string\n  bangid: string\n}\nexport interface Board {\n  list: BoardItem[]\n  source: LX.OnlineSource\n}\ntype Boards = Partial<Record<LX.OnlineSource, Board>>\n\nexport const boards = shallowReactive<Boards>({})\n\nexport interface ListDetailInfo {\n  list: LX.Music.MusicInfoOnline[]\n  total: number\n  page: number\n  source: LX.OnlineSource | null\n  limit: number\n  key: string | null\n  id: string\n  noItemLabel: string\n}\n\nexport const listDetailInfo = reactive<ListDetailInfo>({\n  list: [],\n  total: 0,\n  page: 1,\n  limit: 30,\n  key: null,\n  source: null,\n  id: '',\n  noItemLabel: '',\n})\n\n"
  },
  {
    "path": "src/renderer/store/list/action.ts",
    "content": "// import {  } from '@renderer/utils/ipc'\n\nimport { appSetting } from '@renderer/store/setting'\nimport { fetchingListStatus, listUpdateTimes, allMusicList, userLists, tempListMeta } from './state'\nimport {\n  registerListAction,\n  createUserList as createUserListAction,\n  addListMusics as addListMusicsAction,\n  moveListMusics as moveListMusicsAction,\n  overwriteListMusics,\n} from '@renderer/store/list/listManage'\nimport { toRaw } from '@common/utils/vueTools'\nimport { LIST_IDS } from '@common/constants'\n\nexport const registerAction = (onListChanged: (listIds: string[]) => void) => {\n  return registerListAction(appSetting, onListChanged)\n}\n\n/**\n * 从缓存获取列表内歌曲，前提是知道列表之前已被获取过，否则返回空数组\n * @param listId 列表ID\n * @returns\n */\nexport const getListMusicsFromCache = (listId: string | null): LX.Music.MusicInfo[] => {\n  if (!listId) return []\n  if (allMusicList.has(listId)) return allMusicList.get(listId) as LX.Music.MusicInfo[]\n  return []\n}\n\nexport const setFetchingListStatus = (id: string, status: boolean) => {\n  fetchingListStatus[id] = status\n}\n\nexport const setUpdateTime = (id: string, time: string) => {\n  listUpdateTimes[id] = time\n}\n\nexport const addListMusics = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType?: LX.AddMusicLocationType) => {\n  return addListMusicsAction({\n    id,\n    musicInfos: toRaw(musicInfos),\n    addMusicLocationType: addMusicLocationType ?? appSetting['list.addMusicLocationType'],\n  })\n}\n\nexport const moveListMusics = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType?: LX.AddMusicLocationType) => {\n  return moveListMusicsAction({\n    fromId,\n    toId,\n    musicInfos: toRaw(musicInfos),\n    addMusicLocationType: addMusicLocationType ?? appSetting['list.addMusicLocationType'],\n  })\n}\n\nexport const createUserList = async({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position = -1 }: {\n  name?: string\n  id?: string\n  list?: LX.Music.MusicInfo[]\n  source?: LX.OnlineSource\n  sourceListId?: string\n  position?: number\n}) => {\n  await createUserListAction({\n    position: position < 0 ? userLists.length : position,\n    listInfos: [\n      {\n        id,\n        name: name ?? 'list',\n        source,\n        sourceListId,\n        locationUpdateTime: position < 0 ? null : Date.now(),\n      },\n    ],\n  })\n  if (list) await addListMusics(id, list)\n}\n\n\nexport const setTempList = async(id: string, list: LX.Music.MusicInfoOnline[]) => {\n  tempListMeta.id = id\n  await overwriteListMusics({\n    listId: LIST_IDS.TEMP,\n    musicInfos: list,\n  })\n}\n\nexport {\n  addListMusicsAction,\n  moveListMusicsAction,\n}\n\nexport {\n  getUserLists,\n  removeUserList,\n  updateUserList,\n  updateUserListPosition,\n  getListMusics,\n  removeListMusics,\n  updateListMusics,\n  updateListMusicsPosition,\n  overwriteListMusics,\n  clearListMusics,\n  overwriteListFull,\n  checkListExistMusic,\n  getMusicExistListIds,\n} from '@renderer/store/list/listManage'\n"
  },
  {
    "path": "src/renderer/store/list/listManage/action.ts",
    "content": "import { markRaw, markRawList, toRaw } from '@common/utils/vueTools'\nimport {\n  allMusicList,\n  defaultList,\n  loveList,\n  tempList,\n  userLists,\n} from './state'\nimport { overwriteListPosition, overwriteListUpdateInfo, removeListPosition, removeListUpdateInfo } from '@renderer/utils/data'\nimport { LIST_IDS } from '@common/constants'\nimport { arrPush, arrUnshift } from '@common/utils/common'\n\nexport const setUserLists = (lists: LX.List.UserListInfo[]) => {\n  userLists.splice(0, userLists.length, ...lists)\n  return userLists\n}\n\nexport const setMusicList = (listId: string, musicList: LX.Music.MusicInfo[]) => {\n  const list = markRawList(musicList)\n  allMusicList.set(listId, list)\n  return list\n}\n\nconst overwriteMusicList = (id: string, list: LX.Music.MusicInfo[]) => {\n  // console.log(id, list)\n  markRawList(list)\n  let targetList = allMusicList.get(id)\n  if (targetList) {\n    targetList.splice(0, targetList.length)\n    arrPush(targetList, list)\n  } else {\n    allMusicList.set(id, list)\n  }\n}\nconst removeMusicList = (id: string) => {\n  allMusicList.delete(id)\n}\n\nconst createUserList = ({\n  name,\n  id,\n  source,\n  sourceListId,\n  locationUpdateTime,\n}: LX.List.UserListInfo, position: number) => {\n  if (position < 0 || position >= userLists.length) {\n    userLists.push({\n      name,\n      id,\n      source,\n      sourceListId,\n      locationUpdateTime,\n    })\n  } else {\n    userLists.splice(position, 0, {\n      name,\n      id,\n      source,\n      sourceListId,\n      locationUpdateTime,\n    })\n  }\n}\n\nconst updateList = ({\n  name,\n  id,\n  source,\n  sourceListId,\n  meta,\n  locationUpdateTime,\n}: LX.List.UserListInfo & { meta?: { id?: string } }) => {\n  let targetList\n  switch (id) {\n    case defaultList.id:\n    case loveList.id:\n      break\n    case tempList.id:\n      tempList.meta = meta ?? {}\n      break\n    default:\n      targetList = userLists.find(l => l.id == id)\n      if (!targetList) return\n      targetList.name = name\n      targetList.source = source\n      targetList.sourceListId = sourceListId\n      targetList.locationUpdateTime = locationUpdateTime\n      break\n  }\n}\n\nconst removeUserList = (id: string) => {\n  const index = userLists.findIndex(l => l.id == id)\n  if (index < 0) return\n  userLists.splice(index, 1)\n  // removeMusicList(id)\n}\n\nconst overwriteUserList = (lists: LX.List.UserListInfo[]) => {\n  userLists.splice(0, userLists.length, ...lists)\n}\n\n\n// const sendMyListUpdateEvent = (ids: string[]) => {\n//   window.app_event.myListUpdate(ids)\n// }\n\n\nexport const listDataOverwrite = ({ defaultList, loveList, userList, tempList }: MakeOptional<LX.List.ListDataFull, 'tempList'>): string[] => {\n  const updatedListIds: string[] = []\n  const newUserIds: string[] = []\n  const newUserListInfos = userList.map(({ list, ...listInfo }) => {\n    newUserIds.push(listInfo.id)\n    if (allMusicList.has(listInfo.id)) {\n      overwriteMusicList(listInfo.id, list)\n      updatedListIds.push(listInfo.id)\n    }\n    return listInfo\n  })\n  for (const list of userLists) {\n    if (!allMusicList.has(list.id) || newUserIds.includes(list.id)) continue\n    removeMusicList(list.id)\n    updatedListIds.push(list.id)\n  }\n  overwriteUserList(newUserListInfos)\n\n  if (allMusicList.has(LIST_IDS.DEFAULT)) {\n    overwriteMusicList(LIST_IDS.DEFAULT, defaultList)\n    updatedListIds.push(LIST_IDS.DEFAULT)\n  }\n\n  overwriteMusicList(LIST_IDS.LOVE, loveList)\n  updatedListIds.push(LIST_IDS.LOVE)\n\n  if (tempList && allMusicList.has(LIST_IDS.TEMP)) {\n    overwriteMusicList(LIST_IDS.TEMP, tempList)\n    updatedListIds.push(LIST_IDS.TEMP)\n  }\n  const newIds = [LIST_IDS.DEFAULT, LIST_IDS.LOVE, ...userList.map(l => l.id)]\n  if (tempList) newIds.push(LIST_IDS.TEMP)\n  void overwriteListPosition(newIds)\n  void overwriteListUpdateInfo(newIds)\n  return updatedListIds\n}\n\nexport const userListCreate = ({ name, id, source, sourceListId, position, locationUpdateTime }: {\n  name: string\n  id: string\n  source?: LX.OnlineSource\n  sourceListId?: string\n  position: number\n  locationUpdateTime: number | null\n}) => {\n  if (userLists.some(item => item.id == id)) return\n  const newList: LX.List.UserListInfo = {\n    name,\n    id,\n    source,\n    sourceListId,\n    locationUpdateTime,\n  }\n  createUserList(newList, position)\n}\n\nexport const userListsRemove = (ids: string[]) => {\n  const changedIds = []\n  for (const id of ids) {\n    removeUserList(id)\n    void removeListPosition(id)\n    void removeListUpdateInfo(id)\n    if (!allMusicList.has(id)) continue\n    removeMusicList(id)\n    changedIds.push(id)\n  }\n\n  return changedIds\n}\n\nexport const userListsUpdate = (listInfos: LX.List.UserListInfo[]) => {\n  for (const info of listInfos) {\n    updateList(info)\n  }\n}\n\nexport const userListsUpdatePosition = (position: number, ids: string[]) => {\n  const newUserLists = [...userLists]\n\n  // console.log(position, ids)\n\n  const updateLists: LX.List.UserListInfo[] = []\n\n  // const targetItem = list[position]\n  const map = new Map<string, LX.List.UserListInfo>()\n  for (const item of newUserLists) map.set(item.id, item)\n  for (const id of ids) {\n    const listInfo = map.get(id)!\n    listInfo.locationUpdateTime = Date.now()\n    updateLists.push(listInfo)\n    map.delete(id)\n  }\n  newUserLists.splice(0, newUserLists.length, ...newUserLists.filter(mInfo => map.has(mInfo.id)))\n  newUserLists.splice(Math.min(position, newUserLists.length), 0, ...updateLists)\n\n  setUserLists(newUserLists)\n}\n\nexport const listMusicOverwrite = (listId: string, musicInfos: LX.Music.MusicInfo[]): string[] => {\n  const isExist = allMusicList.has(listId)\n  overwriteMusicList(listId, musicInfos)\n  return isExist || listId == loveList.id ? [listId] : []\n}\n\nexport const listMusicClear = (ids: string[]): string[] => {\n  const changedIds: string[] = []\n  for (const id of ids) {\n    const list = allMusicList.get(id)\n    if (!list?.length) continue\n    overwriteMusicList(id, [])\n    changedIds.push(id)\n  }\n  return changedIds\n}\n\nexport const listMusicAdd = (id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): string[] => {\n  const targetList = allMusicList.get(id)\n  if (!targetList) return id == loveList.id ? [id] : []\n\n  const listSet = new Set<string>()\n  for (const item of targetList) listSet.add(item.id)\n  musicInfos = musicInfos.filter(item => {\n    if (listSet.has(item.id)) return false\n    markRaw(item)\n    listSet.add(item.id)\n    return true\n  })\n  switch (addMusicLocationType) {\n    case 'top':\n      arrUnshift(targetList, musicInfos)\n      break\n    case 'bottom':\n    default:\n      arrPush(targetList, musicInfos)\n      break\n  }\n\n  return [id]\n}\n\nexport const listMusicMove = (fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): string[] => {\n  return [\n    ...listMusicRemove(fromId, musicInfos.map(musicInfo => musicInfo.id)),\n    ...listMusicAdd(toId, musicInfos, addMusicLocationType),\n  ]\n}\n\nexport const listMusicRemove = (listId: string, ids: string[]): string[] => {\n  let targetList = allMusicList.get(listId)\n  if (!targetList) return listId == loveList.id ? [listId] : []\n\n  const idsSet = new Set<string>(ids)\n  const newList = targetList.filter(mInfo => !idsSet.has(mInfo.id))\n  targetList.splice(0, targetList.length)\n  arrPush(targetList, newList)\n\n  return [listId]\n}\n\nexport const listMusicUpdateInfo = (musicInfos: LX.List.ListActionMusicUpdate): string[] => {\n  const updateListIds = new Set<string>()\n  for (const { id, musicInfo } of musicInfos) {\n    const targetList = allMusicList.get(id)\n    if (!targetList) continue\n    const index = targetList.findIndex(l => l.id == musicInfo.id)\n    if (index < 0) continue\n    const info: LX.Music.MusicInfo = { ...targetList[index] }\n    Object.assign(info, {\n      name: musicInfo.name,\n      singer: musicInfo.singer,\n      source: musicInfo.source,\n      interval: musicInfo.interval,\n      meta: musicInfo.meta,\n    })\n    targetList.splice(index, 1, markRaw(info))\n    updateListIds.add(id)\n  }\n  return Array.from(updateListIds)\n}\n\nexport const listMusicUpdatePosition = async(listId: string, position: number, ids: string[]): Promise<string[]> => {\n  let targetList = allMusicList.get(listId)\n  if (!targetList) return listId == loveList.id ? [listId] : []\n\n\n  // const infos = Array(ids.length)\n  // for (let i = targetList.length; i--;) {\n  //   const item = targetList[i]\n  //   const index = ids.indexOf(item.id)\n  //   if (index < 0) continue\n  //   infos.splice(index, 1, targetList.splice(i, 1)[0])\n  // }\n  // targetList.splice(Math.min(position, targetList.length - 1), 0, ...infos)\n\n  // console.time('ts')\n\n  const list = await window.lx.worker.main.createSortedList(toRaw(targetList), position, ids)\n  markRawList(list)\n  targetList.splice(0, targetList.length)\n  arrPush(targetList, list)\n\n  // console.timeEnd('ts')\n  return [listId]\n}\n"
  },
  {
    "path": "src/renderer/store/list/listManage/index.ts",
    "content": "export * from './rendererListManage'\nexport * from './state'\n"
  },
  {
    "path": "src/renderer/store/list/listManage/rendererListManage.ts",
    "content": "import { toRaw } from '@common/utils/vueTools'\nimport { rendererInvoke, rendererOff, rendererOn } from '@common/rendererIpc'\nimport { PLAYER_EVENT_NAME } from '@common/ipcNames'\nimport {\n  userListCreate,\n  listDataOverwrite,\n  userListsRemove,\n  userListsUpdate,\n  userListsUpdatePosition,\n  listMusicAdd,\n  listMusicMove,\n  listMusicRemove,\n  listMusicOverwrite,\n  listMusicUpdateInfo,\n  listMusicUpdatePosition,\n  setMusicList,\n  setUserLists,\n  listMusicClear,\n} from './action'\nimport { allMusicList } from './state'\n\n/**\n * 获取用户列表\n * @returns 所有用户列表\n */\nexport const getUserLists = async() => {\n  const lists = await rendererInvoke<LX.List.UserListInfo[]>(PLAYER_EVENT_NAME.list_get)\n  return setUserLists(lists)\n}\n\n/**\n * 添加用户列表\n * @param data\n */\nexport const createUserList = async(data: LX.List.ListActionAdd) => {\n  data.listInfos = data.listInfos.map(info => toRaw(info))\n  await rendererInvoke<LX.List.ListActionAdd>(PLAYER_EVENT_NAME.list_add, data)\n}\n\n/**\n * 移除用户列表及列表内歌曲\n * @param data\n */\nexport const removeUserList = async(data: LX.List.ListActionRemove) => {\n  await rendererInvoke<LX.List.ListActionRemove>(PLAYER_EVENT_NAME.list_remove, data)\n}\n\n/**\n * 更新用户列表\n * @param data\n */\nexport const updateUserList = async(data: LX.List.ListActionUpdate) => {\n  data = data.map(info => toRaw(info))\n  await rendererInvoke<LX.List.ListActionUpdate>(PLAYER_EVENT_NAME.list_update, data)\n}\n\n/**\n * 批量移动用户列表位置\n * @param data\n */\nexport const updateUserListPosition = async(data: LX.List.ListActionUpdatePosition) => {\n  await rendererInvoke<LX.List.ListActionUpdatePosition>(PLAYER_EVENT_NAME.list_update_position, data)\n}\n\n/**\n * 获取列表内的歌曲\n * @param listId\n */\nexport const getListMusics = async(listId: string | null): Promise<LX.Music.MusicInfo[]> => {\n  if (!listId) return []\n  if (allMusicList.has(listId)) return allMusicList.get(listId)!\n  const list = await rendererInvoke<string, LX.Music.MusicInfo[]>(PLAYER_EVENT_NAME.list_music_get, listId)\n  return setMusicList(listId, list)\n}\n\n/**\n * 批量添加歌曲到列表\n * @param data\n */\nexport const addListMusics = async(data: LX.List.ListActionMusicAdd) => {\n  await rendererInvoke<LX.List.ListActionMusicAdd>(PLAYER_EVENT_NAME.list_music_add, data)\n}\n\n/**\n * 跨列表批量移动歌曲\n * @param data\n */\nexport const moveListMusics = async(data: LX.List.ListActionMusicMove) => {\n  await rendererInvoke<LX.List.ListActionMusicMove>(PLAYER_EVENT_NAME.list_music_move, data)\n}\n\n/**\n * 批量删除列表内歌曲\n * @param data\n */\nexport const removeListMusics = async(data: LX.List.ListActionMusicRemove) => {\n  await rendererInvoke<LX.List.ListActionMusicRemove>(PLAYER_EVENT_NAME.list_music_remove, data)\n}\n\n/**\n * 批量更新列表内歌曲\n * @param data\n */\nexport const updateListMusics = async(data: LX.List.ListActionMusicUpdate) => {\n  await rendererInvoke<LX.List.ListActionMusicUpdate>(PLAYER_EVENT_NAME.list_music_update, data)\n}\n\n/**\n * 批量移动列表内歌曲的位置\n * @param data\n */\nexport const updateListMusicsPosition = async(data: LX.List.ListActionMusicUpdatePosition) => {\n  await rendererInvoke<LX.List.ListActionMusicUpdatePosition>(PLAYER_EVENT_NAME.list_music_update_position, data)\n}\n\n/**\n * 覆盖列表内的歌曲\n * @param data\n */\nexport const overwriteListMusics = async(data: LX.List.ListActionMusicOverwrite) => {\n  await rendererInvoke<LX.List.ListActionMusicOverwrite>(PLAYER_EVENT_NAME.list_music_overwrite, data)\n}\n\n/**\n * 清空列表内的歌曲\n * @param ids\n */\nexport const clearListMusics = async(ids: LX.List.ListActionMusicClear) => {\n  await rendererInvoke<LX.List.ListActionMusicClear>(PLAYER_EVENT_NAME.list_music_clear, ids)\n}\n\n/**\n * 覆盖全部列表数据\n * @param data\n */\nexport const overwriteListFull = async(data: LX.List.ListActionDataOverwrite) => {\n  data.defaultList = toRaw(data.defaultList)\n  data.loveList = toRaw(data.loveList)\n  if (data.tempList) {\n    data.tempList = toRaw(data.tempList)\n  }\n  data.userList = data.userList.map(info => {\n    return {\n      ...info,\n      list: toRaw(info.list),\n    }\n  })\n\n  await rendererInvoke<LX.List.ListActionDataOverwrite>(PLAYER_EVENT_NAME.list_data_overwire, data)\n}\n\n/**\n * 检查音乐是否存在列表中\n * @param listId\n * @param musicInfoId\n */\nexport const checkListExistMusic = async(listId: string, musicInfoId: string): Promise<boolean> => {\n  return rendererInvoke<LX.List.ListActionCheckMusicExistList, boolean>(PLAYER_EVENT_NAME.list_music_check_exist, { listId, musicInfoId })\n}\n\n/**\n * 获取所有存在该音乐的列表id\n * @param musicInfoId\n */\nexport const getMusicExistListIds = async(musicInfoId: string): Promise<string[]> => {\n  return rendererInvoke<string, string[]>(PLAYER_EVENT_NAME.list_music_get_list_ids, musicInfoId)\n}\n\n\nconst noop = () => {}\n\n\nexport const registerListAction = (appSetting: LX.AppSetting, onListChanged: (listIds: string[]) => void = noop) => {\n  const list_data_overwrite = ({ params: datas }: LX.IpcRendererEventParams<LX.List.ListActionDataOverwrite>) => {\n    const updatedListIds = listDataOverwrite(datas)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_create = ({ params: { position, listInfos } }: LX.IpcRendererEventParams<LX.List.ListActionAdd>) => {\n    for (const list of listInfos) {\n      userListCreate({ ...list, position })\n    }\n  }\n  const list_remove = ({ params: ids }: LX.IpcRendererEventParams<LX.List.ListActionRemove>) => {\n    const updatedListIds = userListsRemove(ids)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_update = ({ params: listInfos }: LX.IpcRendererEventParams<LX.List.ListActionUpdate>) => {\n    userListsUpdate(listInfos)\n  }\n  const list_update_position = ({ params: { position, ids } }: LX.IpcRendererEventParams<LX.List.ListActionUpdatePosition>) => {\n    userListsUpdatePosition(position, ids)\n  }\n  const list_music_add = ({ params: { id, musicInfos, addMusicLocationType } }: LX.IpcRendererEventParams<LX.List.ListActionMusicAdd>) => {\n    addMusicLocationType ??= appSetting['list.addMusicLocationType']\n    const updatedListIds = listMusicAdd(id, musicInfos, addMusicLocationType)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_music_move = ({ params: { fromId, toId, musicInfos, addMusicLocationType } }: LX.IpcRendererEventParams<LX.List.ListActionMusicMove>) => {\n    addMusicLocationType ??= appSetting['list.addMusicLocationType']\n    const updatedListIds = listMusicMove(fromId, toId, musicInfos, addMusicLocationType)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_music_remove = ({ params: { listId, ids } }: LX.IpcRendererEventParams<LX.List.ListActionMusicRemove>) => {\n    // console.log(listId, ids)\n    const updatedListIds = listMusicRemove(listId, ids)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_music_update = ({ params: musicInfos }: LX.IpcRendererEventParams<LX.List.ListActionMusicUpdate>) => {\n    const updatedListIds = listMusicUpdateInfo(musicInfos)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_music_update_position = ({ params: { listId, position, ids } }: LX.IpcRendererEventParams<LX.List.ListActionMusicUpdatePosition>) => {\n    void listMusicUpdatePosition(listId, position, ids).then(updatedListIds => {\n      if (updatedListIds.length) onListChanged(updatedListIds)\n    })\n  }\n  const list_music_overwrite = ({ params: { listId, musicInfos } }: LX.IpcRendererEventParams<LX.List.ListActionMusicOverwrite>) => {\n    const updatedListIds = listMusicOverwrite(listId, musicInfos)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n  const list_music_clear = ({ params: ids }: LX.IpcRendererEventParams<LX.List.ListActionMusicClear>) => {\n    const updatedListIds = listMusicClear(ids)\n    if (updatedListIds.length) onListChanged(updatedListIds)\n  }\n\n  rendererOn(PLAYER_EVENT_NAME.list_data_overwire, list_data_overwrite)\n  rendererOn(PLAYER_EVENT_NAME.list_add, list_create)\n  rendererOn(PLAYER_EVENT_NAME.list_remove, list_remove)\n  rendererOn(PLAYER_EVENT_NAME.list_update, list_update)\n  rendererOn(PLAYER_EVENT_NAME.list_update_position, list_update_position)\n  rendererOn(PLAYER_EVENT_NAME.list_music_add, list_music_add)\n  rendererOn(PLAYER_EVENT_NAME.list_music_move, list_music_move)\n  rendererOn(PLAYER_EVENT_NAME.list_music_remove, list_music_remove)\n  rendererOn(PLAYER_EVENT_NAME.list_music_update, list_music_update)\n  rendererOn(PLAYER_EVENT_NAME.list_music_update_position, list_music_update_position)\n  rendererOn(PLAYER_EVENT_NAME.list_music_overwrite, list_music_overwrite)\n  rendererOn(PLAYER_EVENT_NAME.list_music_clear, list_music_clear)\n\n  return () => {\n    rendererOff(PLAYER_EVENT_NAME.list_data_overwire, list_data_overwrite)\n    rendererOff(PLAYER_EVENT_NAME.list_add, list_create)\n    rendererOff(PLAYER_EVENT_NAME.list_remove, list_remove)\n    rendererOff(PLAYER_EVENT_NAME.list_update, list_update)\n    rendererOff(PLAYER_EVENT_NAME.list_update_position, list_update_position)\n    rendererOff(PLAYER_EVENT_NAME.list_music_add, list_music_add)\n    rendererOff(PLAYER_EVENT_NAME.list_music_move, list_music_move)\n    rendererOff(PLAYER_EVENT_NAME.list_music_remove, list_music_remove)\n    rendererOff(PLAYER_EVENT_NAME.list_music_update, list_music_update)\n    rendererOff(PLAYER_EVENT_NAME.list_music_update_position, list_music_update_position)\n    rendererOff(PLAYER_EVENT_NAME.list_music_overwrite, list_music_overwrite)\n    rendererOff(PLAYER_EVENT_NAME.list_music_clear, list_music_clear)\n  }\n}\n"
  },
  {
    "path": "src/renderer/store/list/listManage/state.ts",
    "content": "import { LIST_IDS } from '@common/constants'\nimport { markRaw, reactive } from '@common/utils/vueTools'\n\nexport const allMusicList: Map<string, LX.Music.MusicInfo[]> = markRaw(new Map())\n\nexport const defaultList = markRaw<LX.List.MyDefaultListInfo>({\n  id: LIST_IDS.DEFAULT,\n  name: 'list__name_default',\n  // name: '试听列表',\n})\n\nexport const loveList = markRaw<LX.List.MyLoveListInfo>({\n  id: LIST_IDS.LOVE,\n  name: 'list__name_love',\n  // name: '我的收藏',\n})\nexport const tempList = markRaw<LX.List.MyTempListInfo>({\n  id: LIST_IDS.TEMP,\n  name: '临时列表',\n  meta: {},\n})\n\nexport const userLists: LX.List.UserListInfo[] = reactive([])\n"
  },
  {
    "path": "src/renderer/store/list/state.ts",
    "content": "import { reactive } from '@common/utils/vueTools'\n\nexport {\n  allMusicList,\n  defaultList,\n  loveList,\n  tempList,\n  userLists,\n} from '@renderer/store/list/listManage'\n// import { reactive, ref, markRaw, Ref } from '@common/utils/vueTools'\n\n// // const TEMP_LIST = 'TEMP_LIST'\n\n// export const isInitedList: Ref<boolean> = ref(false)\n\n// export const allList: Map<string, LX.Music.MusicInfo[]> = window.lxData.allList = markRaw(new Map())\n\n// export const defaultList: Omit<LX.List.MyDefaultListInfo, 'list'> = reactive({\n//   id: 'default',\n//   name: '试听列表',\n// })\n\n// export const loveList: Omit<LX.List.MyLoveListInfo, 'list'> = reactive({\n//   id: 'love',\n//   name: '我的收藏',\n// })\n\n// export const tempList: Omit<LX.List.MyTempListInfo, 'list'> = reactive({\n//   id: 'temp',\n//   name: '临时列表',\n//   meta: {},\n// })\n\nexport const tempListMeta = {\n  id: '',\n}\n\n\n// export const userLists: LX.List.UserListInfo[] = window.lxData.userLists = reactive([])\n\nexport const fetchingListStatus = reactive<Record<string, boolean>>({})\n\nexport const listUpdateTimes = reactive<Record<string, string>>({})\n"
  },
  {
    "path": "src/renderer/store/list/syncSourceList.ts",
    "content": "import { setListUpdateTime } from '@renderer/utils/data'\nimport { setFetchingListStatus, overwriteListMusics, setUpdateTime } from './action'\nimport { getListDetailAll } from '@renderer/store/songList/action'\nimport { getListDetailAll as getBoardListAll } from '@renderer/store/leaderboard/action'\nimport { dateFormat } from '@common/utils/common'\n\nconst fetchList = async(id: string, source: LX.OnlineSource, sourceListId: string) => {\n  setFetchingListStatus(id, true)\n\n  let promise\n  if (/^board__/.test(sourceListId)) {\n    const id = sourceListId.replace(/^board__/, '')\n    promise = id ? getBoardListAll(id, true) : Promise.reject(new Error('id not defined: ' + sourceListId))\n  } else {\n    promise = getListDetailAll(sourceListId, source, true)\n  }\n  return promise.finally(() => {\n    setFetchingListStatus(id, false)\n  })\n}\n\nexport default async(targetListInfo: LX.List.UserListInfo) => {\n  // console.log(targetListInfo)\n  if (!targetListInfo.source || !targetListInfo.sourceListId) return\n  const list = await fetchList(targetListInfo.id, targetListInfo.source, targetListInfo.sourceListId)\n  // console.log(list)\n  void overwriteListMusics({ listId: targetListInfo.id, musicInfos: list })\n  const now = Date.now()\n  void setListUpdateTime(targetListInfo.id, now)\n  setUpdateTime(targetListInfo.id, dateFormat(now))\n}\n"
  },
  {
    "path": "src/renderer/store/player/action.ts",
    "content": "// import { reactive, ref, shallowRef } from '@common/utils/vueTools'\nimport {\n  type PlayerMusicInfo,\n  musicInfo,\n  isPlay,\n  status,\n  statusText,\n  isShowPlayerDetail,\n  isShowPlayComment,\n  isShowLrcSelectContent,\n  playInfo,\n  playMusicInfo,\n  playedList,\n  tempPlayList,\n} from './state'\nimport { getListMusicsFromCache } from '@renderer/store/list/action'\nimport { downloadList } from '@renderer/store/download/state'\nimport { setProgress } from './playProgress'\nimport { playNext } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\nimport { toRaw } from '@common/utils/vueTools'\nimport { arrPush, arrUnshift } from '@common/utils/common'\n\n\ntype PlayerMusicInfoKeys = keyof typeof musicInfo\n\nconst musicInfoKeys: PlayerMusicInfoKeys[] = Object.keys(musicInfo) as PlayerMusicInfoKeys[]\n\nexport const setMusicInfo = (_musicInfo: Partial<PlayerMusicInfo>) => {\n  for (const key of musicInfoKeys) {\n    const val = _musicInfo[key]\n    if (val !== undefined) {\n      // @ts-expect-error\n      musicInfo[key] = val\n    }\n  }\n}\n\nexport const setPlay = (val: boolean) => {\n  isPlay.value = val\n}\n\nexport const setStatus = (val: string) => {\n  console.log('setStatus', val)\n  status.value = val\n}\n\n\nexport const setStatusText = (val: string) => {\n  statusText.value = val\n}\n\nexport const setAllStatus = (val: string) => {\n  console.log('setAllStatus', val)\n  status.value = val\n  statusText.value = val\n}\n\n\nexport const setShowPlayerDetail = (val: boolean) => {\n  isShowPlayerDetail.value = val\n}\n\nexport const setShowPlayComment = (val: boolean) => {\n  isShowPlayComment.value = val\n}\n\nexport const setShowPlayLrcSelectContentLrc = (val: boolean) => {\n  isShowLrcSelectContent.value = val\n}\n\nexport const setPlayListId = (listId: string | null) => {\n  playInfo.playerListId = listId\n}\n\nexport const getList = (listId: string | null): Array<LX.Music.MusicInfo | LX.Download.ListItem> => {\n  return listId == LIST_IDS.DOWNLOAD ? downloadList : getListMusicsFromCache(listId)\n}\n\n/**\n * 更新播放位置\n * @returns 播放位置\n */\nexport const updatePlayIndex = () => {\n  const indexInfo = getPlayIndex(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)\n  // console.log(indexInfo)\n  playInfo.playIndex = indexInfo.playIndex\n  playInfo.playerPlayIndex = indexInfo.playerPlayIndex\n\n  return indexInfo\n}\n\nexport const getPlayIndex = (listId: string | null, musicInfo: LX.Download.ListItem | LX.Music.MusicInfo | null, isTempPlay: boolean): {\n  playIndex: number\n  playerPlayIndex: number\n} => {\n  const playerList = getList(playInfo.playerListId)\n\n  // if (listIndex < 0) throw new Error('music info not found')\n  // playInfo.playIndex = listIndex\n\n  let playIndex = -1\n  let playerPlayIndex = -1\n  if (playerList.length) {\n    playerPlayIndex = Math.min(playInfo.playerPlayIndex, playerList.length - 1)\n  }\n\n  const list = getList(listId)\n  if (list.length && musicInfo) {\n    const currentId = musicInfo.id\n    playIndex = list.findIndex(m => m.id == currentId)\n    if (!isTempPlay) {\n      if (playIndex < 0) {\n        playerPlayIndex = playerPlayIndex < 1 ? (list.length - 1) : (playerPlayIndex - 1)\n      } else {\n        playerPlayIndex = playIndex\n      }\n    }\n  }\n\n  return {\n    playIndex,\n    playerPlayIndex,\n  }\n}\n\nexport const resetPlayerMusicInfo = () => {\n  setMusicInfo({\n    id: null,\n    pic: null,\n    lrc: null,\n    tlrc: null,\n    rlrc: null,\n    lxlrc: null,\n    rawlrc: null,\n    name: '',\n    singer: '',\n    album: '',\n  })\n}\n\nconst setPlayerMusicInfo = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem | null) => {\n  if (musicInfo) {\n    setMusicInfo('progress' in musicInfo ? {\n      id: musicInfo.id,\n      pic: musicInfo.metadata.musicInfo.meta.picUrl,\n      name: musicInfo.metadata.musicInfo.name,\n      singer: musicInfo.metadata.musicInfo.singer,\n      album: musicInfo.metadata.musicInfo.meta.albumName ?? '',\n      lrc: null,\n      tlrc: null,\n      rlrc: null,\n      lxlrc: null,\n      rawlrc: null,\n    } : {\n      id: musicInfo.id,\n      pic: musicInfo.meta.picUrl,\n      name: musicInfo.name,\n      singer: musicInfo.singer,\n      album: musicInfo.meta.albumName ?? '',\n      lrc: null,\n      tlrc: null,\n      rlrc: null,\n      lxlrc: null,\n      rawlrc: null,\n    })\n  } else resetPlayerMusicInfo()\n}\n\n/**\n * 设置当前播放歌曲的信息\n * @param listId 歌曲所属的列表id\n * @param musicInfo 歌曲信息\n * @param isTempPlay 是否临时播放\n */\nexport const setPlayMusicInfo = (listId: string | null, musicInfo: LX.Download.ListItem | LX.Music.MusicInfo | null, isTempPlay: boolean = false) => {\n  musicInfo = toRaw(musicInfo)\n\n  playMusicInfo.listId = listId\n  playMusicInfo.musicInfo = musicInfo\n  playMusicInfo.isTempPlay = isTempPlay\n\n  setPlayerMusicInfo(musicInfo)\n\n  setProgress(0, 0)\n\n  if (musicInfo == null) {\n    playInfo.playIndex = -1\n    playInfo.playerListId = null\n    playInfo.playerPlayIndex = -1\n  } else {\n    const { playIndex, playerPlayIndex } = getPlayIndex(listId, musicInfo, isTempPlay)\n\n    playInfo.playIndex = playIndex\n    playInfo.playerPlayIndex = playerPlayIndex\n    window.app_event.musicToggled()\n  }\n}\n\n/**\n * 将歌曲添加到已播放列表\n * @param playMusicInfo playMusicInfo对象\n */\nexport const addPlayedList = (playMusicInfo: LX.Player.PlayMusicInfo) => {\n  const id = playMusicInfo.musicInfo.id\n  if (playedList.some(m => m.musicInfo.id === id)) return\n  playedList.push(playMusicInfo)\n}\n/**\n * 将歌曲从已播放列表移除\n * @param index 歌曲位置\n */\nexport const removePlayedList = (index: number) => {\n  playedList.splice(index, 1)\n}\n/**\n * 清空已播放列表\n */\nexport const clearPlayedList = () => {\n  playedList.splice(0, playedList.length)\n}\n\n/**\n * 添加歌曲到稍后播放列表\n * @param list 歌曲列表\n */\nexport const addTempPlayList = (list: LX.Player.TempPlayListItem[]) => {\n  const topList: Array<Omit<LX.Player.TempPlayListItem, 'top'>> = []\n  const bottomList = list.filter(({ isTop, ...musicInfo }) => {\n    if (isTop) {\n      topList.push(musicInfo)\n      return false\n    }\n    return true\n  })\n  if (topList.length) arrUnshift(tempPlayList, topList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))\n  if (bottomList.length) arrPush(tempPlayList, bottomList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))\n\n  if (!playMusicInfo.musicInfo) void playNext()\n}\n/**\n * 从稍后播放列表移除歌曲\n * @param index 歌曲位置\n */\nexport const removeTempPlayList = (index: number) => {\n  tempPlayList.splice(index, 1)\n}\n/**\n * 清空稍后播放列表\n */\nexport const clearTempPlayeList = () => {\n  tempPlayList.splice(0, tempPlayList.length)\n}\n"
  },
  {
    "path": "src/renderer/store/player/lyric.ts",
    "content": "import { reactive } from '@common/utils/vueTools'\n\nexport interface Line {\n  text: string\n  time: number\n  extendedLyrics: string[]\n  dom_line: HTMLDivElement\n}\n\nexport const lyric = reactive<{\n  lines: Line[]\n  text: string\n  line: number\n  offset: number // 歌词延迟\n  tempOffset: number // 歌词临时延迟\n}>({\n  lines: [],\n  text: '',\n  line: 0,\n  offset: 0, // 歌词延迟\n  tempOffset: 0, // 歌词临时延迟\n})\n\nexport const setLines = (lines: Line[]) => {\n  if (!lines.length && !lyric.lines.length) return\n  lyric.lines = lines\n}\nexport const setText = (text: string, line: number) => {\n  lyric.text = text\n  lyric.line = line\n}\nexport const setOffset = (offset: number) => {\n  lyric.offset = offset\n}\nexport const setTempOffset = (offset: number) => {\n  lyric.tempOffset = offset\n}\n"
  },
  {
    "path": "src/renderer/store/player/playProgress.ts",
    "content": "import { reactive } from '@common/utils/vueTools'\nimport { formatPlayTime2 } from '@common/utils/common'\n\nexport const playProgress = reactive({\n  nowPlayTime: 0,\n  maxPlayTime: 0,\n  progress: 0,\n  nowPlayTimeStr: '00:00',\n  maxPlayTimeStr: '00:00',\n})\n\nexport const setNowPlayTime = (time: number) => {\n  playProgress.nowPlayTime = time\n  playProgress.nowPlayTimeStr = formatPlayTime2(time)\n  playProgress.progress = playProgress.maxPlayTime ? time / playProgress.maxPlayTime : 0\n}\n\nexport const setMaxplayTime = (time: number) => {\n  playProgress.maxPlayTime = time\n  playProgress.maxPlayTimeStr = formatPlayTime2(time)\n  playProgress.progress = time ? playProgress.nowPlayTime / time : 0\n}\n\nexport const setProgress = (currentTime: number, totalTime: number) => {\n  setMaxplayTime(totalTime)\n  setNowPlayTime(currentTime)\n}\n"
  },
  {
    "path": "src/renderer/store/player/playbackRate.ts",
    "content": "import { ref } from '@common/utils/vueTools'\n\n\nexport const playbackRate = ref(1)\n\nexport const setPlaybackRate = (num: number) => {\n  playbackRate.value = num\n}\n"
  },
  {
    "path": "src/renderer/store/player/state.ts",
    "content": "import { reactive, shallowReactive, ref } from '@common/utils/vueTools'\n\nexport interface PlayerMusicInfo {\n  id: string | null\n  pic: string | null\n  lrc: string | null\n  tlrc: string | null\n  rlrc: string | null\n  lxlrc: string | null\n  rawlrc: string | null\n  // url: string | null\n  name: string\n  singer: string\n  album: string\n}\n\nexport const musicInfo = window.lxData.musicInfo = reactive<PlayerMusicInfo>({\n  id: null,\n  pic: null,\n  lrc: null,\n  tlrc: null,\n  rlrc: null,\n  lxlrc: null,\n  rawlrc: null,\n  // url: null,\n  name: '',\n  singer: '',\n  album: '',\n})\n\nexport const isPlay = ref(false)\n\nexport const status = window.lxData.status = ref('')\n\nexport const statusText = ref('')\n\nexport const isShowPlayerDetail = ref(false)\n\nexport const isShowPlayComment = ref(false)\n\nexport const isShowLrcSelectContent = ref(false)\n\nexport const playMusicInfo = shallowReactive<{\n  /**\n   * 当前播放歌曲的列表 id\n   */\n  musicInfo: LX.Player.PlayMusicInfo['musicInfo'] | null\n  /**\n   * 当前播放歌曲的列表 id\n   */\n  listId: LX.Player.PlayMusicInfo['listId'] | null\n  /**\n   * 是否属于 “稍后播放”\n   */\n  isTempPlay: boolean\n}>({\n  listId: null,\n  musicInfo: null,\n  isTempPlay: false,\n})\nexport const playInfo = shallowReactive<LX.Player.PlayInfo>({\n  playIndex: -1,\n  playerListId: null,\n  playerPlayIndex: -1,\n})\n\n\nexport const playedList = window.lxData.playedList = shallowReactive<LX.Player.PlayMusicInfo[]>([])\n\nexport const tempPlayList = shallowReactive<LX.Player.PlayMusicInfo[]>([])\n\nwindow.lxData.playInfo = playInfo\nwindow.lxData.playMusicInfo = playMusicInfo\n"
  },
  {
    "path": "src/renderer/store/player/volume.ts",
    "content": "import { ref } from '@common/utils/vueTools'\n\n\nexport const volume = ref(0)\nexport const isMute = ref(false)\n\nexport const setVolume = (num: number) => {\n  volume.value = num\n}\n\nexport const setMute = (flag: boolean) => {\n  isMute.value = flag\n}\n"
  },
  {
    "path": "src/renderer/store/search/action.ts",
    "content": "\nimport { throttle } from '@common/utils/common'\nimport { toRaw } from '@common/utils/vueTools'\nimport {\n  getSearchHistoryList,\n  saveSearchHistoryList,\n} from '@renderer/utils/ipc'\nimport { appSetting } from '../setting'\nimport { searchText, historyList } from './state'\n\n\nexport const setSearchText = (text: string) => {\n  searchText.value = text\n}\n\nlet isInitedSearchHistory = false\nconst saveSearchHistoryListThrottle = throttle((list: LX.List.SearchHistoryList) => {\n  saveSearchHistoryList(list)\n}, 500)\n\n\nexport const getHistoryList = async() => {\n  if (isInitedSearchHistory || historyList.length) return\n  historyList.push(...(await getSearchHistoryList() ?? []))\n  isInitedSearchHistory ||= true\n}\nexport const addHistoryWord = async(word: string) => {\n  if (!appSetting['search.isShowHistorySearch']) return\n  if (!isInitedSearchHistory) await getHistoryList()\n  let index = historyList.indexOf(word)\n  if (index == 0) return\n  if (index > -1) historyList.splice(index, 1)\n  if (historyList.length >= 15) historyList.splice(14, historyList.length - 14)\n  historyList.unshift(word)\n  saveSearchHistoryListThrottle(toRaw(historyList))\n}\nexport const removeHistoryWord = (index: number) => {\n  historyList.splice(index, 1)\n  saveSearchHistoryListThrottle(toRaw(historyList))\n}\nexport const clearHistoryList = (id: string) => {\n  historyList.splice(0, historyList.length)\n  saveSearchHistoryList([])\n}\n"
  },
  {
    "path": "src/renderer/store/search/music/action.ts",
    "content": "import { markRaw } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\nimport { deduplicationList, toNewMusicInfo } from '@renderer/utils'\nimport { sortInsert, similar } from '@common/utils/common'\n\nimport { sources, maxPages, listInfos } from './state'\n\ninterface SearchResult {\n  list: LX.Music.MusicInfo[]\n  allPage: number\n  limit: number\n  total: number\n  source: LX.OnlineSource\n}\n\n\n/**\n * 按搜索关键词重新排序列表\n * @param list 歌曲列表\n * @param keyword 搜索关键词\n * @returns 排序后的列表\n */\nconst handleSortList = (list: LX.Music.MusicInfo[], keyword: string) => {\n  let arr: any[] = []\n  for (const item of list) {\n    sortInsert(arr, {\n      num: similar(keyword, `${item.name} ${item.singer}`),\n      data: item,\n    })\n  }\n  return arr.map(item => item.data).reverse()\n}\n\n\nconst setLists = (results: SearchResult[], page: number, text: string): LX.Music.MusicInfo[] => {\n  let pages = []\n  let totals = []\n  let limit = 0\n  let list = []\n  for (const source of results) {\n    maxPages[source.source] = source.allPage\n    limit = Math.max(source.limit, limit)\n    if (source.allPage < page) continue\n    list.push(...source.list)\n    pages.push(source.allPage)\n    totals.push(source.total)\n  }\n  list = deduplicationList(list.map(s => markRaw(toNewMusicInfo(s))))\n  let listInfo = listInfos.all\n  listInfo.maxPage = Math.max(0, ...pages)\n  const total = Math.max(0, ...totals)\n  if (page == 1 || (total && list.length)) listInfo.total = total\n  else listInfo.total = limit * page\n  // listInfo.limit = limit\n  listInfo.page = page\n  listInfo.list = handleSortList(list, text)\n  if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')\n  else listInfo.noItemLabel = ''\n  return listInfo.list\n}\n\nconst setList = (datas: SearchResult, page: number, text: string): LX.Music.MusicInfo[] => {\n  // console.log(datas.source, datas.list)\n  let listInfo = listInfos[datas.source]!\n  listInfo.list = deduplicationList(datas.list.map(s => markRaw(toNewMusicInfo(s))))\n  if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total\n  else listInfo.total = datas.limit * page\n  listInfo.maxPage = datas.allPage\n  listInfo.page = page\n  listInfo.limit = datas.limit\n  if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')\n  else listInfo.noItemLabel = ''\n  return listInfo.list\n}\n\nexport const resetListInfo = (sourceId: LX.OnlineSource | 'all'): [] => {\n  let listInfo = listInfos[sourceId]\n  if (!listInfo) return []\n  listInfo.list = []\n  listInfo.page = 0\n  listInfo.maxPage = 0\n  listInfo.total = 0\n  listInfo.noItemLabel = ''\n  return []\n}\n\nexport const search = async(text: string, page: number, sourceId: LX.OnlineSource | 'all'): Promise<LX.Music.MusicInfo[]> => {\n  const listInfo = listInfos[sourceId]\n  if (!text) return resetListInfo(sourceId)\n  const key = `${page}__${text}`\n  if (sourceId == 'all') {\n    listInfo!.noItemLabel = window.i18n.t('list__loading')\n    listInfo!.key = key\n    let task = []\n    for (const source of sources) {\n      if (source == 'all') continue\n      task.push((music[source]?.musicSearch.search(text, page, listInfos.all.limit) ?? Promise.reject(new Error('source not found: ' + source))).catch((error: any) => {\n        console.log(error)\n        return {\n          allPage: 1,\n          limit: 30,\n          list: [],\n          source,\n          total: 0,\n        }\n      }))\n    }\n    return Promise.all(task).then((results: SearchResult[]) => {\n      if (key != listInfo!.key) return []\n      return setLists(results, page, text)\n    })\n  } else {\n    if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list\n    listInfo!.noItemLabel = window.i18n.t('list__loading')\n    listInfo!.key = key\n    return music[sourceId].musicSearch.search(text, page, listInfo!.limit).then((data: SearchResult) => {\n      if (key != listInfo!.key) return []\n      return setList(data, page, text)\n    }).catch((error: any) => {\n      resetListInfo(sourceId)\n      listInfo!.noItemLabel = window.i18n.t('list__load_failed')\n      console.log(error)\n      throw error\n    })\n  }\n}\n\n"
  },
  {
    "path": "src/renderer/store/search/music/index.ts",
    "content": "\nexport * from './action'\nexport * from './state'\n"
  },
  {
    "path": "src/renderer/store/search/music/state.ts",
    "content": "import { reactive, markRaw } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\n\n// import { deduplicationList } from '@common/utils/renderer'\n\nexport declare interface ListInfo {\n  list: LX.Music.MusicInfo[]\n  total: number\n  page: number\n  maxPage: number\n  limit: number\n  key: string | null\n  noItemLabel: string\n}\n\ninterface ListInfos extends Partial<Record<LX.OnlineSource, ListInfo>> {\n  'all': ListInfo\n}\n\nexport const sources: Array<LX.OnlineSource | 'all'> = markRaw([])\n\nexport const listInfos: ListInfos = markRaw({\n  all: reactive<ListInfo>({\n    page: 1,\n    maxPage: 0,\n    limit: 30,\n    total: 0,\n    list: [],\n    key: null,\n    noItemLabel: '',\n  }),\n})\nexport const maxPages: Partial<Record<LX.OnlineSource, number>> = {}\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.musicSearch) continue\n  sources.push(source.id as LX.OnlineSource)\n  listInfos[source.id as LX.OnlineSource] = reactive<ListInfo>({\n    page: 1,\n    maxPage: 0,\n    limit: 30,\n    total: 0,\n    list: [],\n    key: '',\n    noItemLabel: '',\n  })\n  maxPages[source.id as LX.OnlineSource] = 0\n}\nsources.push('all')\n"
  },
  {
    "path": "src/renderer/store/search/songlist/action.ts",
    "content": "import { markRawList } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\nimport { sortInsert, similar } from '@common/utils/common'\n\nimport type { ListInfoItem } from './state'\nimport { sources, maxPages, listInfos } from './state'\n\ninterface SearchResult {\n  list: ListInfoItem[]\n  limit: number\n  total: number\n  source: LX.OnlineSource\n}\n\n\n/**\n * 按搜索关键词重新排序列表\n * @param list 歌曲列表\n * @param keyword 搜索关键词\n * @returns 排序后的列表\n */\nconst handleSortList = (list: ListInfoItem[], keyword: string) => {\n  let arr: any[] = []\n  for (const item of list) {\n    sortInsert(arr, {\n      num: similar(keyword, item.name),\n      data: item,\n    })\n  }\n  return arr.map(item => item.data).reverse()\n}\n\n\nlet maxTotals: Partial<Record<LX.OnlineSource, number>> = {\n\n}\nconst setLists = (results: SearchResult[], page: number, text: string): ListInfoItem[] => {\n  let totals = []\n  let limit = 0\n  let list = []\n  for (const source of results) {\n    list.push(...source.list)\n    totals.push(source.total)\n    maxTotals[source.source] = source.total\n    maxPages[source.source] = Math.ceil(source.total / source.limit)\n    limit = Math.max(source.limit, limit)\n  }\n  markRawList(list)\n\n  let listInfo = listInfos.all\n  const total = Math.max(0, ...totals)\n  if (page == 1 || (total && list.length)) listInfo.total = total\n  else listInfo.total = limit * page\n  listInfo.page = page\n  listInfo.list = handleSortList(list, text)\n  if (text && !list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')\n  else listInfo.noItemLabel = ''\n  return listInfo.list\n}\n\nconst setList = (datas: SearchResult, page: number, text: string): ListInfoItem[] => {\n  // console.log(datas.source, datas.list)\n  let listInfo = listInfos[datas.source]!\n  listInfo.list = markRawList(datas.list)\n  if (page == 1 || (datas.total && datas.list.length)) listInfo.total = datas.total\n  else listInfo.total = datas.limit * page\n  listInfo.page = page\n  listInfo.limit = datas.limit\n  if (text && !datas.list.length && page == 1) listInfo.noItemLabel = window.i18n.t('no_item')\n  else listInfo.noItemLabel = ''\n  return listInfo.list\n}\n\nexport const resetListInfo = (sourceId: LX.OnlineSource | 'all'): [] => {\n  let listInfo = listInfos[sourceId]\n  if (!listInfo) return []\n  listInfo.page = 1\n  listInfo.limit = 20\n  listInfo.total = 0\n  listInfo.list = []\n  listInfo.key = null\n  listInfo.noItemLabel = ''\n  listInfo.tagId = ''\n  listInfo.sortId = ''\n  return []\n}\n\nexport const search = async(text: string, page: number, sourceId: LX.OnlineSource | 'all'): Promise<ListInfoItem[]> => {\n  const listInfo = listInfos[sourceId]!\n  if (!text) return resetListInfo(sourceId)\n  const key = `${page}__${sourceId}__${text}`\n  if (listInfo.key == key && listInfo.list.length) return listInfo.list\n  if (sourceId == 'all') {\n    listInfo.noItemLabel = window.i18n.t('list__loading')\n    listInfo.key = key\n    let task = []\n    for (const source of sources) {\n      if (source == 'all' || (page > 1 && page > (maxPages[source]!))) continue\n      task.push((music[source]?.songList.search(text, page, listInfos.all.limit) ?? Promise.reject(new Error('source not found: ' + source))).catch((error: any) => {\n        console.log(error)\n        return {\n          list: [],\n          total: 0,\n          limit: listInfos.all.limit,\n          source,\n        }\n      }))\n    }\n    return Promise.all(task).then((results: SearchResult[]) => {\n      if (key != listInfo.key) return []\n      return setLists(results, page, text)\n    })\n  } else {\n    if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list\n    listInfo.noItemLabel = window.i18n.t('list__loading')\n    listInfo.key = key\n    return (music[sourceId]?.songList.search(text, page, listInfo.limit).then((data: SearchResult) => {\n      if (key != listInfo.key) return []\n      return setList(data, page, text)\n    }) ?? Promise.reject(new Error('source not found: ' + sourceId))).catch((error: any) => {\n      resetListInfo(sourceId)\n      listInfo.noItemLabel = window.i18n.t('list__load_failed')\n      console.log(error)\n      throw error\n    })\n  }\n}\n\n"
  },
  {
    "path": "src/renderer/store/search/songlist/index.ts",
    "content": "\nexport * from './action'\nexport * from './state'\n"
  },
  {
    "path": "src/renderer/store/search/songlist/state.ts",
    "content": "import { reactive, markRaw } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\n\n// import { deduplicationList } from '@common/utils/renderer'\n\nimport { type ListInfo } from '@renderer/store/songList/state'\n\nexport type { ListInfoItem } from '@renderer/store/songList/state'\n\nexport const sources: Array<LX.OnlineSource | 'all'> = markRaw([])\n\nexport type SearchListInfo = Omit<ListInfo, 'source'>\n\n\ninterface ListInfos extends Partial<Record<LX.OnlineSource, SearchListInfo>> {\n  'all': SearchListInfo\n}\n\n\nexport const listInfos: ListInfos = markRaw({\n  all: reactive<SearchListInfo>({\n    page: 1,\n    limit: 15,\n    total: 0,\n    list: [],\n    key: null,\n    noItemLabel: '',\n    tagId: '',\n    sortId: '',\n  }),\n})\nexport const maxPages: Partial<Record<LX.OnlineSource, number>> = {}\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.songList?.search) continue\n  sources.push(source.id as LX.OnlineSource)\n  listInfos[source.id as LX.OnlineSource] = reactive<SearchListInfo>({\n    page: 1,\n    limit: 18,\n    total: 0,\n    list: [],\n    key: null,\n    noItemLabel: '',\n    tagId: '',\n    sortId: '',\n  })\n  maxPages[source.id as LX.OnlineSource] = 0\n}\nsources.push('all')\n"
  },
  {
    "path": "src/renderer/store/search/state.ts",
    "content": "import { ref, shallowReactive } from '@common/utils/vueTools'\n\n\nexport const searchText = ref('')\n\nexport type onlineSource = LX.OnlineSource\n\n\nexport const historyList = shallowReactive<string[]>([])\n"
  },
  {
    "path": "src/renderer/store/setting.ts",
    "content": "import { reactive, computed } from '@common/utils/vueTools'\nimport defaultSetting from '@common/defaultSetting'\nimport { updateSetting as saveSetting } from '@renderer/utils/ipc'\n\nexport const appSetting = window.lxData.appSetting = reactive<LX.AppSetting>({ ...defaultSetting })\n\nexport const isShowAnimation = computed(() => {\n  return appSetting['common.isShowAnimation']\n})\n\n\nexport const initSetting = (newSetting: LX.AppSetting) => {\n  mergeSetting(newSetting)\n}\n\nexport const mergeSetting = (newSetting: Partial<LX.AppSetting>) => {\n  for (const [key, value] of Object.entries(newSetting)) {\n    // @ts-expect-error\n    appSetting[key] = value\n  }\n}\n\nexport const updateSetting = window.lxData.updateSetting = (setting: Partial<LX.AppSetting>) => {\n  // console.warn(setting)\n  void saveSetting(setting)\n}\n\n/**\n * 保存是否同意协议\n * @param isAgreePact 是否同意协议\n */\nexport const saveAgreePact = (isAgreePact: boolean) => {\n  updateSetting({ 'common.isAgreePact': isAgreePact })\n}\n\n/**\n * 保存音频输出id\n * @param id 媒体驱动id\n */\nexport const saveMediaDeviceId = (id: string) => {\n  updateSetting({ 'player.mediaDeviceId': id })\n}\n\n/**\n * 保存音量大小\n * @param volume 音量\n */\nexport const saveVolume = (volume: number) => {\n  updateSetting({ 'player.volume': volume })\n}\n\n/**\n * 设置是否静音\n * @param isMute 是否静音\n */\nexport const saveVolumeIsMute = (isMute: boolean) => {\n  updateSetting({ 'player.isMute': isMute })\n}\n\n/**\n * 设置播放速率\n * @param rate 播放速率\n */\nexport const savePlaybackRate = (rate: number) => {\n  updateSetting({ 'player.playbackRate': rate })\n}\n\n\n/**\n * 设置是否开启桌面歌词\n * @param enabled\n */\nexport const setVisibleDesktopLyric = (enabled: boolean) => {\n  updateSetting({ 'desktopLyric.enable': enabled })\n}\n\n/**\n * 设置是否锁定桌面歌词\n * @param isLock\n */\nexport const setLockDesktopLyric = (isLock: boolean) => {\n  updateSetting({ 'desktopLyric.isLock': isLock })\n}\n\n/**\n * 设置切歌模式\n * @param mode\n */\nexport const setTogglePlayMode = (mode: LX.AppSetting['player.togglePlayMethod']) => {\n  updateSetting({ 'player.togglePlayMethod': mode })\n}\n\n/**\n * 设置API id\n * @param sourceId\n */\nexport const setApiSource = (sourceId: string) => {\n  updateSetting({ 'common.apiSource': sourceId })\n}\n\n/**\n * 设置播放详情页歌词字体大小\n * @param size 字体大小\n */\nexport const setPlayDetailLyricFont = (size: number) => {\n  updateSetting({ 'playDetail.style.fontSize': size })\n}\n\n/**\n * 设置播放详情页歌词对齐方式\n * @param align 对齐方式\n */\nexport const setPlayDetailLyricAlign = (align: LX.AppSetting['playDetail.style.align']) => {\n  updateSetting({ 'playDetail.style.align': align })\n}\n\n/**\n * 设置播放详情页音频可视化\n * @param enable 是否启用\n */\nexport const setEnableAudioVisualization = (enable: boolean) => {\n  updateSetting({ 'player.audioVisualization': enable })\n}\n"
  },
  {
    "path": "src/renderer/store/songList/action.ts",
    "content": "// import { getSongListSetting } from '@renderer/utils/data'\nimport { deduplicationList, toNewMusicInfo } from '@renderer/utils'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { markRaw, markRawList } from '@common/utils/vueTools'\nimport {\n  tags,\n  listInfo,\n  listDetailInfo,\n  selectListInfo,\n  isVisibleListDetail,\n  openSongListInputInfo,\n} from './state'\nimport type {\n  ListDetailInfo,\n  ListInfoItem,\n  ListInfo,\n  TagInfo,\n} from './state'\n\nconst cache = new Map<string, any>()\n\nexport const setTags = (tagInfo: TagInfo, source: LX.OnlineSource) => {\n  tags[source] = markRaw(tagInfo)\n}\n\nexport const clearList = () => {\n  listInfo.list = []\n  listInfo.total = 0\n  listInfo.noItemLabel = ''\n  listInfo.page = 1\n  listInfo.key = ''\n}\n\nexport const setList = (result: ListInfo, tagId: string, sortId: string, page: number) => {\n  listInfo.list = markRaw([...result.list])\n  if (page == 1 || (result.total && result.list.length)) listInfo.total = result.total\n  else listInfo.total = result.limit * page\n  listInfo.limit = result.limit\n  listInfo.page = page\n  listInfo.source = result.source\n  listInfo.tagId = tagId\n  listInfo.sortId = sortId\n  if (result.list.length) listInfo.noItemLabel = ''\n  else if (page == 1) listInfo.noItemLabel = window.i18n.t('no_item')\n}\nexport const setListDetail = (result: ListDetailInfo, id: string, page: number) => {\n  listDetailInfo.list = markRaw([...result.list])\n  listDetailInfo.id = id\n  listDetailInfo.source = result.source\n  if (page == 1 || (result.total && result.list.length)) listDetailInfo.total = result.total\n  else listDetailInfo.total = result.limit * page\n  listDetailInfo.limit = result.limit\n  listDetailInfo.page = page\n  listDetailInfo.info = markRaw({ ...result.info })\n  if (result.list.length) listDetailInfo.noItemLabel = ''\n  else if (page == 1) listDetailInfo.noItemLabel = window.i18n.t('no_item')\n}\n\nexport const setSelectListInfo = (info: ListInfoItem) => {\n  selectListInfo.author = info.author\n  selectListInfo.desc = info.desc\n  selectListInfo.id = info.id\n  selectListInfo.img = info.img\n  selectListInfo.name = info.name\n  selectListInfo.play_count = info.play_count\n  selectListInfo.source = info.source\n}\nexport const clearListDetail = () => {\n  listDetailInfo.list = []\n  listDetailInfo.id = ''\n  listDetailInfo.source = 'kw'\n  listDetailInfo.total = 0\n  listDetailInfo.limit = 30\n  listDetailInfo.page = 1\n  listDetailInfo.key = null\n  listDetailInfo.info = {}\n  listDetailInfo.noItemLabel = ''\n}\n\nexport const getTags = async<T extends LX.OnlineSource>(source: T) => {\n  return musicSdk[source]?.songList.getTags() as Promise<TagInfo<T>>\n}\n\n\n/**\n * 获取歌单列表\n * @param source 歌单源\n * @param tabId 类型id\n * @param sortId 排序\n * @param page 页数\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getAndSetList = async(source: LX.OnlineSource, tabId: string, sortId: string, page: number, isRefresh = false) => {\n  // let source = rootState.setting.songList.source\n  // let tabId = rootState.setting.songList.tagInfo.id\n  // let sortId = rootState.setting.songList.sortId\n  // console.log(sortId)\n  let key = `slist__${source}__${sortId}__${tabId}__${page}`\n  // if (state.list.list.length && state.list.key == key) return\n  if (!isRefresh) {\n    if (listInfo.key == key && listInfo.list.length) return\n    if (cache.has(key)) {\n      listInfo.key = key\n      setList(cache.get(key), tabId, sortId, page)\n      return\n    }\n  }\n  listInfo.noItemLabel = window.i18n.t('list__loading')\n  listInfo.key = key\n  // clearList()\n  return musicSdk[source]?.songList.getList(sortId, tabId, page).then((result: ListInfo) => {\n    cache.set(key, result)\n    if (key != listInfo.key) return\n    setList(result, tabId, sortId, page)\n  }).catch((error: any) => {\n    clearList()\n    listInfo.noItemLabel = window.i18n.t('list__load_failed')\n    console.log(error)\n    throw error\n  })\n}\n\n/**\n * 获取歌单内单页歌曲\n * @param id 歌单id\n * @param source 歌单源\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetail = async(id: string, source: LX.OnlineSource, page: number, isRefresh = false): Promise<ListDetailInfo> => {\n  let key = `sdetail__${source}__${id}__${page}`\n  if (!isRefresh && cache.has(key)) return cache.get(key)\n\n  return musicSdk[source]?.songList.getListDetail(id, page).then((result: ListDetailInfo) => {\n    result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[]))\n    cache.set(key, result)\n    return result\n  })\n}\n\n/**\n * 获取歌单内全部歌曲\n * @param id 歌单id\n * @param source 歌单源\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetailAll = async(id: string, source: LX.OnlineSource, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {\n  // console.log(source, id)\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  const loadData = (id: string, page: number): Promise<ListDetailInfo> => {\n    let key = `sdetail__${source}__${id}__${page}`\n    if (isRefresh && cache.has(key)) cache.delete(key)\n    return cache.has(key)\n      ? Promise.resolve(cache.get(key))\n      : musicSdk[source]?.songList.getListDetail(id, page).then((result: ListDetailInfo) => {\n        result.list = markRawList(deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[]))\n        cache.set(key, result)\n        return result\n      }) ?? Promise.reject(new Error('source not found' + source))\n  }\n  // eslint-disable-next-line @typescript-eslint/promise-function-async\n  return loadData(id, 1).then((result: ListDetailInfo) => {\n    if (result.total <= result.limit) return result.list\n\n    let maxPage = Math.ceil(result.total / result.limit)\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    const loadDetail = (loadPage = 2): Promise<ListDetailInfo['list']> => {\n      return loadPage == maxPage\n        ? loadData(id, loadPage).then((result: ListDetailInfo) => result.list)\n        // eslint-disable-next-line @typescript-eslint/promise-function-async\n        : loadData(id, loadPage).then((result1: ListDetailInfo) => loadDetail(++loadPage).then((result2: ListDetailInfo['list']) => [...result1.list, ...result2]))\n    }\n    return loadDetail().then(result2 => [...result.list, ...result2])\n  }).then((list: ListDetailInfo['list']) => deduplicationList(list))\n}\n\n\n/**\n * 获取并设置歌单内单页歌曲\n * @param id 歌单id\n * @param source 歌单源\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getAndSetListDetail = async(id: string, source: LX.OnlineSource, page: number, isRefresh = false) => {\n  let key = `sdetail__${source}__${id}__${page}`\n\n  if (!isRefresh && listDetailInfo.key == key && listDetailInfo.list.length) return\n\n  listDetailInfo.key = key\n  listDetailInfo.noItemLabel = window.i18n.t('list__loading')\n\n  return getListDetail(id, source, page, isRefresh).then((result: ListDetailInfo) => {\n    if (key != listDetailInfo.key) return\n    setListDetail(result, id, page)\n  }).catch((error: any) => {\n    clearListDetail()\n    listDetailInfo.noItemLabel = window.i18n.t('list__load_failed')\n    console.log(error)\n    throw error\n  })\n}\n\nexport const setVisibleListDetail = (visible: boolean) => {\n  isVisibleListDetail.value = visible\n}\n\nexport const setOpenSongListInputInfo = (text: string, source: string) => {\n  openSongListInputInfo.text = text\n  openSongListInputInfo.source = source\n}\n"
  },
  {
    "path": "src/renderer/store/songList/state.ts",
    "content": "import { reactive, markRaw, ref, shallowReactive } from '@common/utils/vueTools'\nimport music from '@renderer/utils/musicSdk'\n\nexport interface SortInfo {\n  name: string\n  id: string\n}\n\nexport const sources: LX.OnlineSource[] = markRaw([])\nexport const sortList = markRaw<Partial<Record<LX.OnlineSource, SortInfo[]>>>({})\n\nfor (const source of music.sources) {\n  const songList = music[source.id as LX.OnlineSource]?.songList\n  if (!songList) continue\n  sources.push(source.id as LX.OnlineSource)\n  sortList[source.id as LX.OnlineSource] = songList.sortList as SortInfo[]\n}\n\nexport interface TagInfoItem<T extends LX.OnlineSource = LX.OnlineSource> {\n  parent_id: string\n  parent_name: string\n  id: string\n  name: string\n  source: T\n}\nexport interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {\n  name: string\n  list: Array<TagInfoItem<T>>\n}\nexport interface TagInfo<Source extends LX.OnlineSource = LX.OnlineSource> {\n  tags: Array<TagInfoTypeItem<Source>>\n  hotTag: Array<TagInfoItem<Source>>\n  source: Source\n}\n\ntype Tags = Partial<Record<LX.OnlineSource, TagInfo>>\n\nexport const tags = shallowReactive<Tags>({})\n\n\nexport interface ListInfoItem {\n  play_count: string\n  id: string\n  author: string\n  name: string\n  time?: string\n  img: string\n  // grade: basic.favorcnt / 10,\n  desc: string | null\n  source: LX.OnlineSource\n  total?: string\n}\nexport interface ListInfo {\n  list: ListInfoItem[]\n  total: number\n  page: number\n  limit: number\n  key: string | null\n  noItemLabel: string\n  source?: LX.OnlineSource\n  tagId: string\n  sortId: string\n}\n\nexport interface ListDetailInfo {\n  list: LX.Music.MusicInfoOnline[]\n  source: LX.OnlineSource\n  desc: string | null\n  total: number\n  page: number\n  limit: number\n  key: string | null\n  id: string\n  info: {\n    name?: string\n    img?: string\n    desc?: string\n    author?: string\n    play_count?: string\n  }\n  noItemLabel: string\n}\n\nexport const listInfo = reactive<ListInfo>({\n  list: [],\n  total: 0,\n  page: 1,\n  limit: 30,\n  key: null,\n  noItemLabel: '',\n  source: 'kw',\n  tagId: '',\n  sortId: '',\n})\n\nexport const listDetailInfo = reactive<ListDetailInfo>({\n  list: [],\n  id: '',\n  desc: null,\n  total: 0,\n  page: 1,\n  limit: 30,\n  key: null,\n  source: 'kw',\n  info: {},\n  noItemLabel: '',\n})\n\nexport const selectListInfo = markRaw<ListInfoItem>({\n  play_count: '',\n  id: '',\n  author: '',\n  name: '',\n  time: '',\n  img: '',\n  // grade: basic.favorcnt / 10,\n  desc: '',\n  source: 'kw',\n})\n\nexport const isVisibleListDetail = ref(false)\nexport const openSongListInputInfo = markRaw({\n  text: '',\n  source: '',\n})\n"
  },
  {
    "path": "src/renderer/store/soundEffect.ts",
    "content": "import { reactive, toRaw } from '@common/utils/vueTools'\nimport {\n  getUserSoundEffectConvolutionPresetList,\n  getUserSoundEffectEQPresetList,\n  // getUserSoundEffectPitchShifterPresetList,\n  saveUserSoundEffectConvolutionPresetList,\n  saveUserSoundEffectEQPresetList,\n  // saveUserSoundEffectPitchShifterPresetList,\n} from '@renderer/utils/ipc'\n\nlet userEqPresetList: LX.SoundEffect.EQPreset[] | null = null\n\nexport const getUserEQPresetList = async() => {\n  if (userEqPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userEqPresetList = reactive(await getUserSoundEffectEQPresetList())\n  }\n  return userEqPresetList\n}\nexport const saveUserEQPreset = async(preset: LX.SoundEffect.EQPreset) => {\n  if (userEqPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userEqPresetList = reactive(await getUserSoundEffectEQPresetList())\n  }\n  const target = userEqPresetList.find(p => p.id == preset.id)\n  if (target) Object.assign(target, preset)\n  else userEqPresetList.push(preset)\n  saveUserSoundEffectEQPresetList(toRaw(userEqPresetList))\n}\nexport const removeUserEQPreset = async(id: string) => {\n  if (userEqPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userEqPresetList = reactive(await getUserSoundEffectEQPresetList())\n  }\n  const index = userEqPresetList.findIndex(p => p.id == id)\n  if (index < 0) return\n  userEqPresetList.splice(index, 1)\n  saveUserSoundEffectEQPresetList(toRaw(userEqPresetList))\n}\n\n\nlet userConvolutionPresetList: LX.SoundEffect.ConvolutionPreset[] | null = null\nexport const getUserConvolutionPresetList = async() => {\n  if (userConvolutionPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())\n  }\n  return userConvolutionPresetList\n}\nexport const saveUserConvolutionPreset = async(preset: LX.SoundEffect.ConvolutionPreset) => {\n  if (userConvolutionPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())\n  }\n  const target = userConvolutionPresetList.find(p => p.id == preset.id)\n  if (target) Object.assign(target, preset)\n  else userConvolutionPresetList.push(preset)\n  saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))\n}\nexport const removeUserConvolutionPreset = async(id: string) => {\n  if (userConvolutionPresetList == null) {\n    // eslint-disable-next-line require-atomic-updates\n    userConvolutionPresetList = reactive(await getUserSoundEffectConvolutionPresetList())\n  }\n  const index = userConvolutionPresetList.findIndex(p => p.id == id)\n  if (index < 0) return\n  userConvolutionPresetList.splice(index, 1)\n  saveUserSoundEffectConvolutionPresetList(toRaw(userConvolutionPresetList))\n}\n\n\n// let userPitchShifterPresetList: LX.SoundEffect.PitchShifterPreset[] | null = null\n// export const getUserPitchShifterPresetList = async() => {\n//   if (userEqPresetList == null) {\n//     userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())\n//   }\n//   return userPitchShifterPresetList\n// }\n// export const saveUserPitchShifterPreset = async(preset: LX.SoundEffect.PitchShifterPreset) => {\n//   if (userPitchShifterPresetList == null) {\n//     userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())\n//   }\n//   const target = userPitchShifterPresetList.find(p => p.id == preset.id)\n//   if (target) Object.assign(target, preset)\n//   else userPitchShifterPresetList.push(preset)\n//   saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))\n// }\n// export const removeUserPitchShifterPreset = async(id: string) => {\n//   if (userPitchShifterPresetList == null) {\n//     userPitchShifterPresetList = reactive(await getUserSoundEffectPitchShifterPresetList())\n//   }\n//   const index = userPitchShifterPresetList.findIndex(p => p.id == id)\n//   if (index < 0) return\n//   userPitchShifterPresetList.splice(index, 1)\n//   saveUserSoundEffectPitchShifterPresetList(toRaw(userPitchShifterPresetList))\n// }\n"
  },
  {
    "path": "src/renderer/store/utils.ts",
    "content": "// import { getListFromState } from './list'\n// import { downloadList } from './download'\n\n\n// export const getList = (listId: string | null): LX.Download.ListItem[] | LX.Music.MusicInfo[] => {\n//   return listId == 'download' ? downloadList : getListFromState(listId)\n// }\nimport { encodePath, isUrl } from '@common/utils/common'\nimport { joinPath } from '@common/utils/nodejs'\nimport { markRaw, shallowReactive } from '@common/utils/vueTools'\nimport { getThemes as getTheme } from '@renderer/utils/ipc'\nimport { qualityList, themeInfo, themeShouldUseDarkColors } from './index'\n\nexport const assertApiSupport = (source: LX.Source): boolean => {\n  return source == 'local' || qualityList.value[source] != null\n}\n\nexport const buildBgUrl = (originUrl: string, dataPath: string): string => {\n  return isUrl(originUrl)\n    ? `url(${originUrl})`\n    : `url(file:///${encodePath(joinPath(dataPath, originUrl).replaceAll('\\\\', '/'))})`\n}\n\nexport const getThemes = (callback: (themeInfo: LX.ThemeInfo) => void) => {\n  if (themeInfo.themes.length) {\n    callback(themeInfo)\n    return\n  }\n  void getTheme().then(info => {\n    themeInfo.themes = markRaw(info.themes)\n    themeInfo.userThemes = shallowReactive(info.userThemes)\n    themeInfo.dataPath = info.dataPath\n    callback(themeInfo)\n  })\n}\nexport const buildThemeColors = (theme: LX.Theme, dataPath: string) => {\n  if (theme.isCustom && theme.config.extInfo['--background-image'] != 'none') {\n    theme = copyTheme(theme)\n    theme.config.extInfo['--background-image'] = buildBgUrl(theme.config.extInfo['--background-image'], dataPath)\n  }\n  const colors: Record<string, string> = {\n    ...theme.config.themeColors,\n    ...theme.config.extInfo,\n  }\n\n  return colors\n}\n\nexport const copyTheme = (theme: LX.Theme): LX.Theme => {\n  return {\n    ...theme,\n    config: {\n      ...theme.config,\n      extInfo: { ...theme.config.extInfo },\n      themeColors: { ...theme.config.themeColors },\n    },\n  }\n}\n\nexport const findTheme = (themeInfo: LX.ThemeInfo, id: string): LX.Theme | undefined => {\n  let theme = themeInfo.themes.find(theme => theme.id == id)\n  if (theme) return theme\n  theme = themeInfo.userThemes.find(theme => theme.id == id)\n  return theme\n}\n\nexport const applyTheme = (id: string, lightId: string, darkId: string, dataPath: string) => {\n  getThemes((themeInfo) => {\n    let themeId = id == 'auto'\n      ? themeShouldUseDarkColors.value\n        ? darkId\n        : lightId\n      : id\n\n    let theme = findTheme(themeInfo, themeId)\n    if (!theme) {\n      themeId = id == 'auto' && themeShouldUseDarkColors.value ? 'black' : 'green'\n      theme = themeInfo.themes.find(theme => theme.id == themeId)!\n    }\n    window.setTheme(buildThemeColors(theme, dataPath))\n  })\n}\n"
  },
  {
    "path": "src/renderer/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"isolatedModules\": true,\n    \"paths\": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */\n      \"@common/*\": [\"common/*\"],\n      \"@renderer/*\": [\"renderer/*\"],\n      // \"@lyric/*\": [\"renderer-lyric/*\"],\n      \"@static/*\": [\"static/*\"],\n      \"@root/*\": [\"./*\"],\n    },\n    \"typeRoots\": [                                       /* Specify multiple folders that act like './node_modules/@types'. */\n      \"./types\"\n    ],\n  },\n  \"vueCompilerOptions\": {\n    \"plugins\": [\n      \"@vue/language-plugin-pug\"\n    ]\n  }\n  // \"include\": [\n\t//   \"./**/*.ts\",\n\t//   // \"./**/*.js\",\n\t//   \"./**/*.vue\",\n\t//   \"./**/*.json\",\n\t// ],\n}\n"
  },
  {
    "path": "src/renderer/types/app.d.ts",
    "content": "/* eslint-disable no-var */\nimport { type AppEventTypes, type KeyEventTypes } from '@renderer/event'\nimport { type MainTypes, type DownloadTypes } from '@renderer/worker/utils'\nimport { type I18n } from '@renderer/plugins/i18n'\n// interface LX.EnvParams {\n//   deeplink?: string | null\n//   cmdParams: LX.CmdParams\n//   workAreaSize?: Electron.Size\n// }\n\n\ninterface Lx {\n  // appSetting: LX.AppSetting\n  isEditingHotKey: boolean\n  isPlayedStop: boolean\n  appHotKeyConfig: LX.HotKeyConfigAll\n  restorePlayInfo: LX.Player.SavedPlayInfo | null\n  worker: {\n    main: MainTypes\n    download: DownloadTypes\n  }\n  isProd: boolean\n  songListInfo: {\n    fromName: string\n    searchKey: string | null\n    searchPosition?: number\n    songlistKey: string | null\n    songlistPosition?: number\n  }\n  rootOffset: number\n  apiInitPromise: [Promise<boolean>, boolean, (success: boolean) => void]\n}\n\ndeclare global {\n  interface Window {\n    ELECTRON_DISABLE_SECURITY_WARNINGS?: string\n    dt: boolean\n    shouldUseDarkColors: boolean\n    lx: Lx\n    app_event: AppEventTypes\n    key_event: KeyEventTypes\n    i18n: I18n\n\n    lxData: any\n\n    setTheme: (colors: Record<string, string>) => void\n    setLang: (lang?: string) => void\n  }\n\n  module NodeJS {\n    interface ProcessVersions {\n      app: string\n    }\n  }\n\n  // const ENVIRONMENT: NodeJS.ProcessEnv\n\n\n  namespace LX {\n    interface KeyDownEevent {\n      /**\n       * 原始事件\n       */\n      event: KeyEvent | null\n\n      /**\n       * 按下的按键数组\n       */\n      keys: string[]\n\n      /**\n       * 按下的按键组合\n       *\n       * 类似：`shift`、`mod+a`\n       *\n       * 其中 `Ctrl` 的名称为 `mod`， 对应 MacOS 上的 `Command` 键\n       */\n      key: string\n      /**\n       * 当前触发此事件的单个按键（不包括之前已按下的键）\n       */\n      eventKey: string\n      /**\n       * 按键操作类型\n       */\n      type: 'down' | 'up'\n    }\n\n    type LyricFormat = 'gbk' | 'utf8'\n\n    class KeyEvent extends KeyboardEvent {\n      /**\n       * 此事件是否标记为 已被处理，如果设置为`true`，则停止触发key event事件\n       */\n      lx_handled?: boolean\n    }\n  }\n\n  var COMMIT_ID: string\n  var COMMIT_DATE: string\n}\n\n\n// declare const ELECTRON_DISABLE_SECURITY_WARNINGS: string\n// declare const userApiPath: string\n"
  },
  {
    "path": "src/renderer/types/common.d.ts",
    "content": "import '@common/types/app_setting'\nimport '@common/types/common'\nimport '@common/types/user_api'\nimport '@common/types/sync'\nimport '@common/types/list_sync'\nimport '@common/types/music'\nimport '@common/types/list'\nimport '@common/types/download_list'\nimport '@common/types/player'\nimport '@common/types/shims_vue'\nimport '@common/types/utils'\nimport '@common/types/theme'\nimport '@common/types/desktop_lyric'\nimport '@common/types/ipc_renderer'\nimport '@common/types/config_files'\nimport '@common/types/music_metadata'\nimport '@common/types/sound_effect'\nimport '@common/types/dislike_list'\nimport '@common/types/open_api'\n"
  },
  {
    "path": "src/renderer/types/i18n.d.ts",
    "content": "import { type I18n } from '@root/lang'\n\ndeclare module 'vue' {\n  interface ComponentCustomProperties {\n    $t: I18n['t']\n  }\n}\n"
  },
  {
    "path": "src/renderer/types/player.d.ts",
    "content": "\ndeclare namespace LX {\n  namespace Player {\n    interface PlayMusicInfo {\n      /**\n       * 当前播放歌曲的列表 id\n       */\n      musicInfo: LX.Download.ListItem | LX.Music.MusicInfo\n      /**\n        * 当前播放歌曲的列表 id\n        */\n      listId: string | null\n      /**\n        * 是否属于 “稍后播放”\n        */\n      isTempPlay: boolean\n    }\n\n    interface PlayInfo {\n      /**\n       * 当前正在播放歌曲 index\n       */\n      playIndex: number\n      /**\n      * 播放器的播放列表 id\n      */\n      playerListId: string | null\n      /**\n      * 播放器播放歌曲 index\n      */\n      playerPlayIndex: number\n    }\n\n    interface TempPlayListItem {\n      /**\n       * 播放列表id\n       */\n      listId: string | null\n      /**\n       * 歌曲信息\n       */\n      musicInfo: LX.Music.MusicInfo | LX.Download.ListItem\n      /**\n       * 是否添加到列表顶部\n       */\n      isTop?: boolean\n    }\n\n    interface SavedPlayInfo {\n      time: number\n      maxTime: number\n      listId: string\n      index: number\n    }\n\n  }\n}\n"
  },
  {
    "path": "src/renderer/types/worker.d.ts",
    "content": "import { type workerMainTypes } from '@renderer/worker/main/index'\nimport { type workerDownloadTypes } from '@renderer/worker/download/index'\n\n\ndeclare global {\n  namespace LX {\n    type WorkerMainTypes = workerMainTypes\n    type WorkerDownloadTypes = workerDownloadTypes\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useDrag.js",
    "content": "import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm'\nimport { onMounted } from '@common/utils/vueTools'\nimport { clearDownKeys } from '@renderer/event'\n\nSortable.mount(new AutoScroll())\n\nconst noop = () => {}\n\nexport default ({ dom_list, dragingItemClassName, filter, onUpdate, onStart = noop, onEnd = noop }) => {\n  let sortable\n\n  onMounted(() => {\n    sortable = Sortable.create(dom_list.value, {\n      animation: 150,\n      disabled: true,\n      forceFallback: false,\n      filter: filter ? '.' + filter : null,\n      ghostClass: dragingItemClassName,\n      onUpdate(event) {\n        onUpdate(event.newIndex, event.oldIndex)\n      },\n      onMove(event) {\n        return filter ? !event.related.classList.contains(filter) : true\n      },\n      onChoose() {\n        onStart()\n      },\n      onUnchoose() {\n        onEnd()\n        // 处于拖动状态期间，键盘事件无法监听，拖动结束手动清理按下的键\n        // window.app_event.emit(eventBaseName.setClearDownKeys)\n        clearDownKeys()\n      },\n      onStart(event) {\n        window.app_event.dragStart()\n      },\n      onEnd(event) {\n        window.app_event.dragEnd()\n      },\n    })\n  })\n\n  return {\n    setDisabled(enable) {\n      if (!sortable) return\n      sortable.option('disabled', enable)\n    },\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useIconSize.ts",
    "content": "import { type Ref, onBeforeUnmount, onMounted, ref } from '@common/utils/vueTools'\n\nconst onDomSizeChanged = (dom: HTMLElement, onChanged: (width: number, height: number) => void) => {\n  // 使用 ResizeObserver 监听大小变化\n  const resizeObserver = new ResizeObserver(entries => {\n    for (let entry of entries) {\n      const { width, height } = entry.contentRect\n      // console.log(dom.offsetLeft, dom.offsetTop, left, top, width, height)\n      onChanged(Math.trunc(width), Math.trunc(height))\n    }\n  })\n\n  resizeObserver.observe(dom)\n\n  onChanged(dom.clientWidth, dom.clientHeight)\n\n  return () => {\n    resizeObserver.disconnect()\n  }\n}\n\nexport const useIconSize = (parentDom: Ref<HTMLElement | undefined>, size: number) => {\n  const iconSize = ref('32px')\n  let unsub: (() => void) | null = null\n\n  onMounted(() => {\n    if (!parentDom.value) return\n    unsub = onDomSizeChanged(parentDom.value, (width, height) => {\n      iconSize.value = Math.trunc(width * size) + 'px'\n    })\n  })\n  onBeforeUnmount(() => {\n    unsub?.()\n  })\n\n  return iconSize\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useImportTip.js",
    "content": "import { useI18n } from '@renderer/plugins/i18n'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport default () => {\n  const t = useI18n()\n\n\n  return (type) => {\n    let message\n    switch (type) {\n      case 'defautlList':\n      case 'playList':\n      case 'playList_v2':\n        message = t('list_import_tip__playlist')\n        break\n      case 'setting':\n      case 'setting_v2':\n        message = t('list_import_tip__setting')\n        break\n      case 'allData':\n      case 'allData_v2':\n        message = t('list_import_tip__alldata')\n        break\n      case 'playListPart':\n      case 'playListPart_v2':\n        message = t('list_import_tip__playlist_part')\n        break\n\n      default:\n        message = t('list_import_tip__unknown')\n        break\n    }\n\n    dialog({\n      message,\n      confirmButtonText: t('ok'),\n    })\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useKeyDown.ts",
    "content": "import { onMounted, onBeforeUnmount, ref } from '@common/utils/vueTools'\n\nexport default (name: string) => {\n  const keyDown = ref(false)\n  const down = `key_${name}_down`\n  const up = `key_${name}_up`\n\n  const handle_key_down = (event: LX.KeyDownEevent) => {\n    if (!keyDown.value) {\n      // console.log(event)\n      switch ((event.event?.target as HTMLElement).tagName) {\n        case 'INPUT':\n        case 'SELECT':\n        case 'TEXTAREA':\n          return\n        default: if ((event.event?.target as HTMLElement).isContentEditable) return\n      }\n\n      keyDown.value = true\n    }\n  }\n\n  const handle_key_up = () => {\n    keyDown.value &&= false\n  }\n\n  onMounted(() => {\n    window.key_event.on(down, handle_key_down)\n    window.key_event.on(up, handle_key_up)\n  })\n\n  onBeforeUnmount(() => {\n    window.key_event.off(down, handle_key_down)\n    window.key_event.off(up, handle_key_up)\n  })\n\n  return keyDown\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useLyric.js",
    "content": "import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools'\nimport { throttle, formatPlayTime2 } from '@common/utils/common'\nimport { scrollTo } from '@common/utils/renderer'\nimport { play } from '@renderer/core/player/action'\nimport { appSetting } from '@renderer/store/setting'\n// import { player as eventPlayerNames } from '@renderer/event/names'\n\nexport default ({ isPlay, lyric, playProgress, isShowLyricProgressSetting, offset }) => {\n  const dom_lyric = ref(null)\n  const dom_lyric_text = ref(null)\n  const dom_skip_line = ref(null)\n  const isMsDown = ref(false)\n  const isStopScroll = ref(false)\n  const timeStr = ref('--/--')\n\n  let msDownY = 0\n  let msDownScrollY = 0\n  let timeout = null\n  let cancelScrollFn\n  let dom_lines\n  let isSetedLines = false\n  let point = {\n    x: null,\n    y: null,\n  }\n  let time = -1\n  let dom_pre_line = null\n  let isSkipMouseEnter = false\n\n  const handleSkipPlay = () => {\n    if (time == -1) return\n    handleSkipMouseLeave()\n    isStopScroll.value = false\n    window.app_event.setProgress(time)\n    if (!isPlay.value) play()\n  }\n  const handleSkipMouseEnter = () => {\n    isSkipMouseEnter = true\n    clearLyricScrollTimeout()\n  }\n  const handleSkipMouseLeave = () => {\n    isSkipMouseEnter = false\n    startLyricScrollTimeout()\n  }\n\n  const throttleSetTime = throttle(() => {\n    if (!dom_skip_line.value) return\n    const rect = dom_skip_line.value.getBoundingClientRect()\n    point.x = rect.x\n    point.y = rect.y\n    let dom = document.elementFromPoint(point.x, point.y)\n    if (dom_pre_line === dom) return\n    if (dom.tagName == 'SPAN') {\n      dom = dom.parentNode.parentNode\n    } else if (dom.classList.contains('line')) {\n      dom = dom.parentNode\n    }\n    if (dom.time == null) {\n      if (lyric.lines.length) {\n        time = dom.classList.contains('pre') ? 0 : lyric.lines[lyric.lines.length - 1].time ?? 0\n        time = Math.max(time - lyric.offset - lyric.tempOffset, 0)\n        time /= 1000\n        if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime\n        timeStr.value = formatPlayTime2(time)\n      } else {\n        time = -1\n        timeStr.value = '--:--'\n      }\n    } else {\n      time = dom.time\n      time = Math.max(time - lyric.offset - lyric.tempOffset, 0)\n      time /= 1000\n      if (time > playProgress.maxPlayTime) time = playProgress.maxPlayTime\n      timeStr.value = formatPlayTime2(time)\n    }\n    dom_pre_line = dom\n  })\n  const setTime = () => {\n    if (isShowLyricProgressSetting.value) throttleSetTime()\n  }\n\n  const handleScrollLrc = (duration = 300) => {\n    if (!dom_lines?.length || !dom_lyric.value) return\n    if (isSkipMouseEnter) return\n    if (isStopScroll.value) return\n    let dom_p = dom_lines[lyric.line]\n    cancelScrollFn = scrollTo(dom_lyric.value, dom_p ? (dom_p.offsetTop - dom_lyric.value.clientHeight * 0.38) : 0, duration)\n  }\n  const clearLyricScrollTimeout = () => {\n    if (!timeout) return\n    clearTimeout(timeout)\n    timeout = null\n  }\n  const startLyricScrollTimeout = () => {\n    clearLyricScrollTimeout()\n    if (isSkipMouseEnter) return\n    timeout = setTimeout(() => {\n      timeout = null\n      isStopScroll.value = false\n      if (!isPlay.value) return\n      handleScrollLrc()\n    }, 3000)\n  }\n  const handleLyricDown = (y) => {\n    // console.log(event)\n    if (delayScrollTimeout) {\n      clearTimeout(delayScrollTimeout)\n      delayScrollTimeout = null\n    }\n    isMsDown.value = true\n    msDownY = y\n    msDownScrollY = dom_lyric.value.scrollTop\n  }\n  const handleLyricMouseDown = event => {\n    handleLyricDown(event.clientY)\n  }\n  const handleLyricTouchStart = event => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      handleLyricDown(touch.clientY)\n    }\n  }\n  const handleMouseMsUp = event => {\n    isMsDown.value = false\n  }\n  const handleMove = (y) => {\n    if (isMsDown.value) {\n      isStopScroll.value ||= true\n      if (cancelScrollFn) {\n        cancelScrollFn()\n        cancelScrollFn = null\n      }\n      dom_lyric.value.scrollTop = msDownScrollY + msDownY - y\n      startLyricScrollTimeout()\n      setTime()\n    }\n  }\n  const handleMouseMsMove = event => {\n    handleMove(event.clientY)\n  }\n  const handleTouchMove = (e) => {\n    if (e.changedTouches.length) {\n      const touch = e.changedTouches[0]\n      handleMove(touch.clientY)\n    }\n  }\n\n  const handleWheel = (event) => {\n    console.log(event.deltaY)\n    isStopScroll.value ||= true\n    if (cancelScrollFn) {\n      cancelScrollFn()\n      cancelScrollFn = null\n    }\n    dom_lyric.value.scrollTop = dom_lyric.value.scrollTop + event.deltaY\n    startLyricScrollTimeout()\n    setTime()\n  }\n\n  const setLyric = (lines) => {\n    const dom_line_content = document.createDocumentFragment()\n    for (const line of lines) {\n      dom_line_content.appendChild(line.dom_line)\n    }\n    dom_lyric_text.value.textContent = ''\n    dom_lyric_text.value.appendChild(dom_line_content)\n    nextTick(() => {\n      dom_lines = dom_lyric.value.querySelectorAll('.line-content')\n      handleScrollLrc()\n    })\n  }\n\n  const initLrc = (lines, oLines) => {\n    isSetedLines = true\n    if (oLines) {\n      if (lines.length) {\n        setLyric(lines)\n      } else {\n        cancelScrollFn = scrollTo(dom_lyric.value, 0, 300, () => {\n          if (lyric.lines !== lines) return\n          setLyric(lines)\n        }, 50)\n      }\n    } else {\n      setLyric(lines)\n    }\n  }\n\n  let delayScrollTimeout\n  const scrollLine = (line, oldLine) => {\n    if (line < 0) return\n    if (line == 0 && isSetedLines) return isSetedLines = false\n    isSetedLines &&= false\n    if (oldLine == null || line - oldLine != 1) return handleScrollLrc()\n\n    if (appSetting['playDetail.isDelayScroll']) {\n      delayScrollTimeout = setTimeout(() => {\n        delayScrollTimeout = null\n        handleScrollLrc(600)\n      }, 600)\n    } else {\n      handleScrollLrc()\n    }\n  }\n\n  watch(() => lyric.lines, initLrc)\n  watch(() => lyric.line, scrollLine)\n\n  onMounted(() => {\n    document.addEventListener('mousemove', handleMouseMsMove)\n    document.addEventListener('mouseup', handleMouseMsUp)\n    document.addEventListener('touchmove', handleTouchMove)\n    document.addEventListener('touchend', handleMouseMsUp)\n\n    initLrc(lyric.lines, null)\n  })\n\n  onBeforeUnmount(() => {\n    document.removeEventListener('mousemove', handleMouseMsMove)\n    document.removeEventListener('mouseup', handleMouseMsUp)\n    document.removeEventListener('touchmove', handleTouchMove)\n    document.removeEventListener('touchend', handleMouseMsUp)\n  })\n\n  return {\n    dom_lyric,\n    dom_lyric_text,\n    dom_skip_line,\n    isStopScroll,\n    isMsDown,\n    timeStr,\n    handleLyricMouseDown,\n    handleLyricTouchStart,\n    handleWheel,\n    handleSkipPlay,\n    handleSkipMouseEnter,\n    handleSkipMouseLeave,\n    handleScrollLrc,\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useMenuLocation.js",
    "content": "import { onMounted, onBeforeUnmount, watch, reactive, ref } from '@common/utils/vueTools'\n\n\nexport default ({ visible, location, onHide }) => {\n  const transition1 = 'transform, opacity'\n  const transition2 = 'transform, opacity, top, left'\n  let show = false\n  const dom_menu = ref(null)\n  const menuStyles = reactive({\n    left: 0,\n    top: 0,\n    opacity: 0,\n    transitionProperty: 'transform, opacity',\n    transform: 'scale(.8, .7) translate(0,0)',\n    pointerEvents: 'none',\n  })\n\n  const handleShow = () => {\n    show = true\n    menuStyles.opacity = 1\n    menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.value.x, location.value.y)})`\n    menuStyles.pointerEvents = 'auto'\n  }\n  const handleHide = () => {\n    menuStyles.opacity = 0\n    menuStyles.transform = 'scale(.8, .7) translate(0, 0)'\n    menuStyles.pointerEvents = 'none'\n    show = false\n  }\n  const handleGetOffsetXY = (left, top) => {\n    const listWidth = dom_menu.value.clientWidth\n    const listHeight = dom_menu.value.clientHeight\n    const dom_container_parant = dom_menu.value.offsetParent\n    const containerWidth = dom_container_parant.clientWidth\n    const containerHeight = dom_container_parant.clientHeight\n    const offsetWidth = containerWidth - left - listWidth\n    const offsetHeight = containerHeight - top - listHeight\n    let x = 0\n    let y = 0\n    if (containerWidth > listWidth && offsetWidth < 12) {\n      x = offsetWidth - 12\n    }\n    if (containerHeight > listHeight && offsetHeight < 5) {\n      y = offsetHeight - 5\n    }\n    return `${x}px, ${y}px`\n  }\n  const handleDocumentClick = (event) => {\n    if (!show) return\n\n    if (event.target == dom_menu.value || dom_menu.value.contains(event.target)) return\n\n    if (show && menuStyles.transitionProperty != transition1) menuStyles.transitionProperty = transition1\n\n    onHide()\n  }\n\n  watch(visible, visible => {\n    visible ? handleShow() : handleHide()\n  }, { immediate: true })\n\n  watch(location, location => {\n    menuStyles.left = location.x - window.lx.rootOffset + 2 + 'px'\n    menuStyles.top = location.y - window.lx.rootOffset + 'px'\n    // nextTick(() => {\n    if (show) {\n      if (menuStyles.transitionProperty != transition2) menuStyles.transitionProperty = transition2\n      menuStyles.transform = `scale(1) translate(${handleGetOffsetXY(location.x, location.y)})`\n    }\n    // })\n  }, { deep: true })\n\n  onMounted(() => {\n    document.addEventListener('click', handleDocumentClick)\n  })\n\n  onBeforeUnmount(() => {\n    document.removeEventListener('click', handleDocumentClick)\n  })\n\n  return {\n    dom_menu,\n    menuStyles,\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useNextTogglePlay.ts",
    "content": "import { appSetting, setTogglePlayMode } from '@renderer/store/setting'\nimport {\n  computed,\n} from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\n\n// const playNextModes = [\n//   'listLoop',\n//   'random',\n//   'list',\n//   'singleLoop',\n//   'none',\n// ] as const\n\nexport default () => {\n  const t = useI18n()\n  const nextTogglePlayName = computed(() => {\n    switch (appSetting['player.togglePlayMethod']) {\n      case 'listLoop': return t('player__play_toggle_mode_list_loop')\n      case 'random': return t('player__play_toggle_mode_random')\n      case 'singleLoop': return t('player__play_toggle_mode_single_loop')\n      case 'list': return t('player__play_toggle_mode_list')\n      default: return t('player__play_toggle_mode_off')\n    }\n  })\n\n  const toggleNextPlayMode = (mode: LX.AppSetting['player.togglePlayMethod']) => {\n    if (mode == appSetting['player.togglePlayMethod']) return\n    // let index = playNextModes.indexOf(appSetting['player.togglePlayMethod'])\n    // if (++index >= playNextModes.length) index = 0\n    setTogglePlayMode(mode)\n  }\n\n  return {\n    nextTogglePlayName,\n    toggleNextPlayMode,\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/usePlayProgress.js",
    "content": "import { ref, onBeforeUnmount, toRef } from '@common/utils/vueTools'\nimport { playProgress } from '@renderer/store/player/playProgress'\n\nexport default () => {\n  const isActiveTransition = ref(false)\n  const progress = toRef(playProgress, 'progress')\n  const nowPlayTimeStr = toRef(playProgress, 'nowPlayTimeStr')\n  const maxPlayTimeStr = toRef(playProgress, 'maxPlayTimeStr')\n\n  const handleTransitionEnd = () => {\n    isActiveTransition.value = false\n  }\n  const handleActiveTransition = () => {\n    isActiveTransition.value = true\n  }\n\n  window.app_event.on('activePlayProgressTransition', handleActiveTransition)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('activePlayProgressTransition', handleActiveTransition)\n  })\n\n  return {\n    nowPlayTimeStr,\n    maxPlayTimeStr,\n    progress,\n    isActiveTransition,\n    handleTransitionEnd,\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/compositions/useToggleDesktopLyric.js",
    "content": "import {\n  computed,\n} from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, setLockDesktopLyric, setVisibleDesktopLyric } from '@renderer/store/setting'\n\nexport default () => {\n  const t = useI18n()\n\n  const toggleDesktopLyricBtnTitle = computed(() => {\n    return `${\n      appSetting['desktopLyric.enable']\n        ? t('player__desktop_lyric_off')\n        : t('player__desktop_lyric_on')\n    }\\n(${\n      appSetting['desktopLyric.isLock']\n        ? t('player__desktop_lyric_unlock')\n        : t('player__desktop_lyric_lock')\n    })`\n  })\n\n  const toggleDesktopLyric = () => {\n    setVisibleDesktopLyric(!appSetting['desktopLyric.enable'])\n  }\n  const toggleLockDesktopLyric = () => {\n    setLockDesktopLyric(!appSetting['desktopLyric.isLock'])\n  }\n\n  return {\n    toggleDesktopLyricBtnTitle,\n    toggleDesktopLyric,\n    toggleLockDesktopLyric,\n  }\n}\n"
  },
  {
    "path": "src/renderer/utils/data.ts",
    "content": "/* eslint-disable @typescript-eslint/no-dynamic-delete */\nimport {\n  saveListPositionInfo as saveListPositionInfoFromData,\n  getListPositionInfo as getListPositionInfoFromData,\n  saveListPrevSelectId as saveListPrevSelectIdFromData,\n  getListPrevSelectId as getListPrevSelectIdFromData,\n  saveListUpdateInfo as saveListUpdateInfoFromData,\n  getListUpdateInfo as getListUpdateInfoFromData,\n  saveSearchSetting as saveSearchSettingFromData,\n  getSearchSetting as getSearchSettingFromData,\n  saveSongListSetting as saveSongListSettingFromData,\n  getSongListSetting as getSongListSettingFromData,\n  saveLeaderboardSetting as saveLeaderboardSettingFromData,\n  getLeaderboardSetting as getLeaderboardSettingFromData,\n  saveViewPrevState as saveViewPrevStateFromData,\n} from '@renderer/utils/ipc'\nimport { throttle } from '@common/utils'\nimport { type DEFAULT_SETTING, LIST_IDS } from '@common/constants'\nimport { dateFormat } from './index'\nimport { setUpdateTime } from '@renderer/store/list/action'\n\nlet listPosition: LX.List.ListPositionInfo\nlet listPrevSelectId: string\nlet listUpdateInfo: LX.List.ListUpdateInfo\n\nlet searchSetting: typeof DEFAULT_SETTING['search']\nlet songListSetting: typeof DEFAULT_SETTING['songList']\nlet leaderboardSetting: typeof DEFAULT_SETTING['leaderboard']\n\nconst saveListPositionThrottle = throttle(() => {\n  saveListPositionInfoFromData(listPosition)\n}, 1000)\nconst saveSearchSettingThrottle = throttle(() => {\n  saveSearchSettingFromData(searchSetting)\n}, 1000)\nconst saveSongListSettingThrottle = throttle(() => {\n  saveSongListSettingFromData(songListSetting)\n}, 1000)\nconst saveLeaderboardSettingThrottle = throttle(() => {\n  saveLeaderboardSettingFromData(leaderboardSetting)\n}, 1000)\nconst saveViewPrevStateThrottle = throttle((state) => {\n  saveViewPrevStateFromData(state)\n}, 1000)\n\nconst initPosition = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  listPosition ??= await getListPositionInfoFromData() ?? {}\n}\nexport const getListPosition = async(id: string): Promise<number> => {\n  await initPosition()\n  return listPosition[id] ?? 0\n}\nexport const setListPosition = async(id: string, position?: number) => {\n  await initPosition()\n  listPosition[id] = position ?? 0\n  saveListPositionThrottle()\n}\nexport const removeListPosition = async(id: string) => {\n  await initPosition()\n  if (listPosition[id] == null) return\n  delete listPosition[id]\n  saveListPositionThrottle()\n}\nexport const overwriteListPosition = async(ids: string[]) => {\n  await initPosition()\n  const removedIds = []\n  for (const id of Object.keys(listPosition)) {\n    if (ids.includes(id)) continue\n    removedIds.push(id)\n  }\n  for (const id of removedIds) delete listPosition[id]\n  saveListPositionThrottle()\n}\n\nconst saveListPrevSelectIdThrottle = throttle(() => {\n  saveListPrevSelectIdFromData(listPrevSelectId)\n}, 200)\nexport const getListPrevSelectId = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  listPrevSelectId ??= await getListPrevSelectIdFromData() ?? LIST_IDS.DEFAULT\n  return listPrevSelectId ?? LIST_IDS.DEFAULT\n}\nexport const saveListPrevSelectId = (id: string) => {\n  listPrevSelectId = id\n  saveListPrevSelectIdThrottle()\n}\n\nconst saveListUpdateInfo = throttle(() => {\n  saveListUpdateInfoFromData(listUpdateInfo)\n}, 1000)\n\nconst initListUpdateInfo = async() => {\n  if (listUpdateInfo == null) {\n    // eslint-disable-next-line require-atomic-updates\n    listUpdateInfo = await getListUpdateInfoFromData() ?? {}\n    for (const [id, info] of Object.entries(listUpdateInfo)) {\n      setUpdateTime(id, info.updateTime ? dateFormat(info.updateTime) : '')\n    }\n  }\n}\nexport const getListUpdateInfo = async() => {\n  await initListUpdateInfo()\n  return listUpdateInfo\n}\nexport const setListUpdateInfo = async(info: LX.List.ListUpdateInfo) => {\n  await initListUpdateInfo()\n  listUpdateInfo = info\n  saveListUpdateInfo()\n}\nexport const setListAutoUpdate = async(id: string, enable: boolean) => {\n  await initListUpdateInfo()\n  const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false }\n  targetInfo.isAutoUpdate = enable\n  listUpdateInfo[id] = targetInfo\n  saveListUpdateInfo()\n}\nexport const setListUpdateTime = async(id: string, time: number) => {\n  await initListUpdateInfo()\n  const targetInfo = listUpdateInfo[id] ?? { updateTime: 0, isAutoUpdate: false }\n  targetInfo.updateTime = time\n  listUpdateInfo[id] = targetInfo\n  saveListUpdateInfo()\n}\n// export const setListUpdateInfo = (id, { updateTime, isAutoUpdate }) => {\n//   listUpdateInfo[id] = { updateTime, isAutoUpdate }\n//   saveListUpdateInfo()\n// }\nexport const removeListUpdateInfo = async(id: string) => {\n  await initListUpdateInfo()\n  if (listUpdateInfo[id] == null) return\n  delete listUpdateInfo[id]\n  saveListUpdateInfo()\n}\nexport const overwriteListUpdateInfo = async(ids: string[]) => {\n  await initListUpdateInfo()\n  const removedIds = []\n  for (const id of Object.keys(listUpdateInfo)) {\n    if (ids.includes(id)) continue\n    removedIds.push(id)\n  }\n  for (const id of removedIds) delete listUpdateInfo[id]\n  saveListUpdateInfo()\n}\n\n\nexport const getSearchSetting = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  searchSetting ??= await getSearchSettingFromData()\n  return { ...searchSetting }\n}\nexport const setSearchSetting = async(setting: Partial<typeof DEFAULT_SETTING['search']>) => {\n  if (!searchSetting) await getSearchSetting()\n  let requiredSave = false\n  if (setting.source && searchSetting.source != setting.source) requiredSave = true\n  if (setting.type && searchSetting.type != setting.type) requiredSave = true\n  if (setting.temp_source && searchSetting.temp_source != setting.temp_source) requiredSave = true\n\n  if (!requiredSave) return\n  searchSetting = Object.assign(searchSetting, setting)\n  saveSearchSettingThrottle()\n}\n\nexport const getSongListSetting = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  songListSetting ??= await getSongListSettingFromData()\n  return { ...songListSetting }\n}\nexport const setSongListSetting = async(setting: Partial<typeof DEFAULT_SETTING['songList']>) => {\n  if (!songListSetting) await getSongListSetting()\n  songListSetting = Object.assign(songListSetting, setting)\n  saveSongListSettingThrottle()\n}\n\nexport const getLeaderboardSetting = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  leaderboardSetting ??= await getLeaderboardSettingFromData()\n  return { ...leaderboardSetting }\n}\nexport const setLeaderboardSetting = async(setting: Partial<typeof DEFAULT_SETTING['leaderboard']>) => {\n  if (!leaderboardSetting) await getLeaderboardSetting()\n  leaderboardSetting = Object.assign(leaderboardSetting, setting)\n  saveLeaderboardSettingThrottle()\n}\n\nexport const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => {\n  saveViewPrevStateThrottle(state)\n}\n"
  },
  {
    "path": "src/renderer/utils/env.js",
    "content": "const isDev = process.env.NODE_ENV === 'development'\n\nexport const debug = isDev && true\nexport const debugRequest = isDev && false\nexport const debugDownload = isDev && false\n"
  },
  {
    "path": "src/renderer/utils/index.ts",
    "content": "import { dateFormat } from '@common/utils/common'\n\nexport * from '@common/utils/renderer'\nexport * from '@common/utils/nodejs'\nexport * from '@common/utils/common'\nexport * from '@common/utils/tools'\n\n/**\n * 格式化播放数量\n * @param {*} num 数字\n */\nexport const formatPlayCount = (num: number): string => {\n  if (num > 100000000) return `${Math.trunc(num / 10000000) / 10}亿`\n  if (num > 10000) return `${Math.trunc(num / 1000) / 10}万`\n  return String(num)\n}\n\n\n/**\n * 时间格式化\n */\nexport const dateFormat2 = (time: number): string => {\n  let differ = Math.trunc((Date.now() - time) / 1000)\n  if (differ < 60) {\n    return window.i18n.t('date_format_second', { num: differ })\n  } else if (differ < 3600) {\n    return window.i18n.t('date_format_minute', { num: Math.trunc(differ / 60) })\n  } else if (differ < 86400) {\n    return window.i18n.t('date_format_hour', { num: Math.trunc(differ / 3600) })\n  } else {\n    return dateFormat(time)\n  }\n}\n\n\n/**\n * 设置标题\n */\nlet dom_title = document.getElementsByTagName('title')[0]\nexport const setTitle = (title: string | null) => {\n  title ||= 'LX Music'\n  dom_title.innerText = title\n}\n\n\n// export const getProxyInfo = () => {\n//   return proxy.enable && proxy.host\n//     ? `http://${proxy.username}:${proxy.password}@${proxy.host}:${proxy.port}`\n//     : proxy.envProxy\n//       ? `http://${proxy.envProxy.host}:${proxy.envProxy.port}`\n//       : undefined\n// }\n\n\nexport const getFontSizeWithScreen = (screenWidth: number = window.innerWidth): number => {\n  return screenWidth <= 1440\n    ? 16\n    : screenWidth <= 1920\n      ? 18\n      : screenWidth <= 2560\n        ? 20\n        : screenWidth <= 2560 ? 20 : 22\n}\n\n\nexport const deduplicationList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {\n  const ids = new Set<string>()\n  return list.filter(s => {\n    if (ids.has(s.id)) return false\n    ids.add(s.id)\n    return true\n  })\n}\n\nexport const langS2T = async(str: string) => {\n  return window.lx.worker.main.langS2t(Buffer.from(str).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())\n}\n\nexport const decodeName = (str: string | null = '') => {\n  if (!str) return ''\n  return new window.DOMParser().parseFromString(str, 'text/html').body.textContent\n}\n"
  },
  {
    "path": "src/renderer/utils/ipc.ts",
    "content": "import { rendererSend, rendererInvoke, rendererOn, rendererOff } from '@common/rendererIpc'\nimport { HOTKEY_RENDERER_EVENT_NAME, WIN_MAIN_RENDERER_EVENT_NAME, CMMON_EVENT_NAME } from '@common/ipcNames'\nimport { type ProgressInfo, type UpdateDownloadedEvent, type UpdateInfo } from 'electron-updater'\nimport { markRaw } from '@common/utils/vueTools'\nimport * as hotKeys from '@common/hotKey'\nimport { APP_EVENT_NAMES, DATA_KEYS, DEFAULT_SETTING } from '@common/constants'\n\ntype RemoveListener = () => void\n\nexport const getSetting = async() => {\n  return rendererInvoke<LX.AppSetting>(CMMON_EVENT_NAME.get_app_setting)\n}\nexport const updateSetting = async(setting: Partial<LX.AppSetting>) => {\n  await rendererInvoke(CMMON_EVENT_NAME.set_app_setting, setting)\n}\nexport const onSettingChanged = (listener: LX.IpcRendererEventListenerParams<Partial<LX.AppSetting>>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, listener)\n  }\n}\n\nexport const sendInited = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.inited)\n}\n\nexport const getOtherSource = async(id: string): Promise<LX.Music.MusicInfoOnline[]> => {\n  return rendererInvoke<string, LX.Music.MusicInfoOnline[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source, id)\n}\nexport const saveOtherSource = async(id: string, sourceInfo: LX.Music.MusicInfoOnline[]) => {\n  await rendererInvoke<LX.Music.MusicInfoOtherSourceSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_other_source, {\n    id,\n    list: sourceInfo,\n  })\n}\nexport const clearOtherSource = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_other_source)\n}\nexport const getOtherSourceCount = async() => {\n  return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_other_source_count)\n}\n\n// export const updateDislikeInfo = async(dislikeInfo: LX.Dislike.ListItem[]) => {\n//   await rendererInvoke<LX.Dislike.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.update_dislike_music_infos, dislikeInfo)\n// }\n// export const removeDislikeInfo = async(ids: string[]) => {\n//   await rendererInvoke<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.remove_dislike_music_infos, ids)\n// }\n// export const clearDislikeInfo = async() => {\n//   await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_dislike_music_infos)\n// }\n\nexport const getHotKeyConfig = async() => {\n  return rendererInvoke<LX.HotKeyConfigAll>(WIN_MAIN_RENDERER_EVENT_NAME.get_hot_key)\n}\n\nexport const setIgnoreMouseEvents = (ignore: boolean) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_ignore_mouse_events, ignore)\n}\n\nexport const getEnvParams = async() => {\n  return rendererInvoke<LX.EnvParams>(CMMON_EVENT_NAME.get_env_params)\n}\n\nexport const clearEnvParamsDeeplink = () => {\n  rendererSend(CMMON_EVENT_NAME.clear_env_params_deeplink)\n}\n\nexport const onDeeplink = (listener: LX.IpcRendererEventListenerParams<string>): RemoveListener => {\n  rendererOn(CMMON_EVENT_NAME.deeplink, listener)\n  return () => {\n    rendererOff(CMMON_EVENT_NAME.deeplink, listener)\n  }\n}\n\nexport const checkUpdate = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.update_check)\n}\n\nexport const downloadUpdate = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.update_download_update)\n}\n\nexport const quitUpdate = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit_update)\n}\n\nexport const onUpdateAvailable = (listener: LX.IpcRendererEventListenerParams<UpdateInfo>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_available, listener)\n  }\n}\n\nexport const onUpdateError = (listener: LX.IpcRendererEventListenerParams<string>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_error, listener)\n  }\n}\n\nexport const onUpdateProgress = (listener: LX.IpcRendererEventListenerParams<ProgressInfo>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_progress, listener)\n  }\n}\n\nexport const onUpdateDownloaded = (listener: LX.IpcRendererEventListenerParams<UpdateDownloadedEvent>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_downloaded, listener)\n  }\n}\n\nexport const onUpdateNotAvailable = (listener: LX.IpcRendererEventListenerParams<UpdateInfo>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.update_not_available, listener)\n  }\n}\n\n\nexport const importUserApi = async(fileText: string) => {\n  return rendererInvoke<string, LX.UserApi.ImportUserApi>(WIN_MAIN_RENDERER_EVENT_NAME.import_user_api, fileText)\n}\nexport const setUserApi = async(source: LX.UserApi.UserApiSetApiParams): Promise<void> => {\n  return rendererInvoke<LX.UserApi.UserApiSetApiParams>(WIN_MAIN_RENDERER_EVENT_NAME.set_user_api, source)\n}\nexport const removeUserApi = async(ids: string[]) => {\n  return rendererInvoke<string[], LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.remove_user_api, ids)\n}\nexport const onShowUserApiUpdateAlert = (listener: LX.IpcRendererEventListenerParams<LX.UserApi.UserApiUpdateInfo>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_show_update_alert, listener)\n  }\n}\nexport const setAllowShowUserApiUpdateAlert = async(id: string, enable: boolean): Promise<void> => {\n  return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.user_api_set_allow_update_alert, { id, enable })\n}\nexport const onUserApiStatus = (listener: LX.IpcRendererEventListenerParams<LX.UserApi.UserApiStatus>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.user_api_status, listener)\n  }\n}\nexport const getUserApiList = async() => {\n  return rendererInvoke<LX.UserApi.UserApiInfo[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_user_api_list)\n}\nexport const sendUserApiRequest = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise<any> => {\n  return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api, {\n    requestKey,\n    data,\n  })\n}\nexport const userApiRequestCancel = (requestKey: LX.UserApi.UserApiRequestCancelParams) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.request_user_api_cancel, requestKey)\n}\n\n// export const setDesktopLyricInfo = (type, data, info) => {\n//   rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_lyric_info, {\n//     type,\n//     data,\n//     info,\n//   })\n// }\n// export const onGetDesktopLyricInfo = callback => {\n//   rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_info, callback)\n//   return () => {\n//     rendererOff(callback)\n//   }\n// }\n\nexport const sendPlayerStatus = (status: Partial<LX.Player.Status>) => {\n  rendererSend<Partial<LX.Player.Status>>(WIN_MAIN_RENDERER_EVENT_NAME.player_status, status)\n}\n\n\nexport const sendOpenAPIAction = async(action: LX.OpenAPI.Actions) => {\n  return rendererInvoke<LX.OpenAPI.Actions, LX.OpenAPI.Status>(WIN_MAIN_RENDERER_EVENT_NAME.open_api_action, action)\n}\n\nexport const saveLastStartInfo = (version: string) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.lastStartInfo,\n    data: version,\n  })\n}\n// 获取最后一次启动时的版本号\nexport const getLastStartInfo = async() => {\n  return rendererInvoke<string, string | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.lastStartInfo)\n}\n\nexport const savePlayInfo = (playInfo: LX.Player.SavedPlayInfo) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.playInfo,\n    data: playInfo,\n  })\n}\n// 获取上次关闭时的当前歌曲播放信息\nexport const getPlayInfo = async() => {\n  return rendererInvoke<string, LX.Player.SavedPlayInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.playInfo)\n}\n\nexport const saveSearchHistoryList = (list: LX.List.SearchHistoryList) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.searchHistoryList,\n    data: list,\n  })\n}\n// 获取搜索历史列表\nexport const getSearchHistoryList = async() => {\n  return rendererInvoke<string, string[] | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchHistoryList)\n}\n\nexport const saveListPositionInfo = (listPosition: LX.List.ListPositionInfo) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.listScrollPosition,\n    data: listPosition,\n  })\n}\n// 获取搜索历史列表\nexport const getListPositionInfo = async() => {\n  return rendererInvoke<string, LX.List.ListPositionInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listScrollPosition)\n}\n\nexport const saveListPrevSelectId = (listPosition: string | null) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.listPrevSelectId,\n    data: listPosition,\n  })\n}\n// 获取上一次选中的列表id\nexport const getListPrevSelectId = async() => {\n  return rendererInvoke<string, string | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listPrevSelectId)\n}\n\nexport const saveListUpdateInfo = (listPosition: LX.List.ListUpdateInfo) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.listUpdateInfo,\n    data: listPosition,\n  })\n}\n// 获取列表更新记录\nexport const getListUpdateInfo = async() => {\n  return rendererInvoke<string, LX.List.ListUpdateInfo | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.listUpdateInfo)\n}\n\nexport const saveIgnoreVersion = (version: string) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.ignoreVersion,\n    data: version,\n  })\n}\n// 获取忽略更新的版本号\nexport const getIgnoreVersion = async() => {\n  return rendererInvoke<string, string | null>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.ignoreVersion)\n}\n\nexport const saveLeaderboardSetting = (source: typeof DEFAULT_SETTING['leaderboard']) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.leaderboardSetting,\n    data: source,\n  })\n}\nexport const getLeaderboardSetting = async() => {\n  return (await rendererInvoke<string, typeof DEFAULT_SETTING['leaderboard']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.leaderboardSetting)) ?? { ...DEFAULT_SETTING.leaderboard }\n}\nexport const saveSongListSetting = (setting: typeof DEFAULT_SETTING['songList']) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.songListSetting,\n    data: setting,\n  })\n}\nexport const getSongListSetting = async() => {\n  return (await rendererInvoke<string, typeof DEFAULT_SETTING['songList']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.songListSetting)) ?? { ...DEFAULT_SETTING.songList }\n}\nexport const saveSearchSetting = (setting: typeof DEFAULT_SETTING['search']) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.searchSetting,\n    data: setting,\n  })\n}\nexport const getSearchSetting = async() => {\n  return (await rendererInvoke<string, typeof DEFAULT_SETTING['search']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.searchSetting)) ?? { ...DEFAULT_SETTING.search }\n}\nexport const saveViewPrevState = (state: typeof DEFAULT_SETTING['viewPrevState']) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.save_data, {\n    path: DATA_KEYS.viewPrevState,\n    data: state,\n  })\n}\nexport const getViewPrevState = async() => {\n  return (await rendererInvoke<string, typeof DEFAULT_SETTING['viewPrevState']>(WIN_MAIN_RENDERER_EVENT_NAME.get_data, DATA_KEYS.viewPrevState)) ?? { ...DEFAULT_SETTING.viewPrevState }\n}\n\n\nexport const getSystemFonts = async() => {\n  return rendererInvoke<string[]>(CMMON_EVENT_NAME.get_system_fonts).catch(() => {\n    return []\n  })\n}\n\nexport const getUserSoundEffectEQPresetList = async() => {\n  return rendererInvoke<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset)\n}\n\nexport const saveUserSoundEffectEQPresetList = (list: LX.SoundEffect.EQPreset[]) => {\n  rendererSend<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, list)\n}\n\nexport const getUserSoundEffectConvolutionPresetList = async() => {\n  return rendererInvoke<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset)\n}\n\nexport const saveUserSoundEffectConvolutionPresetList = (list: LX.SoundEffect.ConvolutionPreset[]) => {\n  rendererSend<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, list)\n}\n\n// export const getUserSoundEffectPitchShifterPresetList = async() => {\n//   return rendererInvoke<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_pitch_shifter_preset)\n// }\n\n// export const saveUserSoundEffectPitchShifterPresetList = (list: LX.SoundEffect.PitchShifterPreset[]) => {\n//   rendererSend<LX.SoundEffect.PitchShifterPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_pitch_shifter_preset, list)\n// }\n\nexport const allHotKeys = markRaw({\n  local: [\n    {\n      name: hotKeys.HOTKEY_PLAYER.toggle_play.name,\n      action: hotKeys.HOTKEY_PLAYER.toggle_play.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.prev.name,\n      action: hotKeys.HOTKEY_PLAYER.prev.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.next.name,\n      action: hotKeys.HOTKEY_PLAYER.next.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.seekbackward.name,\n      action: hotKeys.HOTKEY_PLAYER.seekbackward.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.seekforward.name,\n      action: hotKeys.HOTKEY_PLAYER.seekforward.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.music_dislike.name,\n      action: hotKeys.HOTKEY_PLAYER.music_dislike.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_COMMON.focusSearchInput.name,\n      action: hotKeys.HOTKEY_COMMON.focusSearchInput.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_COMMON.min.name,\n      action: hotKeys.HOTKEY_COMMON.min.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_COMMON.close.name,\n      action: hotKeys.HOTKEY_COMMON.close.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n  ],\n  global: [\n    {\n      name: hotKeys.HOTKEY_COMMON.min_toggle.name,\n      action: hotKeys.HOTKEY_COMMON.min_toggle.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_COMMON.hide_toggle.name,\n      action: hotKeys.HOTKEY_COMMON.hide_toggle.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_COMMON.close.name,\n      action: hotKeys.HOTKEY_COMMON.close.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.toggle_play.name,\n      action: hotKeys.HOTKEY_PLAYER.toggle_play.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.prev.name,\n      action: hotKeys.HOTKEY_PLAYER.prev.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.next.name,\n      action: hotKeys.HOTKEY_PLAYER.next.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.seekbackward.name,\n      action: hotKeys.HOTKEY_PLAYER.seekbackward.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.seekforward.name,\n      action: hotKeys.HOTKEY_PLAYER.seekforward.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.volume_up.name,\n      action: hotKeys.HOTKEY_PLAYER.volume_up.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.volume_down.name,\n      action: hotKeys.HOTKEY_PLAYER.volume_down.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.volume_mute.name,\n      action: hotKeys.HOTKEY_PLAYER.volume_mute.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.music_love.name,\n      action: hotKeys.HOTKEY_PLAYER.music_love.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.music_unlove.name,\n      action: hotKeys.HOTKEY_PLAYER.music_unlove.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_PLAYER.music_dislike.name,\n      action: hotKeys.HOTKEY_PLAYER.music_dislike.action,\n      type: APP_EVENT_NAMES.winMainName,\n    },\n    {\n      name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.name,\n      action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_visible.action,\n      type: APP_EVENT_NAMES.winLyricName,\n    },\n    {\n      name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.name,\n      action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_lock.action,\n      type: APP_EVENT_NAMES.winLyricName,\n    },\n    {\n      name: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.name,\n      action: hotKeys.HOTKEY_DESKTOP_LYRIC.toggle_always_top.action,\n      type: APP_EVENT_NAMES.winLyricName,\n    },\n  ],\n})\n\nexport const hotKeySetEnable = async(enable: boolean) => {\n  return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.enable, enable)\n}\n\nexport const hotKeySetConfig = async(config: LX.HotKeyActions) => {\n  return rendererInvoke(HOTKEY_RENDERER_EVENT_NAME.set_config, config)\n}\n\nexport const hotKeyGetStatus = async() => {\n  return rendererInvoke<LX.HotKeyState>(HOTKEY_RENDERER_EVENT_NAME.status)\n}\n\n// 主进程操作播放器状态\nexport const onPlayerAction = (listener: LX.IpcRendererEventListenerParams<{\n  action: LX.Player.StatusButtonActions\n  data?: unknown\n}>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, listener)\n  }\n}\n// export const setTaskbarThumbnailClip = async(clip: Electron.Rectangle) => {\n//   await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.taskbar_set_thumbnail_clip, clip)\n// }\n// 播放器状态更新 通知主进程\nexport const setPlayerAction = (buttons: LX.TaskBarButtonFlags) => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.player_action_set_buttons, buttons)\n}\n\n/**\n * On Theme Change\n * @param listener LX.IpcRendererEventListenerParams<shouldUseDarkColors: boolean>\n * @returns RemoveListener Fn\n */\nexport const onThemeChange = (listener: LX.IpcRendererEventListenerParams<LX.ThemeSetting>): RemoveListener => {\n  rendererOn(CMMON_EVENT_NAME.theme_change, listener)\n  return () => {\n    rendererOff(CMMON_EVENT_NAME.theme_change, listener)\n  }\n}\n\n/**\n * 选择路径\n */\nexport const showSelectDialog = async(options: Electron.OpenDialogOptions) => {\n  return rendererInvoke<Electron.OpenDialogOptions, Electron.OpenDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_select_dialog, options)\n}\n\n/**\n * 打开保存对话框\n */\nexport const openSaveDir = async(options: Electron.SaveDialogOptions) => {\n  return rendererInvoke<Electron.SaveDialogOptions, Electron.SaveDialogReturnValue>(WIN_MAIN_RENDERER_EVENT_NAME.show_save_dialog, options)\n}\n\n/**\n * 在资源管理器中定位文件\n */\nexport const openDirInExplorer = async(path: string) => {\n  return rendererSend<string>(WIN_MAIN_RENDERER_EVENT_NAME.open_dir_in_explorer, path)\n}\n\n/**\n * 获取缓存大小\n */\nexport const getCacheSize = async() => {\n  return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size)\n}\n\n/**\n * 清除缓存\n */\nexport const clearCache = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache)\n}\n\n/**\n * 设置窗口大小\n * @param {*} width\n * @param {*} height\n */\nexport const setWindowSize = (width: number, height: number) => {\n  const params: Partial<Electron.Rectangle> = {\n    width,\n    height,\n  }\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.set_window_size, params)\n}\n\n\nexport const getPlayerLyric = async(musicInfo: LX.Music.MusicInfo) => {\n  return rendererInvoke<string, LX.Player.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_palyer_lyric, musicInfo.id)\n}\n\nexport const getLyricRaw = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Music.LyricInfo> => {\n  return rendererInvoke<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw, musicInfo.id)\n}\n\nexport const clearLyricRaw = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw)\n}\n\nexport const getLyricRawCount = async() => {\n  return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_raw_count)\n}\n\n\nexport const getLyricEdited = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Music.LyricInfo> => {\n  return rendererInvoke<string, LX.Music.LyricInfo>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited, musicInfo.id)\n}\n\nexport const saveLyric = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo | LX.Player.LyricInfo) => {\n  // console.log(musicInfo)\n  if ('rawlrcInfo' in lyricInfo) {\n    const { rawlrcInfo, ...info } = lyricInfo\n    const tasks = [\n      rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, {\n        id: musicInfo.id,\n        lyrics: rawlrcInfo,\n      }),\n    ]\n    if (info.lyric != rawlrcInfo.lyric) {\n      tasks.push(rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, {\n        id: musicInfo.id,\n        lyrics: info,\n      }))\n    }\n    console.log(tasks)\n    await Promise.all(tasks)\n  } else {\n    await rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_raw, {\n      id: musicInfo.id,\n      lyrics: lyricInfo,\n    })\n  }\n}\nexport const saveLyricEdited = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo) => {\n  await rendererInvoke<LX.Music.LyricInfoSave>(WIN_MAIN_RENDERER_EVENT_NAME.save_lyric_edited, {\n    id: musicInfo.id,\n    lyrics: lyricInfo,\n  })\n}\nexport const removeLyricEdited = async(musicInfo: LX.Music.MusicInfo) => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.remove_lyric_edited, musicInfo.id)\n}\n\nexport const clearLyric = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_raw)\n}\n\nexport const clearLyricEdited = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_lyric_edited)\n}\n\nexport const getLyricEditedCount = async() => {\n  return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_lyric_edited_count)\n}\n\n\nexport const saveTheme = async(theme: LX.Theme) => {\n  return rendererInvoke<LX.Theme>(WIN_MAIN_RENDERER_EVENT_NAME.save_theme, theme)\n}\nexport const removeTheme = async(id: string) => {\n  return rendererInvoke<string>(WIN_MAIN_RENDERER_EVENT_NAME.remove_theme, id)\n}\nexport const getThemes = async() => {\n  return rendererInvoke<{ themes: LX.Theme[], userThemes: LX.Theme[], dataPath: string }>(WIN_MAIN_RENDERER_EVENT_NAME.get_themes)\n}\n\n/**\n * 从缓存获取歌曲URL\n * @param musicInfo 歌曲信息\n * @param type URL音质\n * @returns\n */\nexport const getMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality): Promise<string> => {\n  return rendererInvoke<string, string>(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url, `${musicInfo.id}_${type}`)\n}\n\n/**\n * 缓存歌曲URL\n * @param musicInfo 歌曲信息\n * @param type URL音质\n * @param url 歌曲URL\n */\nexport const saveMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality, url: string) => {\n  await rendererInvoke<LX.Music.MusicUrlInfo>(WIN_MAIN_RENDERER_EVENT_NAME.save_music_url, {\n    id: `${musicInfo.id}_${type}`,\n    url,\n  })\n}\n/**\n * 清理所有缓存的歌曲URL\n */\nexport const clearMusicUrl = async() => {\n  await rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.clear_music_url)\n}\n\nexport const getMusicUrlCount = async() => {\n  return rendererInvoke<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_music_url_count)\n}\n\n/**\n * 退出应用\n */\nexport const quitApp = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.quit)\n}\n\n/**\n * 关闭窗口\n */\nexport const closeWindow = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.close)\n}\n\n/**\n * 最小化窗口\n */\nexport const minWindow = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min)\n}\n\n/**\n * 最大化窗口\n */\nexport const maxWindow = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.max)\n}\n\n/**\n * 最小化、最大化窗口切换\n */\nexport const minMaxWindowToggle = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.min_toggle)\n}\n/**\n * 显示、隐藏窗口切换\n */\nexport const showHideWindowToggle = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.hide_toggle)\n}\n/**\n * 聚焦窗口\n */\nexport const focusWindow = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.focus)\n}\n/**\n * 是否启用电源锁\n */\nexport const setPowerSaveBlocker = (enabled: boolean) => {\n  rendererSend<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.set_power_save_blocker, enabled)\n}\n\n/**\n * 窗口获取焦点事件\n * @param listener\n * @returns\n */\nexport const onFocus = (listener: LX.IpcRendererEventListener): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.focus, listener)\n  }\n}\n\n/**\n * 快捷键触发事件\n * @param listener\n * @returns\n */\nexport const onKeyDown = (listener: LX.IpcRendererEventListenerParams<LX.HotKeyEvent>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.key_down, listener)\n  }\n}\n\n/**\n * 快捷键设置更新事件\n * @param listener\n * @returns\n */\nexport const onUpdateHotkey = (listener: LX.IpcRendererEventListenerParams<LX.HotKeyConfigAll>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.set_hot_key_config, listener)\n  }\n}\n\n/**\n * 设置全屏\n * @param isFullscreen 是否全屏\n * @returns\n */\nexport const setFullScreen = async(isFullscreen: boolean): Promise<boolean> => {\n  return rendererInvoke<boolean, boolean>(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, isFullscreen)\n}\n\n/**\n * 打开开发者工具\n * @returns\n */\nexport const openDevTools = () => {\n  rendererSend(WIN_MAIN_RENDERER_EVENT_NAME.open_dev_tools)\n}\n\n/**\n * 接收同步事件\n * @param listener\n * @returns\n */\nexport const onSyncAction = (listener: LX.IpcRendererEventListenerParams<LX.Sync.SyncMainWindowActions>): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, listener)\n  }\n}\n\n/**\n * 发送同步事件\n * @param action\n * @returns\n */\nexport const sendSyncAction = async(action: LX.Sync.SyncServiceActions) => {\n  return rendererInvoke<LX.Sync.SyncServiceActions>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, action)\n}\n\n/**\n * 获取同步服务端连接设备历史列表\n * @returns\n */\nexport const getSyncServerDevices = () => {\n  return rendererInvoke<LX.Sync.ServerDevices>(WIN_MAIN_RENDERER_EVENT_NAME.sync_get_server_devices)\n}\n\n/**\n * 移除同步服务端连接设备\n * @returns\n */\nexport const removeSyncServerDevice = (clientId: string) => {\n  return rendererInvoke<string>(WIN_MAIN_RENDERER_EVENT_NAME.sync_remove_server_device, clientId)\n}\n\n\n// export const refreshSyncCode = async(): Promise<string> => {\n//   return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.sync_generate_code)\n// }\n\n// export const onSyncStatus = (listener: LX.IpcRendererEventListenerParams<LX.Sync.Status>): RemoveListener => {\n//   rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener)\n\n//   return () => {\n//     rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.sync_status, listener)\n//   }\n// }\n\n/**\n * 桌面歌词进程创建事件\n * @param listener\n * @returns\n */\nexport const onNewDesktopLyricProcess = (listener: LX.IpcRendererEventListener): RemoveListener => {\n  rendererOn(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener)\n  return () => {\n    rendererOff(WIN_MAIN_RENDERER_EVENT_NAME.process_new_desktop_lyric_client, listener)\n  }\n}\n\n\nexport const downloadTasksGet = async() => {\n  return rendererInvoke<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_get)\n}\nexport const downloadTasksCreate = async(list: LX.Download.ListItem[], addMusicLocationType: LX.AddMusicLocationType) => {\n  return rendererInvoke<LX.Download.saveDownloadMusicInfo>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_add, {\n    list,\n    addMusicLocationType,\n  })\n}\nexport const downloadTasksUpdate = async(list: LX.Download.ListItem[]) => {\n  return rendererInvoke<LX.Download.ListItem[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_update, list)\n}\nexport const downloadTasksRemove = async(ids: string[]) => {\n  return rendererInvoke<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, ids)\n}\nexport const downloadListClear = async() => {\n  return rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear)\n}\n"
  },
  {
    "path": "src/renderer/utils/keyBind.ts",
    "content": "import { isMac } from '@common/utils'\n\nconst downKeys = new Set<string>()\n\nexport type KeyActionType = LX.KeyDownEevent['type']\nexport type Keys = LX.KeyDownEevent['keys']\nexport type Key = LX.KeyDownEevent['key']\nexport type EventKey = LX.KeyDownEevent['eventKey']\nexport type Event = LX.KeyDownEevent['event']\n\nconst handleEvent = (type: KeyActionType, event: LX.KeyEvent, keys: Keys, isEditing: boolean) => {\n  let eventKey = event.key\n  if (isMac) {\n    let index = keys.indexOf('meta')\n    if (index > -1) keys.splice(index, 1, 'mod')\n    if (eventKey == 'Meta') eventKey = 'mod'\n  } else {\n    let index = keys.indexOf('ctrl')\n    if (index > -1) keys.splice(index, 1, 'mod')\n    if (eventKey == 'Control') eventKey = 'mod'\n  }\n  let key = keys.join('+')\n\n  switch (type) {\n    case 'down':\n      downKeys.add(key)\n      break\n    case 'up':\n      downKeys.delete(key)\n      break\n  }\n  handleSendEvent(key, eventKey, type, event, keys, isEditing)\n}\n\n// 修饰键处理\nconst eventModifiers = (event: LX.KeyEvent): string[] => {\n  let modifiers: string[] = []\n  if (event.ctrlKey) modifiers.push('ctrl')\n  if (event.shiftKey) modifiers.push('shift')\n  if (event.altKey) modifiers.push('alt')\n  if (event.metaKey) modifiers.push('meta')\n\n  return modifiers\n}\n\n// 是否忽略事件（表单元素等默认忽略）\nconst assertStopCallback = (element: HTMLElement) => {\n  // if the element has the class \"keybind\" then no need to stop\n  if (element.classList.contains('key-bind')) return false\n\n  // stop for input, select, and textarea\n  switch (element.tagName) {\n    case 'INPUT':\n    case 'SELECT':\n    case 'TEXTAREA':\n      return true\n    default:\n      return !!element.isContentEditable\n  }\n}\n\nconst handleKeyDown = (event: LX.KeyEvent) => {\n  // if (assertStopCallback(event.target)) return\n  // event.preventDefault()\n  let keys = eventModifiers(event)\n  switch (event.key) {\n    case 'Control':\n    case 'Alt':\n    case 'Meta':\n    case 'Shift':\n      break\n    case ' ':\n      keys.push('space')\n      break\n    default:\n      keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\\w{1,3})\\w*$/i, 'num$1') : event.key).toLowerCase())\n      break\n  }\n  handleEvent('down', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false)\n}\n\nconst handleKeyUp = (event: LX.KeyEvent) => {\n  // if (assertStopCallback(event.target)) return\n  event.preventDefault()\n  let keys = eventModifiers(event)\n  switch (event.key) {\n    case 'Control':\n      keys.push('ctrl')\n      break\n    case ' ':\n      keys.push('space')\n      break\n    default:\n      keys.push((event.code.includes('Numpad') ? event.code.replace(/^Numpad(\\w{1,3})\\w*$/i, 'num$1') : event.key).toLowerCase())\n      break\n  }\n  handleEvent('up', event, keys, event.target ? assertStopCallback(event.target as HTMLElement) : false)\n}\n\ntype HandleSendEvent = (key: Key, eventKey: EventKey, type: KeyActionType, event: Event, keys: Keys, isEditing: boolean) => void\nlet handleSendEvent: HandleSendEvent\n\nconst bindKey = (handle: HandleSendEvent = () => {}) => {\n  handleSendEvent = handle\n  document.addEventListener('keydown', handleKeyDown)\n  document.addEventListener('keyup', handleKeyUp)\n}\n\nconst unbindKey = () => {\n  document.removeEventListener('keydown', handleKeyDown)\n  document.removeEventListener('keyup', handleKeyUp)\n}\n\nconst clearDownKeys = () => {\n  let keys = Array.from(downKeys)\n  for (let i = keys.length - 1; i > -1; i--) {\n    handleSendEvent(keys[i], keys[i], 'up', null, [keys[i]], false)\n  }\n  downKeys.clear()\n}\n\nexport default {\n  bindKey,\n  unbindKey,\n  clearDownKeys,\n}\n"
  },
  {
    "path": "src/renderer/utils/message.ts",
    "content": "export const requestMsg = {\n  fail: '请求异常😮，可以多试几次，若还是不行就换一首吧。。。',\n  unachievable: '哦No😱...接口无法访问了！',\n  timeout: '请求超时',\n  // unachievable: '哦No😱...接口无法访问了！已帮你切换到临时接口，重试下看能不能播放吧~',\n  notConnectNetwork: '无法连接到服务器',\n  cancelRequest: '取消http请求',\n  tooManyRequests: '服务器繁忙',\n} as const\n"
  },
  {
    "path": "src/renderer/utils/music.ts",
    "content": "import { checkPath, joinPath, extname, basename, readFile, getFileStats } from '@common/utils/nodejs'\nimport { formatPlayTime } from '@common/utils/common'\nimport type { IComment } from 'music-metadata/lib/type'\nimport { decodeKrc } from '@common/utils/lyricUtils/kg'\n\nexport const checkDownloadFileAvailable = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<boolean> => {\n  return musicInfo.isComplate && !/\\.ape$/.test(musicInfo.metadata.fileName) &&\n    (await checkPath(musicInfo.metadata.filePath) || await checkPath(joinPath(savePath, musicInfo.metadata.fileName)))\n}\n\nexport const checkLocalFileAvailable = async(musicInfo: LX.Music.MusicInfoLocal): Promise<boolean> => {\n  return checkPath(musicInfo.meta.filePath)\n}\n\n/**\n * 检查音乐文件是否存在\n * @param musicInfo\n * @param savePath\n */\nexport const checkMusicFileAvailable = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<boolean> => {\n  if ('progress' in musicInfo) {\n    return checkDownloadFileAvailable(musicInfo, savePath)\n  } else if (musicInfo.source == 'local') {\n    return checkLocalFileAvailable(musicInfo)\n  } else return true\n}\n\nexport const getDownloadFilePath = async(musicInfo: LX.Download.ListItem, savePath: string): Promise<string> => {\n  if (musicInfo.isComplate && !/\\.ape$/.test(musicInfo.metadata.fileName)) {\n    if (await checkPath(musicInfo.metadata.filePath)) return musicInfo.metadata.filePath\n    const path = joinPath(savePath, musicInfo.metadata.fileName)\n    if (await checkPath(path)) return path\n  }\n  return ''\n}\n\nexport const getLocalFilePath = async(musicInfo: LX.Music.MusicInfoLocal): Promise<string> => {\n  return (await checkPath(musicInfo.meta.filePath)) ? musicInfo.meta.filePath : ''\n}\n\n\n/**\n * 获取音乐文件路径\n * @param musicInfo\n * @param savePath\n * @returns\n */\nexport const getMusicFilePath = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, savePath: string): Promise<string> => {\n  if ('progress' in musicInfo) {\n    return getDownloadFilePath(musicInfo, savePath)\n  } else if (musicInfo.source == 'local') {\n    return getLocalFilePath(musicInfo)\n  }\n  return ''\n}\n\n/**\n * 创建本地音乐信息对象\n * @param path 文件路径\n * @returns\n */\nexport const createLocalMusicInfo = async(path: string): Promise<LX.Music.MusicInfoLocal | null> => {\n  if (!await checkPath(path)) return null\n  const { parseFile } = await import('music-metadata')\n\n  let metadata\n  try {\n    metadata = await parseFile(path)\n  } catch (err) {\n    console.log(err)\n    return null\n  }\n\n  // console.log(metadata)\n  let ext = extname(path)\n  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n  let name = (metadata.common.title || basename(path, ext)).trim()\n  let singer = metadata.common.artists?.length ? metadata.common.artists.map(a => a.trim()).join('、') : ''\n  let interval = metadata.format.duration ? formatPlayTime(metadata.format.duration) : ''\n  let albumName = metadata.common.album?.trim() ?? ''\n\n  return {\n    id: path,\n    name,\n    singer,\n    source: 'local',\n    interval,\n    meta: {\n      albumName,\n      filePath: path,\n      songId: path,\n      picUrl: '',\n      ext: ext.replace(/^\\./, ''),\n    },\n  }\n}\n\nlet prevFileInfo: {\n  path: string\n  promise: Promise<LX.MusicMetadataModule.IAudioMetadata | null>\n} = {\n  path: '',\n  promise: Promise.resolve(null),\n}\nconst getFileMetadata = async(path: string) => {\n  if (prevFileInfo.path == path) return prevFileInfo.promise\n  prevFileInfo.path = path\n  return prevFileInfo.promise = checkPath(path).then(async(isExist) => {\n    return isExist ? import('music-metadata').then(async({ parseFile }) => parseFile(path)).catch(err => {\n      console.log(err)\n      return null\n    }) : null\n  })\n}\n/**\n * 获取歌曲文件封面图片\n * @param path 路径\n */\nexport const getLocalMusicFilePic = async(path: string) => {\n  const filePath = new RegExp('\\\\' + extname(path) + '$')\n  let picPath = path.replace(filePath, '.jpg')\n  let stats = await getFileStats(picPath)\n  if (stats) return picPath\n  picPath = path.replace(filePath, '.png')\n  stats = await getFileStats(picPath)\n  if (stats) return picPath\n  const metadata = await getFileMetadata(path)\n  if (!metadata) return null\n  const { selectCover } = await import('music-metadata')\n  return selectCover(metadata.common.picture)\n}\n\n// const timeExp = /^\\[([\\d:.]*)\\]{1}/\n/**\n * 解析歌词文件，分离可能存在的翻译、罗马音歌词\n * @param lrc 歌词内容\n * @returns\n */\n// export const parseLyric = (lrc: string): LX.Music.LyricInfo => {\n//   const lines = lrc.split(/\\r\\n|\\r|\\n/)\n//   const lyrics: string[][] = []\n//   const map = new Map<string, number>()\n\n//   for (let i = 0; i < lines.length; i++) {\n//     const line = lines[i].trim()\n//     let result = timeExp.exec(line)\n//     if (result) {\n//       const index = map.get(result[1]) ?? 0\n//       if (!lyrics[index]) lyrics[index] = []\n//       lyrics[index].push(line)\n//       map.set(result[1], index + 1)\n//     } else {\n//       if (!lyrics[0]) lyrics[0] = []\n//       lyrics[0].push(line)\n//     }\n//   }\n//   const lyricInfo: LX.Music.LyricInfo = {\n//     lyric: lyrics[0].join('\\n'),\n//     tlyric: '',\n//   }\n//   if (lyrics[1]) lyricInfo.tlyric = lyrics[1].join('\\n')\n//   if (lyrics[2]) lyricInfo.rlyric = lyrics[2].join('\\n')\n\n//   return lyricInfo\n// }\n\n\n/**\n * 获取歌曲文件歌词\n * @param path 路径\n */\nexport const getLocalMusicFileLyric = async(path: string): Promise<LX.Music.LyricInfo | null> => {\n  // 尝试读取同目录下的同名lrc文件\n  const filePath = new RegExp('\\\\' + extname(path) + '$')\n  let lrcPath = path.replace(filePath, '.lrc')\n  let stats = await getFileStats(lrcPath)\n  // console.log(lrcPath, stats)\n  if (stats && stats.size < 1024 * 1024 * 10) {\n    const lrcBuf = await readFile(lrcPath)\n    const { detect } = await import('jschardet')\n    const { confidence, encoding } = detect(lrcBuf)\n    console.log('lrc file encoding', confidence, encoding)\n    if (confidence > 0.8) {\n      const iconv = (await import('iconv-lite')).default\n      if (iconv.encodingExists(encoding)) {\n        const lrc = iconv.decode(lrcBuf, encoding)\n        if (lrc) {\n          return {\n            lyric: lrc,\n          }\n        }\n      }\n    }\n  }\n  // 尝试读取同目录下的同名krc文件\n  lrcPath = path.replace(filePath, '.krc')\n  stats = await getFileStats(lrcPath)\n  console.log(lrcPath, stats?.size)\n  if (stats && stats.size < 1024 * 1024 * 10) {\n    const lrcBuf = await readFile(lrcPath)\n    try {\n      return await decodeKrc(lrcBuf)\n    } catch (e) {\n      console.log(e)\n    }\n  }\n\n\n  // 尝试读取文件内歌词\n  const metadata = await getFileMetadata(path)\n  // console.log(metadata?.common)\n  if (!metadata) return null\n  // let lyricInfo = metadata.common.lyrics?.[0]\n  // if (lyricInfo) {\n  //   let lyric: string | undefined\n  //   if (typeof lyricInfo == 'object') lyric = lyricInfo.text\n  //   else if (typeof lyricInfo == 'string') lyric = lyricInfo\n  //   if (lyric && lyric.length > 10) {\n  //     return { lyric }\n  //   }\n  // }\n  // console.log(metadata)\n  for (const info of Object.values(metadata.native)) {\n    for (const ust of info) {\n      switch (ust.id) {\n        case 'LYRICS': {\n          const value = typeof ust.value == 'string' ? ust.value : (ust as IComment).text\n          if (value && value.length > 10) return { lyric: value }\n          break\n        }\n        case 'USLT': {\n          const value = ust.value as IComment\n          if (value.text && value.text.length > 10) return { lyric: value.text }\n          break\n        }\n      }\n    }\n  }\n  return null\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/api-source-info.ts",
    "content": "// Support qualitys: 128k 320k flac wav\n\nconst sources: Array<{\n  id: string\n  name: string\n  disabled: boolean\n  supportQualitys: Partial<Record<LX.OnlineSource, LX.Quality[]>>\n}> = [\n  // {\n  //   id: 'test',\n  //   name: '测试接口',\n  //   disabled: false,\n  //   supportQualitys: {\n  //     kw: ['128k'],\n  //     kg: ['128k'],\n  //     tx: ['128k'],\n  //     wy: ['128k'],\n  //     mg: ['128k'],\n  //     // bd: ['128k'],\n  //   },\n  // },\n  // {\n  //   id: 'temp',\n  //   name: '临时接口',\n  //   disabled: false,\n  //   supportQualitys: {\n  //     kw: ['128k'],\n  //   },\n  // },\n]\n\nexport default sources\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/api-source.js",
    "content": "import apiSourceInfo from './api-source-info'\nimport { apiSource, userApi } from '@renderer/store'\n// import api_temp_kw from './kw/api-temp'\n// // import api_test_bd from './bd/api-test'\n// import api_test_tx from './tx/api-test'\n// import api_test_kg from './kg/api-test'\n// import api_test_kw from './kw/api-test'\n// import api_test_mg from './mg/api-test'\n// import api_test_wy from './wy/api-test'\n\nconst allApi = {\n  // temp_kw: api_temp_kw,\n  // // test_bd: api_test_bd,\n  // test_tx: api_test_tx,\n  // test_kg: api_test_kg,\n  // test_kw: api_test_kw,\n  // test_mg: api_test_mg,\n  // test_wy: api_test_wy,\n}\n\nconst apiList = {}\nconst supportQuality = {}\n\nfor (const api of apiSourceInfo) {\n  supportQuality[api.id] = api.supportQualitys\n  for (const source of Object.keys(api.supportQualitys)) {\n    apiList[`${api.id}_api_${source}`] = allApi[`${api.id}_${source}`]\n  }\n}\n\nconst getAPI = source => apiList[`${apiSource.value}_api_${source}`]\n\nconst apis = source => {\n  if (/^user_api/.test(apiSource.value)) return userApi.apis[source]\n  let api = getAPI(source)\n  if (api) return api\n  throw new Error('Api is not found')\n}\n\nexport { apis, supportQuality }\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_test = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/bd/${songInfo.songmid}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n}\n\nexport default api_test\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/hotSearch.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    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', {\n      method: 'get',\n      headers: {\n        'User-Agent': 'android_7.0.2.0;baiduyinyue',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.error_code !== 22000) throw new Error('获取热搜词失败')\n    // console.log(body, statusCode)\n    return { source: 'bd', list: this.filterList(body.result) }\n  },\n  filterList(rawList) {\n    return rawList.map(item => item.word)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/index.js",
    "content": "import leaderboard from './leaderboard'\nimport { apis } from '../api-source'\nimport musicInfo from './musicInfo'\nimport songList from './songList'\nimport { httpFetch } from '../../request'\nimport musicSearch from './musicSearch'\nimport hotSearch from './hotSearch'\n\nconst bd = {\n  leaderboard,\n  songList,\n  musicSearch,\n  hotSearch,\n  getMusicUrl(songInfo, type) {\n    return apis('bd').getMusicUrl(songInfo, type)\n  },\n  getPic(songInfo) {\n    const requestObj = this.getMusicInfo(songInfo)\n    return requestObj.promise.then(info => info.pic_premium)\n  },\n  getLyric(songInfo) {\n    const requestObj = this.getMusicInfo(songInfo)\n    requestObj.promise = requestObj.promise.then(info => httpFetch(info.lrclink).promise.then(resp => ({ lyric: resp.body, tlyric: '' })))\n    return requestObj\n  },\n  // getLyric(songInfo) {\n  //   return apis('bd').getLyric(songInfo)\n  // },\n  // getPic(songInfo) {\n  //   return apis('bd').getPic(songInfo)\n  // },\n  getMusicInfo(songInfo) {\n    return musicInfo.getMusicInfo(songInfo.songmid)\n  },\n  getMusicDetailPageUrl(songInfo) {\n    return `http://music.taihe.com/song/${songInfo.songmid}`\n  },\n}\n\nexport default bd\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/leaderboard.js",
    "content": "import { httpFetch } from '../../request'\n// import { formatPlayTime } from '../../index'\n\n\nconst boardList = [\n  // { id: 'bd__601', name: '歌单榜', bangid: '601' },\n  { id: 'bd__2', name: '热歌榜', bangid: '2' },\n  { id: 'bd__20', name: '华语金曲榜', bangid: '20' },\n  { id: 'bd__25', name: '网络歌曲榜', bangid: '25' },\n  { id: 'bd__1', name: '新歌榜', bangid: '1' },\n  { id: 'bd__21', name: '欧美金曲榜', bangid: '21' },\n  { id: 'bd__200', name: '原创音乐榜', bangid: '200' },\n  { id: 'bd__22', name: '经典老歌榜', bangid: '22' },\n  { id: 'bd__24', name: '影视金曲榜', bangid: '24' },\n  { id: 'bd__23', name: '情歌对唱榜', bangid: '23' },\n  { id: 'bd__11', name: '摇滚榜', bangid: '11' },\n  { id: 'bd__105', name: '好童星榜', bangid: '105' },\n  { id: 'bd__106', name: '雅克•藏羌彝原创音乐榜', bangid: '106' },\n]\n\nexport default {\n  limit: 20,\n  list: [\n    {\n      id: 'bdrgb',\n      name: '热歌榜',\n      bangid: '2',\n    },\n    {\n      id: 'bdxgb',\n      name: '新歌榜',\n      bangid: '1',\n    },\n    {\n      id: 'bdycb',\n      name: '原创榜',\n      bangid: '200',\n    },\n    {\n      id: 'bdhyjqb',\n      name: '华语榜',\n      bangid: '20',\n    },\n    {\n      id: 'bdomjqb',\n      name: '欧美榜',\n      bangid: '21',\n    },\n    {\n      id: 'bdwugqb',\n      name: '网络榜',\n      bangid: '25',\n    },\n    {\n      id: 'bdjdlgb',\n      name: '老歌榜',\n      bangid: '22',\n    },\n    {\n      id: 'bdysjqb',\n      name: '影视金曲榜',\n      bangid: '24',\n    },\n    {\n      id: 'bdqgdcb',\n      name: '情歌对唱榜',\n      bangid: '23',\n    },\n    {\n      id: 'bdygb',\n      name: '摇滚榜',\n      bangid: '11',\n    },\n  ],\n  getUrl(id, p) {\n    return `http://musicmini.qianqian.com/2018/static/bangdan/bangdanList_${id}_${p}.html`\n  },\n  regExps: {\n    item: /data-song=\"({.+?})\"/g,\n    info: /{total[\\s:]+\"(\\d+)\", size[\\s:]+\"(\\d+)\", page[\\s:]+\"(\\d+)\"}/,\n  },\n  getData(url) {\n    const requestObj = httpFetch(url)\n    return requestObj.promise\n  },\n  filterData(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      const types = []\n      const _types = {}\n      let size = null\n      types.push({ type: '128k', size })\n      _types['128k'] = {\n        size,\n      }\n      if (item.biaoshi) {\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      // types.reverse()\n\n      return {\n        singer: item.song_artist.replace(',', '、'),\n        name: item.song_title,\n        albumName: item.album_title,\n        albumId: item.album_id,\n        source: 'bd',\n        interval: '',\n        songmid: item.song_id,\n        img: null,\n        lrc: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  parseData(rawData) {\n    // return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/&quot;/g, '\"').replace(/\\\\\\//g, '/').replace(/(@s_1,w_)\\d+(,h_)\\d+/, '$1500$2500')))\n    return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/&quot;/g, '\"').replace(/\\\\\\//g, '/')))\n  },\n  async getBoards(retryNum = 0) {\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'bd',\n    }\n  },\n  getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    return this.getData(this.getUrl(bangid, page)).then(({ body }) => {\n      let result = body.match(this.regExps.item)\n      if (!result) return this.getList(bangid, page, retryNum)\n      let info = body.match(this.regExps.info)\n      if (!info) return this.getList(bangid, page, retryNum)\n      const list = this.filterData(this.parseData(result))\n      this.limit = parseInt(info[2])\n      return {\n        total: parseInt(info[1]),\n        list,\n        limit: this.limit,\n        page: parseInt(info[3]),\n        source: 'bd',\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/musicInfo.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  cache: {},\n  getMusicInfo(songmid) {\n    if (this.cache[songmid]) {\n      return { promise: Promise.resolve(this.cache[songmid]) }\n    }\n    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`)\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      // console.log(body)\n      if (body.error_code == 22000) {\n        this.cache[songmid] = body.result.songinfo\n        return body.result.songinfo\n      }\n      return Promise.reject(new Error('获取音乐信息失败'))\n    })\n    return requestObj\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/musicSearch.js",
    "content": "// import '../../polyfill/array.find'\n\nimport { httpFetch } from '../../request'\nimport { formatPlayTime } from '../../index'\n// import { debug } from '../../utils/env'\n// import { formatSinger } from './util'\n\nexport default {\n  limit: 30,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  musicSearch(str, page, limit) {\n    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`)\n    return searchRequest.promise.then(({ body }) => body)\n  },\n  handleResult(rawData) {\n    let ids = new Set()\n    const list = []\n    if (!rawData) return list\n    rawData.forEach(item => {\n      if (ids.has(item.song_id)) return\n      ids.add(item.song_id)\n      const types = []\n      const _types = {}\n      let size = null\n      let itemTypes = item.all_rate.split(',')\n      if (itemTypes.includes('128')) {\n        types.push({ type: '128k', size })\n        _types['128k'] = {\n          size,\n        }\n      }\n      if (itemTypes.includes('320')) {\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n      }\n      if (itemTypes.includes('flac')) {\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      // types.reverse()\n\n      list.push({\n        singer: item.author.replace(',', '、'),\n        name: item.title,\n        albumName: item.album_title,\n        albumId: item.album_id,\n        source: 'bd',\n        interval: formatPlayTime(parseInt(item.file_duration)),\n        songmid: item.song_id,\n        img: null,\n        lrc: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    return list\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n\n    return this.musicSearch(str, page, limit).then(result => {\n      if (!result || result.error_code !== 22000) return this.search(str, page, limit, retryNum)\n      let list = this.handleResult(result.result.song_info.song_list)\n\n      if (list == null) return this.search(str, page, limit, retryNum)\n\n      this.total = result.result.song_info.total\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return Promise.resolve({\n        list,\n        allPage: this.allPage,\n        limit,\n        total: this.total,\n        source: 'bd',\n      })\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/bd/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, toMD5 } from '../../index'\nimport CryptoJS from 'crypto-js'\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_list: null,\n  _requestObj_listRecommend: null,\n  limit_list: 30,\n  limit_song: 10000,\n  successCode: 22000,\n  sortList: [\n    {\n      name: '最热',\n      id: '1',\n    },\n    {\n      name: '最新',\n      id: '0',\n    },\n  ],\n  regExps: {\n    // http://music.taihe.com/songlist/566347741\n    listDetailLink: /^.+\\/songlist\\/(\\d+)(?:\\?.*|&.*$|#.*$|$)/,\n  },\n  aesPassEncod(jsonData) {\n    let timestamp = Math.floor(Date.now() / 1000)\n    let privateKey = toMD5('baidu_taihe_music_secret_key' + timestamp).substr(8, 16)\n    let key = CryptoJS.enc.Utf8.parse(privateKey)\n    let iv = CryptoJS.enc.Utf8.parse(privateKey)\n    let arrData = []\n    let strData = ''\n    for (let key in jsonData) arrData.push(key)\n    arrData.sort()\n    for (let i = 0; i < arrData.length; i++) {\n      let key = arrData[i]\n      strData +=\n        (i === 0 ? '' : '&') + key + '=' + encodeURIComponent(jsonData[key])\n    }\n    let JsonFormatter = {\n      stringify(cipherParams) {\n        let jsonObj = {\n          ct: cipherParams.ciphertext.toString(CryptoJS.enc.Base64),\n        }\n        if (cipherParams.iv) {\n          jsonObj.iv = cipherParams.iv.toString()\n        }\n        if (cipherParams.salt) {\n          jsonObj.s = cipherParams.salt.toString()\n        }\n        return jsonObj\n      },\n      parse(jsonStr) {\n        let jsonObj = JSON.parse(jsonStr)\n        let cipherParams = CryptoJS.lib.CipherParams.create({\n          ciphertext: CryptoJS.enc.Base64.parse(jsonObj.ct),\n        })\n        if (jsonObj.iv) {\n          cipherParams.iv = CryptoJS.enc.Hex.parse(jsonObj.iv)\n        }\n        if (jsonObj.s) {\n          cipherParams.salt = CryptoJS.enc.Hex.parse(jsonObj.s)\n        }\n        return cipherParams\n      },\n    }\n    let encrypted = CryptoJS.AES.encrypt(strData, key, {\n      iv,\n      blockSize: 16,\n      mode: CryptoJS.mode.CBC,\n      format: JsonFormatter,\n    })\n    let ciphertext = encrypted.toString().ct\n    let sign = toMD5('baidu_taihe_music' + ciphertext + timestamp)\n    let jsonRet = {\n      timestamp,\n      param: ciphertext,\n      sign,\n    }\n    return jsonRet\n  },\n  createUrl(param, method) {\n    let data = this.aesPassEncod(param)\n    return `http://musicmini.qianqian.com/v1/restserver/ting?method=${method}&time=${Date.now()}&timestamp=${data.timestamp}&param=${data.param}&sign=${data.sign}`\n  },\n  getTagsUrl() {\n    return this.createUrl({\n      from: 'qianqianmini',\n      type: 'diy',\n      version: '10.1.8',\n    }, 'baidu.ting.ugcdiy.getChannels')\n  },\n  getListUrl(sortType, tagName, page) {\n    return this.createUrl({\n      channelname: tagName || '全部',\n      from: 'qianqianmini',\n      offset: (page - 1) * this.limit_list,\n      order_type: sortType,\n      size: this.limit_list,\n      version: '10.1.8',\n    }, 'baidu.ting.ugcdiy.getChanneldiy')\n  },\n  getListDetailUrl(list_id, page) {\n    return this.createUrl({\n      list_id,\n      offset: (page - 1) * this.limit_song,\n      size: this.limit_song,\n      withcount: '1',\n      withsong: '1',\n    }, 'baidu.ting.ugcdiy.getBaseInfo')\n  },\n\n  // 获取标签\n  getTags(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.getTagsUrl())\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.error_code !== this.successCode) return this.getTags(++tryNum)\n      return {\n        hotTag: this.filterInfoHotTag(body.result.hot),\n        tags: this.filterTagInfo(body.result.tags),\n        source: 'bd',\n      }\n    })\n  },\n  filterInfoHotTag(rawList) {\n    return rawList.map(item => ({\n      name: item,\n      id: item,\n      source: 'bd',\n    }))\n  },\n  filterTagInfo(rawList) {\n    return rawList.map(type => ({\n      name: type.first,\n      list: type.second.map(item => ({\n        parent_id: type.first,\n        parent_name: type.first,\n        id: item,\n        name: item,\n        source: 'bd',\n      })),\n    }))\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_list = httpFetch(this.getListUrl(sortId, tagId, page))\n    return this._requestObj_list.promise.then(({ body }) => {\n      if (body.error_code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)\n      return {\n        list: this.filterList(body.diyInfo),\n        total: body.nums,\n        page,\n        limit: this.limit_list,\n        source: 'bd',\n      }\n    })\n  },\n\n\n  /**\n   * 格式化播放数量\n   * @param {*} num\n   */\n  formatPlayCount(num) {\n    if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿'\n    if (num > 10000) return parseInt(num / 1000) / 10 + '万'\n    return num\n  },\n  filterList(rawData) {\n    return rawData.map(item => ({\n      play_count: this.formatPlayCount(item.listen_num),\n      id: String(item.list_id),\n      author: item.username,\n      name: item.title,\n      // time: item.publish_time,\n      img: item.list_pic_large || item.list_pic,\n      grade: item.grade,\n      desc: item.desc || item.tag,\n      source: 'bd',\n    }))\n  },\n\n  // 获取歌曲列表内的音乐\n  getListDetail(id, page, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n\n    if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')\n\n    const requestObj_listDetail = httpFetch(this.getListDetailUrl(id, page))\n    return requestObj_listDetail.promise.then(({ body }) => {\n      if (body.error_code !== this.successCode) return this.getListDetail(id, page, ++tryNum)\n      let listData = this.filterData(body.result.songlist)\n      return {\n        list: listData,\n        page,\n        limit: this.limit_song,\n        total: body.result.song_num,\n        source: 'bd',\n        info: {\n          name: body.result.info.list_title,\n          img: body.result.info.list_pic,\n          desc: body.result.info.list_desc,\n          author: body.result.info.userinfo.username,\n          play_count: this.formatPlayCount(body.result.listen_num),\n        },\n      }\n    })\n  },\n  filterData(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      const types = []\n      const _types = {}\n      let size = null\n      let itemTypes = item.all_rate.split(',')\n      if (itemTypes.includes('128')) {\n        types.push({ type: '128k', size })\n        _types['128k'] = {\n          size,\n        }\n      }\n      if (itemTypes.includes('320')) {\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n      }\n      if (itemTypes.includes('flac')) {\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      // types.reverse()\n\n      return {\n        singer: item.author.replace(',', '、'),\n        name: item.title,\n        albumName: item.album_title,\n        albumId: item.album_id,\n        source: 'bd',\n        interval: formatPlayTime(parseInt(item.file_duration)),\n        songmid: item.song_id,\n        img: item.pic_s500,\n        lrc: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/index.js",
    "content": "import kw from './kw/index'\nimport kg from './kg/index'\nimport tx from './tx/index'\nimport wy from './wy/index'\nimport mg from './mg/index'\nimport bd from './bd/index'\nimport xm from './xm'\nimport { supportQuality } from './api-source'\n\n\nconst sources = {\n  sources: [\n    {\n      name: '酷我音乐',\n      id: 'kw',\n    },\n    {\n      name: '酷狗音乐',\n      id: 'kg',\n    },\n    {\n      name: 'QQ音乐',\n      id: 'tx',\n    },\n    {\n      name: '网易音乐',\n      id: 'wy',\n    },\n    {\n      name: '咪咕音乐',\n      id: 'mg',\n    },\n    {\n      name: '虾米音乐',\n      id: 'xm',\n    },\n    // {\n    //   name: '百度音乐',\n    //   id: 'bd',\n    // },\n  ],\n  kw,\n  kg,\n  tx,\n  wy,\n  mg,\n  bd,\n  xm,\n}\nexport default {\n  ...sources,\n  init() {\n    const tasks = []\n    for (let source of sources.sources) {\n      let sm = sources[source.id]\n      sm && sm.init && tasks.push(sm.init())\n    }\n    return Promise.all(tasks)\n  },\n  supportQuality,\n\n  async searchMusic({ name, singer, source: s, limit = 25 }) {\n    const trimStr = str => typeof str == 'string' ? str.trim() : str\n    const musicName = trimStr(name)\n    const tasks = []\n    const excludeSource = ['xm']\n    for (const source of sources.sources) {\n      if (!sources[source.id].musicSearch || source.id == s || excludeSource.includes(source.id)) continue\n      tasks.push(sources[source.id].musicSearch.search(`${musicName} ${singer || ''}`.trim(), 1, limit).catch(_ => null))\n    }\n    return (await Promise.all(tasks)).filter(s => s)\n  },\n\n  async findMusic({ name, singer, albumName, interval, source: s }) {\n    const lists = await this.searchMusic({ name, singer, source: s, limit: 25 })\n    // console.log(lists)\n    // console.log({ name, singer, albumName, interval, source: s })\n\n    const singersRxp = /、|&|;|；|\\/|,|，|\\|/\n    const sortSingle = singer => singersRxp.test(singer)\n      ? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、')\n      : (singer || '')\n    const sortMusic = (arr, callback) => {\n      const tempResult = []\n      for (let i = arr.length - 1; i > -1; i--) {\n        const item = arr[i]\n        if (callback(item)) {\n          delete item.fSinger\n          delete item.fMusicName\n          delete item.fAlbumName\n          delete item.fInterval\n          tempResult.push(item)\n          arr.splice(i, 1)\n        }\n      }\n      tempResult.reverse()\n      return tempResult\n    }\n    const getIntv = (interval) => {\n      if (!interval) return 0\n      // if (musicInfo._interval) return musicInfo._interval\n      let intvArr = interval.split(':')\n      let intv = 0\n      let unit = 1\n      while (intvArr.length) {\n        intv += parseInt(intvArr.pop()) * unit\n        unit *= 60\n      }\n      return intv\n    }\n    const trimStr = str => typeof str == 'string' ? str.trim() : (str || '')\n    const filterStr = str => typeof str == 'string' ? str.replace(/\\s|'|\\.|,|，|&|\"|、|\\(|\\)|（|）|`|~|-|<|>|\\||\\/|\\]|\\[|!|！/g, '') : String(str || '')\n    const fMusicName = filterStr(name).toLowerCase()\n    const fSinger = filterStr(sortSingle(singer)).toLowerCase()\n    const fAlbumName = filterStr(albumName).toLowerCase()\n    const fInterval = getIntv(interval)\n    const isEqualsInterval = (intv) => Math.abs((fInterval || intv) - (intv || fInterval)) < 5\n    const isIncludesName = (name) => (fMusicName.includes(name) || name.includes(fMusicName))\n    const isIncludesSinger = (singer) => fSinger ? (fSinger.includes(singer) || singer.includes(fSinger)) : true\n    const isEqualsAlbum = (album) => fAlbumName ? fAlbumName == album : true\n\n    const result = lists.map(source => {\n      for (const item of source.list) {\n        item.name = trimStr(item.name)\n        item.singer = trimStr(item.singer)\n        item.fSinger = filterStr(sortSingle(item.singer).toLowerCase())\n        item.fMusicName = filterStr(String(item.name ?? '').toLowerCase())\n        item.fAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())\n        item.fInterval = getIntv(item.interval)\n        // console.log(fMusicName, item.fMusicName, item.source)\n        if (!isEqualsInterval(item.fInterval)) {\n          item.name = null\n          continue\n        }\n        if (item.fMusicName == fMusicName && isIncludesSinger(item.fSinger)) return item\n      }\n      for (const item of source.list) {\n        if (item.name == null) continue\n        if (item.fSinger == fSinger && isIncludesName(item.fMusicName)) return item\n      }\n      for (const item of source.list) {\n        if (item.name == null) continue\n        if (isEqualsAlbum(item.fAlbumName) && isIncludesSinger(item.fSinger) && isIncludesName(item.fMusicName)) return item\n      }\n      return null\n    }).filter(s => s)\n    const newResult = []\n    if (result.length) {\n      newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.fMusicName == fMusicName && item.interval == interval))\n      newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName && item.fSinger == fSinger && item.fAlbumName == fAlbumName))\n      newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.fMusicName == fMusicName))\n      newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName && item.interval == interval))\n      newResult.push(...sortMusic(result, item => item.fSinger == fSinger && item.interval == interval))\n      newResult.push(...sortMusic(result, item => item.interval == interval))\n      newResult.push(...sortMusic(result, item => item.fMusicName == fMusicName))\n      newResult.push(...sortMusic(result, item => item.fSinger == fSinger))\n      newResult.push(...sortMusic(result, item => item.fAlbumName == fAlbumName))\n      for (const item of result) {\n        delete item.fSinger\n        delete item.fMusicName\n        delete item.fAlbumName\n        delete item.fInterval\n      }\n      newResult.push(...result)\n    }\n    // console.log(newResult)\n    return newResult\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/album.js",
    "content": "import { getMusicInfosByList } from './musicInfo'\nimport { createHttpFetch } from './util'\n\nexport default {\n  /**\n   * 通过AlbumId获取专辑信息\n   * @param {*} id\n   */\n  async getAlbumInfo(id) {\n    const albumInfoRequest = await createHttpFetch('http://kmrserviceretry.kugou.com/container/v1/album?dfid=1tT5He3kxrNC4D29ad1MMb6F&mid=22945702112173152889429073101964063697&userid=0&appid=1005&clientver=11589', {\n      method: 'POST',\n      body: {\n        appid: 1005,\n        clienttime: 1681833686,\n        clientver: 11589,\n        data: [{ album_id: id }],\n        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',\n        isBuy: 0,\n        key: 'e6f3306ff7e2afb494e89fbbda0becbf',\n        mid: '22945702112173152889429073101964063697',\n        show_album_tag: 0,\n      },\n    })\n    if (!albumInfoRequest) return Promise.reject(new Error('get album info failed.'))\n    const albumInfo = albumInfoRequest[0]\n\n    return {\n      name: albumInfo.album_name,\n      image: albumInfo.sizable_cover.replace('{size}', 240),\n      desc: albumInfo.intro,\n      authorName: albumInfo.author_name,\n      // play_count: this.formatPlayCount(info.count),\n    }\n  },\n  /**\n   * 通过AlbumId获取专辑\n   * @param {*} id\n   * @param {*} page\n   */\n  async getAlbumDetail(id, page = 1, limit = 200) {\n    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`)\n    if (!albumList.info) return Promise.reject(new Error('Get album list failed.'))\n\n    let result = await getMusicInfosByList(albumList.info)\n\n    const info = await this.getAlbumInfo(id)\n\n    return {\n      list: result || [],\n      page,\n      limit,\n      total: albumList.total,\n      source: 'kg',\n      info: {\n        name: info.name,\n        img: info.image,\n        desc: info.desc,\n        author: info.authorName,\n        // play_count: this.formatPlayCount(info.count),\n      },\n    }\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_test = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kg/${songInfo._types[type].hash}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n  getPic(songInfo) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/pic/kg/${songInfo.hash}`, {\n      method: 'get',\n      timeout,\n      headers,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))\n    })\n    return requestObj\n  },\n  getLyric(songInfo) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/lrc/kg/${songInfo.hash}`, {\n      method: 'get',\n      timeout,\n      headers,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))\n    })\n    return requestObj\n  },\n}\n\nexport default api_test\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/comment.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, dateFormat2 } from '../../index'\nimport { signatureParams } from './util'\n// import { getMusicInfoRaw } from './musicInfo'\n\nexport default {\n  _requestObj: null,\n  _requestObj2: null,\n  async getComment({ hash }, page = 1, limit = 20) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n\n    // const res_id = (await getMusicInfoRaw(hash)).classification?.[0]?.res_id\n    // if (!res_id) throw new Error('获取评论失败')\n\n    let timestamp = Date.now()\n    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`\n    // 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`\n    const _requestObj = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/newest?${params}&signature=${signatureParams(params)}`, {\n      headers: {\n        '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',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    // console.log(body)\n    if (statusCode != 200 || body.err_code !== 0) throw new Error('获取评论失败')\n    const total = body.count ?? 0\n    return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  async getHotComment({ hash }, page = 1, limit = 20) {\n    // console.log(songmid)\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n    let timestamp = Date.now()\n    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`\n    // https://github.com/GitHub-ZC/wp_MusicApi/blob/bf9307dd138dc8ac6c4f7de29361209d4f5b665f/routes/v1/kugou/comment.js#L53\n    const _requestObj2 = httpFetch(`http://m.comment.service.kugou.com/r/v1/rank/topliked?${params}&signature=${signatureParams(params)}`, {\n      headers: {\n        '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',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    // console.log(body)\n    if (statusCode != 200 || body.err_code !== 0) throw new Error('获取热门评论失败')\n    const total = body.count ?? 0\n    return { source: 'kg', comments: this.filterComment(body.list || []), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  async getReplyComment({ songmid, audioId }, replyId, page = 1, limit = 100) {\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n\n    songmid = songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题\n      ? audioId.split('_')[0]\n      : songmid\n\n    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}`, {\n      headers: {\n        'User-Agent': 'Android712-AndroidPhone-8983-18-0-COMMENT-wifi',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    // console.log(body)\n    if (statusCode != 200 || body.err_code !== 0) throw new Error('获取回复评论失败')\n    return { source: 'kg', comments: this.filterComment(body.list || []) }\n  },\n  replaceAt(raw, atList) {\n    atList.forEach((atobj) => {\n      raw = raw.replaceAll(`[at=${atobj.id}]`, `@${atobj.name} `)\n    })\n    return raw\n  },\n  filterComment(rawList) {\n    return rawList.map(item => {\n      let data = {\n        id: item.id,\n        text: decodeName((item.atlist ? this.replaceAt(item.content, item.atlist) : item.content) || ''),\n        images: item.images ? item.images.map(i => i.url) : [],\n        location: item.location,\n        time: item.addtime,\n        timeStr: dateFormat2(new Date(item.addtime).getTime()),\n        userName: item.user_name,\n        avatar: item.user_pic,\n        userId: item.user_id,\n        likedCount: item.like.likenum,\n        replyNum: item.reply_num,\n        reply: [],\n      }\n\n      return item.pcontent\n        ? {\n            id: item.id,\n            text: decodeName(item.pcontent),\n            time: null,\n            userName: item.puser,\n            avatar: null,\n            userId: item.puser_id,\n            likedCount: null,\n            replyNum: null,\n            reply: [data],\n          }\n        : data\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/hotSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName } from '../../index'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const _requestObj = httpFetch('http://gateway.kugou.com/api/v3/search/hot_tab?signature=ee44edb9d7155821412d220bcaf509dd&appid=1005&clientver=10026&plat=0', {\n      method: 'get',\n      headers: {\n        dfid: '1ssiv93oVqMp27cirf2CvoF1',\n        mid: '156798703528610303473757548878786007104',\n        clienttime: 1584257267,\n        'x-router': 'msearch.kugou.com',\n        'user-agent': 'Android9-AndroidPhone-10020-130-0-searchrecommendprotocol-wifi',\n        'kg-rc': 1,\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.errcode !== 0) throw new Error('获取热搜词失败')\n    // console.log(body, statusCode)\n    return { source: 'kg', list: this.filterList(body.data.list) }\n  },\n  filterList(rawList) {\n    const list = []\n    rawList.forEach(item => {\n      item.keywords.map(k => list.push(decodeName(k.keyword)))\n    })\n    return list\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/index.js",
    "content": "import leaderboard from './leaderboard'\nimport { apis } from '../api-source'\nimport songList from './songList'\nimport musicSearch from './musicSearch'\nimport pic from './pic'\nimport lyric from './lyric'\nimport hotSearch from './hotSearch'\nimport comment from './comment'\n// import tipSearch from './tipSearch'\n\nconst kg = {\n  // tipSearch,\n  leaderboard,\n  songList,\n  musicSearch,\n  hotSearch,\n  comment,\n  getMusicUrl(songInfo, type) {\n    return apis('kg').getMusicUrl(songInfo, type)\n  },\n  getLyric(songInfo) {\n    return lyric.getLyric(songInfo)\n  },\n  // getLyric(songInfo) {\n  //   return apis('kg').getLyric(songInfo)\n  // },\n  getPic(songInfo) {\n    return pic.getPic(songInfo)\n  },\n  getMusicDetailPageUrl(songInfo) {\n    return `https://www.kugou.com/song/#hash=${songInfo.hash}&album_id=${songInfo.albumId}`\n  },\n  // getPic(songInfo) {\n  //   return apis('kg').getPic(songInfo)\n  // },\n}\n\nexport default kg\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/leaderboard.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\nlet 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' }]\n\nexport default {\n  listDetailLimit: 100,\n  list: [\n    {\n      id: 'kgtop500',\n      name: 'TOP500',\n      bangid: '8888',\n    },\n    {\n      id: 'kgwlhgb',\n      name: '网络榜',\n      bangid: '23784',\n    },\n    {\n      id: 'kgbsb',\n      name: '飙升榜',\n      bangid: '6666',\n    },\n    {\n      id: 'kgfxb',\n      name: '分享榜',\n      bangid: '21101',\n    },\n    {\n      id: 'kgcyyb',\n      name: '纯音乐榜',\n      bangid: '33164',\n    },\n    {\n      id: 'kggfjqb',\n      name: '古风榜',\n      bangid: '33161',\n    },\n    {\n      id: 'kgyyjqb',\n      name: '粤语榜',\n      bangid: '33165',\n    },\n    {\n      id: 'kgomjqb',\n      name: '欧美榜',\n      bangid: '33166',\n    },\n    {\n      id: 'kgdyrgb',\n      name: '电音榜',\n      bangid: '33160',\n    },\n    {\n      id: 'kgjdrgb',\n      name: 'DJ热歌榜',\n      bangid: '24971',\n    },\n    {\n      id: 'kghyxgb',\n      name: '华语新歌榜',\n      bangid: '31308',\n    },\n  ],\n  getUrl(p, id, limit) {\n    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`\n  },\n  regExps: {\n    total: /total: '(\\d+)',/,\n    page: /page: '(\\d+)',/,\n    limit: /pagesize: '(\\d+)',/,\n    listData: /global\\.features = (\\[.+\\]);/,\n  },\n  _requestBoardsObj: null,\n  getBoardsData() {\n    if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()\n    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')\n    return this._requestBoardsObj.promise\n  },\n  getData(url) {\n    const requestDataObj = httpFetch(url)\n    return requestDataObj.promise\n  },\n  getSinger(singers) {\n    let arr = []\n    singers.forEach(singer => {\n      arr.push(singer.author_name)\n    })\n    return arr.join('、')\n  },\n  filterData(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      const types = []\n      const _types = {}\n      if (item.filesize !== 0) {\n        let size = sizeFormate(item.filesize)\n        types.push({ type: '128k', size, hash: item.hash })\n        _types['128k'] = {\n          size,\n          hash: item.hash,\n        }\n      }\n      if (item['320filesize'] !== 0) {\n        let size = sizeFormate(item['320filesize'])\n        types.push({ type: '320k', size, hash: item['320hash'] })\n        _types['320k'] = {\n          size,\n          hash: item['320hash'],\n        }\n      }\n      if (item.sqfilesize !== 0) {\n        let size = sizeFormate(item.sqfilesize)\n        types.push({ type: 'flac', size, hash: item.sqhash })\n        _types.flac = {\n          size,\n          hash: item.sqhash,\n        }\n      }\n      if (item.filesize_high !== 0) {\n        let size = sizeFormate(item.filesize_high)\n        types.push({ type: 'flac24bit', size, hash: item.hash_high })\n        _types.flac24bit = {\n          size,\n          hash: item.hash_high,\n        }\n      }\n      return {\n        singer: formatSingerName(item.authors, 'author_name'),\n        name: decodeName(item.songname),\n        albumName: decodeName(item.remark),\n        albumId: item.album_id,\n        songmid: item.audio_id,\n        source: 'kg',\n        interval: formatPlayTime(item.duration),\n        img: null,\n        lrc: null,\n        hash: item.hash,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n\n  filterBoardsData(rawList) {\n    // console.log(rawList)\n    let list = []\n    for (const board of rawList) {\n      if (board.isvol != 1) continue\n      list.push({\n        id: 'kg__' + board.rankid,\n        name: board.rankname,\n        bangid: String(board.rankid),\n      })\n    }\n    return list\n  },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // // console.log(response.body)\n    // if (response.statusCode !== 200 || response.body.errcode !== 0) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.data.info)\n    // console.log(list)\n    // // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'kg',\n    // }\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'kg',\n    }\n  },\n  async getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 3) throw new Error('try max num')\n    const { body } = await this.getData(this.getUrl(page, bangid, this.listDetailLimit))\n\n    if (body.errcode != 0) return this.getList(bangid, page, retryNum)\n\n    // console.log(body)\n    let total = body.data.total\n    let limit = 100\n    let listData = this.filterData(body.data.info)\n    // console.log(listData)\n    return {\n      total,\n      list: listData,\n      limit,\n      page,\n      source: 'kg',\n    }\n  },\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') id = id.replace('kg__', '')\n    return `https://www.kugou.com/yy/rank/home/1-${id}.html`\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeKrc } from '@common/utils/lyricUtils/kg'\n\nexport default {\n  getIntv(interval) {\n    if (!interval) return 0\n    let intvArr = interval.split(':')\n    let intv = 0\n    let unit = 1\n    while (intvArr.length) {\n      intv += (intvArr.pop()) * unit\n      unit *= 60\n    }\n    return parseInt(intv)\n  },\n  // getLyric(songInfo, tryNum = 0) {\n  //   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`, {\n  //     headers: {\n  //       'KG-RC': 1,\n  //       'KG-THash': 'expand_search_manager.cpp:852736169:451',\n  //       'User-Agent': 'KuGou2012-9020-ExpandSearchManager',\n  //     },\n  //   })\n  //   requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {\n  //     if (statusCode !== 200) {\n  //       if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))\n  //       let tryRequestObj = this.getLyric(songInfo, ++tryNum)\n  //       requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)\n  //       return tryRequestObj.promise\n  //     }\n  //     return {\n  //       lyric: body,\n  //       tlyric: '',\n  //     }\n  //   })\n  //   return requestObj\n  // },\n  searchLyric(name, hash, time, tryNum = 0) {\n    let requestObj = httpFetch(`http://lyrics.kugou.com/search?ver=1&man=yes&client=pc&keyword=${encodeURIComponent(name)}&hash=${hash}&timelength=${time}&lrctxt=1`, {\n      headers: {\n        'KG-RC': 1,\n        'KG-THash': 'expand_search_manager.cpp:852736169:451',\n        'User-Agent': 'KuGou2012-9020-ExpandSearchManager',\n      },\n    })\n    requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {\n      if (statusCode !== 200) {\n        if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))\n        let tryRequestObj = this.searchLyric(name, hash, time, ++tryNum)\n        requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)\n        return tryRequestObj.promise\n      }\n      if (body.candidates.length) {\n        let info = body.candidates[0]\n        return { id: info.id, accessKey: info.accesskey, fmt: (info.krctype == 1 && info.contenttype != 1) ? 'krc' : 'lrc' }\n      }\n      return null\n    })\n    return requestObj\n  },\n  getLyricDownload(id, accessKey, fmt, tryNum = 0) {\n    let requestObj = httpFetch(`http://lyrics.kugou.com/download?ver=1&client=pc&id=${id}&accesskey=${accessKey}&fmt=${fmt}&charset=utf8`, {\n      headers: {\n        'KG-RC': 1,\n        'KG-THash': 'expand_search_manager.cpp:852736169:451',\n        'User-Agent': 'KuGou2012-9020-ExpandSearchManager',\n      },\n    })\n    requestObj.promise = requestObj.promise.then(({ body, statusCode }) => {\n      if (statusCode !== 200) {\n        if (tryNum > 5) return Promise.reject(new Error('歌词获取失败'))\n        let tryRequestObj = this.getLyric(id, accessKey, fmt, ++tryNum)\n        requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)\n        return tryRequestObj.promise\n      }\n\n      switch (body.fmt) {\n        case 'krc':\n          return decodeKrc(body.content)\n        case 'lrc':\n          return {\n            lyric: Buffer.from(body.content, 'base64').toString('utf-8'),\n            tlyric: '',\n            rlyric: '',\n            lxlyric: '',\n          }\n        default:\n          return Promise.reject(new Error(`未知歌词格式: ${body.fmt}`))\n      }\n    })\n\n    return requestObj\n  },\n  getLyric(songInfo, tryNum = 0) {\n    let requestObj = this.searchLyric(songInfo.name, songInfo.hash, songInfo._interval || this.getIntv(songInfo.interval))\n\n    requestObj.promise = requestObj.promise.then(result => {\n      if (!result) return Promise.reject(new Error('Get lyric failed'))\n\n      let requestObj2 = this.getLyricDownload(result.id, result.accessKey, result.fmt)\n\n      requestObj.cancelHttp = requestObj2.cancelHttp.bind(requestObj2)\n\n      return requestObj2.promise\n    })\n    return requestObj\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/musicInfo.js",
    "content": "import { decodeName, formatPlayTime, sizeFormate } from '../../index'\nimport { createHttpFetch } from './util'\n\nconst createGetMusicInfosTask = (hashs) => {\n  let data = {\n    area_code: '1',\n    show_privilege: 1,\n    show_album_info: '1',\n    is_publish: '',\n    appid: 1005,\n    clientver: 11451,\n    mid: '1',\n    dfid: '-',\n    clienttime: Date.now(),\n    key: 'OIlwieks28dk2k092lksi2UIkp',\n    fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname,classification',\n  }\n  let list = hashs\n  let tasks = []\n  while (list.length) {\n    tasks.push(Object.assign({ data: list.slice(0, 100) }, data))\n    if (list.length < 100) break\n    list = list.slice(100)\n  }\n  let url = 'http://gateway.kugou.com/v3/album_audio/audio'\n  return tasks.map(task => createHttpFetch(url, {\n    method: 'POST',\n    body: task,\n    headers: {\n      'KG-THash': '13a3164',\n      'KG-RC': '1',\n      'KG-Fake': '0',\n      'KG-RF': '00869891',\n      'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi',\n      'x-router': 'kmr.service.kugou.com',\n    },\n  }).then(data => data.map(s => s[0])))\n}\n\nexport const filterMusicInfoList = (rawList) => {\n  // console.log(rawList)\n  let ids = new Set()\n  let list = []\n  rawList.forEach(item => {\n    if (!item) return\n    if (ids.has(item.audio_info.audio_id)) return\n    ids.add(item.audio_info.audio_id)\n    const types = []\n    const _types = {}\n    if (item.audio_info.filesize !== '0') {\n      let size = sizeFormate(parseInt(item.audio_info.filesize))\n      types.push({ type: '128k', size, hash: item.audio_info.hash })\n      _types['128k'] = {\n        size,\n        hash: item.audio_info.hash,\n      }\n    }\n    if (item.audio_info.filesize_320 !== '0') {\n      let size = sizeFormate(parseInt(item.audio_info.filesize_320))\n      types.push({ type: '320k', size, hash: item.audio_info.hash_320 })\n      _types['320k'] = {\n        size,\n        hash: item.audio_info.hash_320,\n      }\n    }\n    if (item.audio_info.filesize_flac !== '0') {\n      let size = sizeFormate(parseInt(item.audio_info.filesize_flac))\n      types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })\n      _types.flac = {\n        size,\n        hash: item.audio_info.hash_flac,\n      }\n    }\n    if (item.audio_info.filesize_high !== '0') {\n      let size = sizeFormate(parseInt(item.audio_info.filesize_high))\n      types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })\n      _types.flac24bit = {\n        size,\n        hash: item.audio_info.hash_high,\n      }\n    }\n    list.push({\n      singer: decodeName(item.author_name),\n      name: decodeName(item.songname),\n      albumName: decodeName(item.album_info.album_name),\n      albumId: item.album_info.album_id,\n      songmid: item.audio_info.audio_id,\n      source: 'kg',\n      interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000),\n      img: null,\n      lrc: null,\n      hash: item.audio_info.hash,\n      otherSource: null,\n      types,\n      _types,\n      typeUrl: {},\n    })\n  })\n  return list\n}\n\nexport const getMusicInfos = async(hashs) => {\n  return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(hashs)).then(data => data.flat()))\n}\n\nexport const getMusicInfoRaw = async(hash) => {\n  return Promise.all(createGetMusicInfosTask([{ hash }])).then(data => data.flat()[0])\n}\n\nexport const getMusicInfo = async(hash) => {\n  return getMusicInfos([{ hash }]).then(data => data[0])\n}\n\nexport const getMusicInfosByList = (list) => {\n  return getMusicInfos(list.map(item => ({ hash: item.hash })))\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/musicSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\n\nexport default {\n  limit: 30,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  musicSearch(str, page, limit) {\n    const searchRequest = httpFetch(`https://songsearch.kugou.com/song_search_v2?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${limit}&userid=0&clientver=&platform=WebFilter&filter=2&iscorrection=1&privilege_filter=0&area_code=1`)\n    return searchRequest.promise.then(({ body }) => body)\n  },\n  filterData(rawData) {\n    const types = []\n    const _types = {}\n    if (rawData.FileSize !== 0) {\n      let size = sizeFormate(rawData.FileSize)\n      types.push({ type: '128k', size, hash: rawData.FileHash })\n      _types['128k'] = {\n        size,\n        hash: rawData.FileHash,\n      }\n    }\n    if (rawData.HQFileSize !== 0) {\n      let size = sizeFormate(rawData.HQFileSize)\n      types.push({ type: '320k', size, hash: rawData.HQFileHash })\n      _types['320k'] = {\n        size,\n        hash: rawData.HQFileHash,\n      }\n    }\n    if (rawData.SQFileSize !== 0) {\n      let size = sizeFormate(rawData.SQFileSize)\n      types.push({ type: 'flac', size, hash: rawData.SQFileHash })\n      _types.flac = {\n        size,\n        hash: rawData.SQFileHash,\n      }\n    }\n    if (rawData.ResFileSize !== 0) {\n      let size = sizeFormate(rawData.ResFileSize)\n      types.push({ type: 'flac24bit', size, hash: rawData.ResFileHash })\n      _types.flac24bit = {\n        size,\n        hash: rawData.ResFileHash,\n      }\n    }\n    return {\n      singer: decodeName(formatSingerName(rawData.Singers, 'name')),\n      name: decodeName(rawData.SongName),\n      albumName: decodeName(rawData.AlbumName),\n      albumId: rawData.AlbumID,\n      songmid: rawData.Audioid,\n      source: 'kg',\n      interval: formatPlayTime(rawData.Duration),\n      _interval: rawData.Duration,\n      img: null,\n      lrc: null,\n      otherSource: null,\n      hash: rawData.FileHash,\n      types,\n      _types,\n      typeUrl: {},\n    }\n  },\n  handleResult(rawData) {\n    let ids = new Set()\n    const list = []\n    rawData.forEach(item => {\n      const key = item.Audioid + item.FileHash\n      if (ids.has(key)) return\n      ids.add(key)\n      list.push(this.filterData(item))\n      for (const childItem of item.Grp) {\n        const key = item.Audioid + item.FileHash\n        if (ids.has(key)) continue\n        ids.add(key)\n        list.push(this.filterData(childItem))\n      }\n    })\n    return list\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n    // http://newlyric.kuwo.cn/newlyric.lrc?62355680\n    return this.musicSearch(str, page, limit).then(result => {\n      if (!result || result.error_code !== 0) return this.search(str, page, limit, retryNum)\n      let list = this.handleResult(result.data.lists)\n\n      if (list == null) return this.search(str, page, limit, retryNum)\n\n      this.total = result.data.total\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return Promise.resolve({\n        list,\n        allPage: this.allPage,\n        limit,\n        total: this.total,\n        source: 'kg',\n      })\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/pic.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  getPic(songInfo) {\n    const requestObj = httpFetch(\n      'http://media.store.kugou.com/v1/get_res_privilege',\n      {\n        method: 'POST',\n        headers: {\n          'KG-RC': 1,\n          'KG-THash': 'expand_search_manager.cpp:852736169:451',\n          'User-Agent': 'KuGou2012-9020-ExpandSearchManager',\n        },\n        body: {\n          appid: 1001,\n          area_code: '1',\n          behavior: 'play',\n          clientver: '9020',\n          need_hash_offset: 1,\n          relate: 1,\n          resource: [\n            {\n              album_audio_id:\n                songInfo.songmid.length == 32 // 修复歌曲ID存储变更导致图片获取失败的问题\n                  ? songInfo.audioId.split('_')[0]\n                  : songInfo.songmid,\n              album_id: songInfo.albumId,\n              hash: songInfo.hash,\n              id: 0,\n              name: `${songInfo.singer} - ${songInfo.name}.mp3`,\n              type: 'audio',\n            },\n          ],\n          token: '',\n          userid: 2626431536,\n          vip: 1,\n        },\n      },\n    )\n    return requestObj.promise.then(({ body }) => {\n      if (body.error_code !== 0) return Promise.reject(new Error('图片获取失败'))\n      let info = body.data[0].info\n      const img = info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image\n      if (!img) return Promise.reject(new Error('Pic get failed'))\n      return img\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/singer.js",
    "content": "import { getMusicInfosByList } from './musicInfo'\nimport { createHttpFetch } from './util'\n\nexport default {\n  /**\n   * 获取歌手信息\n   * @param {*} id\n   */\n  getInfo(id) {\n    if (id == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0\n    return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${id}`).then(body => {\n      if (!body) throw new Error('get singer info faild.')\n\n      return {\n        source: 'kg',\n        id: body.singerid,\n        info: {\n          name: body.singername,\n          desc: body.intro,\n          avatar: body.imgurl.replace('{size}', 480),\n          gender: body.grade === 1 ? 'man' : 'woman',\n        },\n        count: {\n          music: body.songcount,\n          album: body.albumcount,\n        },\n      }\n    })\n  },\n  /**\n   * 获取歌手专辑列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  getAlbumList(id, page = 1, limit = 10) {\n    if (id == 0) throw new Error('歌手不存在')\n    return createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/album?singerid=${id}&page=${page}&pagesize=${limit}`).then(body => {\n      if (!body.info) throw new Error('get singer album list faild.')\n\n      const list = this.filterAlbumList(body.info)\n      return {\n        source: 'kg',\n        list,\n        limit,\n        page,\n        total: body.total,\n      }\n    })\n  },\n  /**\n   * 获取歌手歌曲列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  async getSongList(id, page = 1, limit = 100) {\n    if (id == 0) throw new Error('歌手不存在')\n    const body = await createHttpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${id}&page=${page}&pagesize=${limit}`)\n    if (!body.info) throw new Error('get singer song list faild.')\n\n    const list = await getMusicInfosByList(body.info)\n    return {\n      source: 'kg',\n      list,\n      limit,\n      page,\n      total: body.total,\n    }\n  },\n  filterAlbumList(raw) {\n    return raw.map(item => {\n      return {\n        id: item.albumid,\n        count: item.songcount,\n        info: {\n          name: item.albumname,\n          author: item.singername,\n          img: item.replaceAll('{size}', '480'),\n          desc: item.intro,\n        },\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index'\nimport infSign from '@renderer/utils/musicSdk/kg/vendors/infSign.min'\nimport { signatureParams } from './util'\n\nconst handleSignature = (id, page, limit) => new Promise((resolve, reject) => {\n  infSign({ appid: 1058, type: 0, module: 'playlist', page, pagesize: limit, specialid: id }, null, {\n    useH5: !0,\n    isCDN: !0,\n    callback(i) {\n      resolve(i.signature)\n    },\n  })\n})\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_listInfo: null,\n  _requestObj_list: null,\n  _requestObj_listRecommend: null,\n  listDetailLimit: 10000,\n  currentTagInfo: {\n    id: undefined,\n    info: undefined,\n  },\n  sortList: [\n    {\n      name: '推荐',\n      id: '5',\n    },\n    {\n      name: '最热',\n      id: '6',\n    },\n    {\n      name: '最新',\n      id: '7',\n    },\n    {\n      name: '热藏',\n      id: '3',\n    },\n    {\n      name: '飙升',\n      id: '8',\n    },\n  ],\n  cache: new Map(),\n  regExps: {\n    listData: /global\\.data = (\\[.+\\]);/,\n    listInfo: /global = {[\\s\\S]+?name: \"(.+)\"[\\s\\S]+?pic: \"(.+)\"[\\s\\S]+?};/,\n    // https://www.kugou.com/yy/special/single/1067062.html\n    listDetailLink: /^.+\\/(\\d+)\\.html(?:\\?.*|&.*$|#.*$|$)/,\n  },\n  // async getGlobalSpecialId(specialId) {\n  //   return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, {\n  //     headers: {\n  //       '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',\n  //     },\n  //   }).promise.then(({ body }) => {\n  //     // console.log(body)\n  //     if (!body.data.global_specialid) Promise.reject(new Error('Failed to get global collection id.'))\n  //     return body.data.global_specialid\n  //   })\n  // },\n  // async getListInfoBySpecialId(special_id, retry = 0) {\n  //   if (++retry > 2) throw new Error('failed')\n  //   return httpFetch(`https://m.kugou.com/plist/list/${special_id}/?json=true`, {\n  //     headers: {\n  //       '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',\n  //     },\n  //     follow_max: 2,\n  //   }).promise.then(({ body }) => {\n  //     // console.log(body)\n  //     if (!body.info.list) return this.getListInfoBySpecialId(special_id, retry)\n  //     let listinfo = body.info.list\n  //     return {\n  //       listInfo: {\n  //         name: listinfo.specialname,\n  //         image: listinfo.imgurl.replace('{size}', '150'),\n  //         intro: listinfo.intro,\n  //         author: listinfo.nickname,\n  //         playcount: listinfo.playcount,\n  //         total: listinfo.songcount,\n  //       },\n  //       globalSpecialId: listinfo.global_specialid,\n  //     }\n  //   })\n  // },\n  // async getSongListDetailByGlobalSpecialId(id, page, limit = 100, retry = 0) {\n  //   if (++retry > 2) throw new Error('failed')\n  //   console.log(id)\n  //   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`\n  //   return httpFetch(`http://pubsongscdn.tx.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params)}`).promise.then(({ body }) => {\n  //     // console.log(body)\n  //     if (body.data?.info == null) return this.getSongListDetailByGlobalSpecialId(id, page, limit, retry)\n  //     return body.data.info\n  //   })\n  // },\n  parseHtmlDesc(html) {\n    const prefix = '<div class=\"pc_specail_text pc_singer_tab_content\" id=\"specailIntroduceWrap\">'\n    let index = html.indexOf(prefix)\n    if (index < 0) return null\n    const afterStr = html.substring(index + prefix.length)\n    index = afterStr.indexOf('</div>')\n    if (index < 0) return null\n    return decodeName(afterStr.substring(0, index))\n  },\n  async getListDetailBySpecialId(id, page, tryNum = 0) {\n    if (tryNum > 2) throw new Error('try max num')\n\n    const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise\n    let listData = body.match(this.regExps.listData)\n    let listInfo = body.match(this.regExps.listInfo)\n    if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)\n    let list = await this.getMusicInfos(JSON.parse(listData[1]))\n    // listData = this.filterData(JSON.parse(listData[1]))\n    let name\n    let pic\n    if (listInfo) {\n      name = listInfo[1]\n      pic = listInfo[2]\n    }\n    let desc = this.parseHtmlDesc(body)\n\n\n    return {\n      list,\n      page: 1,\n      limit: 10000,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name,\n        img: pic,\n        desc,\n        // author: body.result.info.userinfo.username,\n        // play_count: formatPlayCount(body.result.listen_num),\n      },\n    }\n\n    // const globalSpecialId = await this.getGlobalSpecialId(id)\n    // const limit = 100\n    // const listData = await this.getSongListDetailByGlobalSpecialId(globalSpecialId, page, limit)\n    // if (!Array.isArray(listData))\n    // return this.getUserListDetail2(globalSpecialId)\n    // return {\n    //   list: this.filterDatav9(listData),\n    //   page,\n    //   limit,\n    //   total: listInfo.total,\n    //   source: 'kg',\n    //   info: {\n    //     name: listInfo.name,\n    //     img: listInfo.image,\n    //     desc: listInfo.intro,\n    //     author: listInfo.author,\n    //     play_count: formatPlayCount(listInfo.playcount),\n    //   },\n    // }\n  },\n  getInfoUrl(tagId) {\n    return tagId\n      ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}`\n      : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&'\n  },\n  getSongListUrl(sortId, tagId, page) {\n    if (tagId == null) tagId = ''\n    return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}`\n  },\n  getSongListDetailUrl(id) {\n    return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html`\n  },\n\n  filterInfoHotTag(rawData) {\n    const result = []\n    if (rawData.status !== 1) return result\n    for (const key of Object.keys(rawData.data)) {\n      let tag = rawData.data[key]\n      result.push({\n        id: tag.special_id,\n        name: tag.special_name,\n        source: 'kg',\n      })\n    }\n    return result\n  },\n  filterTagInfo(rawData) {\n    const result = []\n    for (const name of Object.keys(rawData)) {\n      result.push({\n        name,\n        list: rawData[name].data.map(tag => ({\n          parent_id: tag.parent_id,\n          parent_name: tag.pname,\n          id: tag.id,\n          name: tag.name,\n          source: 'kg',\n        })),\n      })\n    }\n    return result\n  },\n\n  getSongList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_list = httpFetch(\n      this.getSongListUrl(sortId, tagId, page),\n    )\n    return this._requestObj_list.promise.then(({ body }) => {\n      if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum)\n      return this.filterList(body.special_db)\n    })\n  },\n  getSongListRecommend(tryNum = 0) {\n    if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_listRecommend = httpFetch(\n      'http://everydayrec.service.kugou.com/guess_special_recommend',\n      {\n        method: 'post',\n        headers: {\n          'User-Agent': 'KuGou2012-8275-web_browser_event_handler',\n        },\n        body: {\n          appid: 1001,\n          clienttime: 1566798337219,\n          clientver: 8275,\n          key: 'f1f93580115bb106680d2375f8032d96',\n          mid: '21511157a05844bd085308bc76ef3343',\n          platform: 'pc',\n          userid: '262643156',\n          return_min: 6,\n          return_max: 15,\n        },\n      },\n    )\n    return this._requestObj_listRecommend.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getSongListRecommend(++tryNum)\n      return this.filterList(body.data.special_list)\n    })\n  },\n  filterList(rawData) {\n    return rawData.map(item => ({\n      play_count: item.total_play_count || formatPlayCount(item.play_count),\n      id: 'id_' + item.specialid,\n      author: item.nickname,\n      name: item.specialname,\n      time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'),\n      img: item.img || item.imgurl,\n      total: item.songcount,\n      grade: item.grade,\n      desc: item.intro,\n      source: 'kg',\n    }))\n  },\n\n  async createHttp(url, options, retryNum = 0) {\n    if (retryNum > 2) throw new Error('try max num')\n    let result\n    try {\n      result = await httpFetch(url, options).promise\n    } catch (err) {\n      console.log(err)\n      return this.createHttp(url, options, ++retryNum)\n    }\n    // console.log(result.statusCode, result.body)\n    if (result.statusCode !== 200 ||\n      (\n        (result.body.error_code !== undefined\n          ? result.body.error_code\n          : result.body.errcode !== undefined\n            ? result.body.errcode\n            : result.body.err_code\n        ) !== 0)\n    ) return this.createHttp(url, options, ++retryNum)\n    if (result.body.data) return result.body.data\n    if (Array.isArray(result.body.info)) return result.body\n    return result.body.info\n  },\n\n  createTask(hashs) {\n    let data = {\n      area_code: '1',\n      show_privilege: 1,\n      show_album_info: '1',\n      is_publish: '',\n      appid: 1005,\n      clientver: 11451,\n      mid: '1',\n      dfid: '-',\n      clienttime: Date.now(),\n      key: 'OIlwieks28dk2k092lksi2UIkp',\n      fields: 'album_info,author_name,audio_info,ori_audio_name,base,songname',\n    }\n    let list = hashs\n    let tasks = []\n    while (list.length) {\n      tasks.push(Object.assign({ data: list.slice(0, 100) }, data))\n      if (list.length < 100) break\n      list = list.slice(100)\n    }\n    let url = 'http://gateway.kugou.com/v2/album_audio/audio'\n    return tasks.map(task => this.createHttp(url, {\n      method: 'POST',\n      body: task,\n      headers: {\n        'KG-THash': '13a3164',\n        'KG-RC': '1',\n        'KG-Fake': '0',\n        'KG-RF': '00869891',\n        'User-Agent': 'Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi',\n        'x-router': 'kmr.service.kugou.com',\n      },\n    }).then(data => data.map(s => s[0])))\n  },\n  async getMusicInfos(list) {\n    return this.filterData2(\n      await Promise.all(\n        this.createTask(\n          this.deDuplication(list)\n            .map(item => ({ hash: item.hash })),\n        ))\n        .then(([...datas]) => datas.flat()))\n  },\n\n  async getUserListDetailByCode(id) {\n    const songInfo = await this.createHttp('http://t.kugou.com/command/', {\n      method: 'POST',\n      headers: {\n        'KG-RC': 1,\n        'KG-THash': 'network_super_call.cpp:3676261689:379',\n        'User-Agent': '',\n      },\n      body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id },\n    })\n    // console.log(songInfo)\n    // type 1单曲，2歌单，3电台，4酷狗码，5别人的播放队列\n    let songList\n    let info = songInfo.info\n    switch (info.type) {\n      case 2:\n        if (!info.global_collection_id) return this.getListDetailBySpecialId(info.id)\n        break\n\n      default:\n        break\n    }\n    if (info.global_collection_id) return this.getUserListDetail2(info.global_collection_id)\n    if (info.userid != null) {\n      songList = await this.createHttp('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', {\n        method: 'POST',\n        headers: {\n          'KG-RC': 1,\n          'KG-THash': 'network_super_call.cpp:3676261689:379',\n          'User-Agent': '',\n        },\n        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 } },\n      })\n      // console.log(songList)\n    }\n    let list = await this.getMusicInfos(songList || songInfo.list)\n    return {\n      list,\n      page: 1,\n      limit: info.count,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name: info.name,\n        img: (info.img_size && info.img_size.replace('{size}', 240)) || info.img,\n        // desc: body.result.info.list_desc,\n        author: info.username,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetail3(chain, page) {\n    const songInfo = await this.createHttp(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, {\n      headers: {\n        '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',\n      },\n    })\n    if (!songInfo.list) {\n      if (songInfo.global_collection_id) return this.getUserListDetail2(songInfo.global_collection_id)\n      else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain))\n    }\n    let list = await this.getMusicInfos(songInfo.list)\n    // console.log(info, songInfo)\n    return {\n      list,\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name: songInfo.info.name,\n        img: songInfo.info.img,\n        // desc: body.result.info.list_desc,\n        author: songInfo.info.username,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  deDuplication(datas) {\n    let ids = new Set()\n    return datas.filter(({ hash }) => {\n      if (ids.has(hash)) return false\n      ids.add(hash)\n      return true\n    })\n  },\n\n  async decodeGcid(gcid) {\n    const params = 'dfid=-&appid=1005&mid=0&clientver=20109&clienttime=640612895&uuid=-'\n    const body = {\n      ret_info: 1,\n      data: [\n        {\n          id: gcid,\n          id_type: 2,\n        },\n      ],\n    }\n    const result = await this.createHttp(`https://t.kugou.com/v1/songlist/batch_decode?${params}&signature=${signatureParams(params, 'android', JSON.stringify(body))}`, {\n      method: 'POST',\n      headers: {\n        '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',\n        Referer: 'https://m.kugou.com/',\n      },\n      body,\n    })\n    return result.list[0].global_collection_id\n  },\n\n  async getUserListDetailByLink({ info }, link) {\n    let listInfo = info['0']\n    let total = listInfo.count\n    let tasks = []\n    let page = 0\n    while (total) {\n      const limit = total > 90 ? 90 : total\n      total -= limit\n      page += 1\n      tasks.push(this.createHttp(link.replace(/pagesize=\\d+/, 'pagesize=' + limit).replace(/page=\\d+/, 'page=' + page), {\n        headers: {\n          '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',\n          Referer: link,\n        },\n      }).then(data => data.list.info))\n    }\n    let result = await Promise.all(tasks).then(([...datas]) => datas.flat())\n    result = await this.getMusicInfos(result)\n    // console.log(result)\n    return {\n      list: result,\n      page,\n      limit: this.listDetailLimit,\n      total: result.length,\n      source: 'kg',\n      info: {\n        name: listInfo.name,\n        img: listInfo.pic && listInfo.pic.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.list_create_username,\n        // play_count: formatPlayCount(listInfo.count),\n      },\n    }\n  },\n  createGetListDetail2Task(id, total) {\n    let tasks = []\n    let page = 0\n    while (total) {\n      const limit = total > 300 ? 300 : total\n      total -= limit\n      page += 1\n      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=-'\n      tasks.push(this.createHttp(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, {\n        headers: {\n          mid: '1586163263991',\n          Referer: 'https://m3ws.kugou.com/share/index.php',\n          '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',\n          dfid: '-',\n          clienttime: '1586163263991',\n        },\n      }).then(data => data.info))\n    }\n    return Promise.all(tasks).then(([...datas]) => datas.flat())\n  },\n  async getUserListDetail2(global_collection_id) {\n    let id = global_collection_id\n    if (id.length > 1000) throw new Error('get list error')\n    const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'\n    let info = await this.createHttp(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, {\n      headers: {\n        mid: '1586163242519',\n        Referer: 'https://m3ws.kugou.com/share/index.php',\n        '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',\n        dfid: '-',\n        clienttime: '1586163242519',\n      },\n    })\n    const songInfo = await this.createGetListDetail2Task(id, info.songcount)\n    let list = await this.getMusicInfos(songInfo)\n    // console.log(info, songInfo, list)\n    return {\n      list,\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name: info.specialname,\n        img: info.imgurl && info.imgurl.replace('{size}', 240),\n        desc: info.intro,\n        author: info.nickname,\n        play_count: formatPlayCount(info.playcount),\n      },\n    }\n  },\n\n  async getListInfoByChain(chain) {\n    if (this.cache.has(chain)) return this.cache.get(chain)\n    const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {\n      headers: {\n        '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',\n      },\n    }).promise\n    let result = body.match(/var\\sphpParam\\s=\\s({.+?});/)\n    if (result) result = JSON.parse(result[1])\n    this.cache.set(chain, result)\n    return result\n  },\n\n  async getUserListDetailByPcChain(chain) {\n    let key = `${chain}_pc_list`\n    if (this.cache.has(key)) return this.cache.get(key)\n    const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n      },\n    }).promise\n    let result = body.match(/var\\sdataFromSmarty\\s=\\s(\\[.+?\\])/)\n    if (result) result = JSON.parse(result[1])\n    this.cache.set(chain, result)\n    result = await this.getMusicInfos(result)\n    // console.log(info, songInfo)\n    return result\n  },\n\n  async getUserListDetail4(songInfo, chain, page) {\n    const limit = 100\n    const [listInfo, list] = await Promise.all([\n      this.getListInfoByChain(chain),\n      this.getUserListDetailById(songInfo.id, page, limit),\n    ])\n    return {\n      list: list || [],\n      page,\n      limit,\n      total: list.length ?? 0,\n      source: 'kg',\n      info: {\n        name: listInfo.specialname,\n        img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.nickname,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetail5(chain) {\n    const [listInfo, list] = await Promise.all([\n      this.getListInfoByChain(chain),\n      this.getUserListDetailByPcChain(chain),\n    ])\n    return {\n      list: list || [],\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length ?? 0,\n      source: 'kg',\n      info: {\n        name: listInfo.specialname,\n        img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.nickname,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetailById(id, page, limit) {\n    const signature = await handleSignature(id, page, limit)\n    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}`, {\n      headers: {\n        Referer: 'https://m3ws.kugou.com/share/index.php',\n        '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',\n        dfid: '-',\n      },\n    })\n\n    // console.log(info)\n    let result = await this.getMusicInfos(info.info)\n    // console.log(info, songInfo)\n    return result\n  },\n\n  async getUserListDetail(link, page, retryNum = 0) {\n    if (retryNum > 3) return Promise.reject(new Error('link try max num'))\n    if (link.includes('#')) link = link.replace(/#.*$/, '')\n    if (link.includes('global_collection_id')) return this.getUserListDetail2(link.replace(/^.*?global_collection_id=(\\w+)(?:&.*$|#.*$|$)/, '$1'))\n    if (link.includes('gcid_')) {\n      let gcid = link.match(/gcid_\\w+/)?.[0]\n      if (gcid) {\n        const global_collection_id = await this.decodeGcid(gcid)\n        if (global_collection_id) return this.getUserListDetail2(global_collection_id)\n      }\n    }\n    if (link.includes('chain=')) return this.getUserListDetail3(link.replace(/^.*?chain=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n    if (link.includes('.html')) {\n      if (link.includes('zlist.html')) {\n        link = link.replace(/^(.*)zlist\\.html/, 'https://m3ws.kugou.com/zlist/list')\n        if (link.includes('pagesize')) {\n          link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)\n        } else {\n          link += `&pagesize=${this.listDetailLimit}&page=${page}`\n        }\n      } else if (!link.includes('song.html')) return this.getUserListDetail3(link.replace(/.+\\/(\\w+).html(?:\\?.*|&.*$|#.*$|$)/, '$1'), page)\n    }\n\n    const requestObj_listDetailLink = httpFetch(link, {\n      headers: {\n        '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',\n        Referer: link,\n      },\n    })\n    const { headers: { location }, statusCode, body } = await requestObj_listDetailLink.promise\n    // console.log(body, location)\n    if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)\n    if (location) {\n      // console.log(location)\n      if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\\w+)(?:&.*$|#.*$|$)/, '$1'))\n      if (location.includes('gcid_')) {\n        let gcid = link.match(/gcid_\\w+/)?.[0]\n        if (gcid) {\n          const global_collection_id = await this.decodeGcid(gcid)\n          if (global_collection_id) return this.getUserListDetail2(global_collection_id)\n        }\n      }\n      if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (location.includes('.html')) {\n        if (location.includes('zlist.html')) {\n          let link = location.replace(/^(.*)zlist\\.html/, 'https://m3ws.kugou.com/zlist/list')\n          if (link.includes('pagesize')) {\n            link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)\n          } else {\n            link += `&pagesize=${this.listDetailLimit}&page=${page}`\n          }\n          return this.getUserListDetail(link, page, ++retryNum)\n        } else return this.getUserListDetail3(location.replace(/.+\\/(\\w+).html(?:\\?.*|&.*$|#.*$|$)/, '$1'), page)\n      }\n      // console.log('location', location)\n      return this.getUserListDetail(location, page, ++retryNum)\n    }\n    if (typeof body == 'string') {\n      let global_collection_id = body.match(/\"global_collection_id\":\"(\\w+)\"/)?.[1]\n      if (!global_collection_id) {\n        let gcid = body.match(/\"encode_gic\":\"(\\w+)\"/)?.[1]\n        if (!gcid) gcid = body.match(/\"encode_src_gid\":\"(\\w+)\"/)?.[1]\n        if (gcid) global_collection_id = await this.decodeGcid(gcid)\n      }\n      if (!global_collection_id) throw new Error('get list error')\n      return this.getUserListDetail2(global_collection_id)\n    }\n    if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum)\n    return this.getUserListDetailByLink(body, link)\n  },\n\n  async getListDetail(id, page) { // 获取歌曲列表内的音乐\n    id = id.toString()\n    if (id.includes('special/single/')) {\n      id = id.replace(this.regExps.listDetailLink, '$1')\n    } else if (/https?:/.test(id)) {\n      // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1\n      return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page)\n    } else if (/^\\d+$/.test(id)) {\n      return this.getUserListDetailByCode(id)\n    } else if (id.startsWith('id_')) {\n      id = id.replace('id_', '')\n    }\n    // if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')\n\n    return this.getListDetailBySpecialId(id, page)\n  },\n  filterData(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      const types = []\n      const _types = {}\n      if (item.filesize !== 0) {\n        let size = sizeFormate(item.filesize)\n        types.push({ type: '128k', size, hash: item.hash })\n        _types['128k'] = {\n          size,\n          hash: item.hash,\n        }\n      }\n      if (item.filesize_320 !== 0) {\n        let size = sizeFormate(item.filesize_320)\n        types.push({ type: '320k', size, hash: item.hash_320 })\n        _types['320k'] = {\n          size,\n          hash: item.hash_320,\n        }\n      }\n      if (item.filesize_ape !== 0) {\n        let size = sizeFormate(item.filesize_ape)\n        types.push({ type: 'ape', size, hash: item.hash_ape })\n        _types.ape = {\n          size,\n          hash: item.hash_ape,\n        }\n      }\n      if (item.filesize_flac !== 0) {\n        let size = sizeFormate(item.filesize_flac)\n        types.push({ type: 'flac', size, hash: item.hash_flac })\n        _types.flac = {\n          size,\n          hash: item.hash_flac,\n        }\n      }\n      return {\n        singer: decodeName(item.singername),\n        name: decodeName(item.songname),\n        albumName: decodeName(item.album_name),\n        albumId: item.album_id,\n        songmid: item.audio_id,\n        source: 'kg',\n        interval: formatPlayTime(item.duration / 1000),\n        img: null,\n        lrc: null,\n        hash: item.hash,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  // getSinger(singers) {\n  //   let arr = []\n  //   singers?.forEach(singer => {\n  //     arr.push(singer.name)\n  //   })\n  //   return arr.join('、')\n  // },\n  // v9 API\n  // filterDatav9(rawList) {\n  //   console.log(rawList)\n  //   return rawList.map(item => {\n  //     const types = []\n  //     const _types = {}\n  //     item.relate_goods.forEach(qualityObj => {\n  //       if (qualityObj.level === 2) {\n  //         let size = sizeFormate(qualityObj.size)\n  //         types.push({ type: '128k', size, hash: qualityObj.hash })\n  //         _types['128k'] = {\n  //           size,\n  //           hash: qualityObj.hash,\n  //         }\n  //       } else if (qualityObj.level === 4) {\n  //         let size = sizeFormate(qualityObj.size)\n  //         types.push({ type: '320k', size, hash: qualityObj.hash })\n  //         _types['320k'] = {\n  //           size,\n  //           hash: qualityObj.hash,\n  //         }\n  //       } else if (qualityObj.level === 5) {\n  //         let size = sizeFormate(qualityObj.size)\n  //         types.push({ type: 'flac', size, hash: qualityObj.hash })\n  //         _types.flac = {\n  //           size,\n  //           hash: qualityObj.hash,\n  //         }\n  //       } else if (qualityObj.level === 6) {\n  //         let size = sizeFormate(qualityObj.size)\n  //         types.push({ type: 'flac24bit', size, hash: qualityObj.hash })\n  //         _types.flac24bit = {\n  //           size,\n  //           hash: qualityObj.hash,\n  //         }\n  //       }\n  //     })\n  //     const nameInfo = item.name.split(' - ')\n  //     return {\n  //       singer: this.getSinger(item.singerinfo),\n  //       name: decodeName((nameInfo[1] ?? nameInfo[0]).trim()),\n  //       albumName: decodeName(item.albuminfo.name),\n  //       albumId: item.albuminfo.id,\n  //       songmid: item.audio_id,\n  //       source: 'kg',\n  //       interval: formatPlayTime(item.timelen / 1000),\n  //       img: null,\n  //       lrc: null,\n  //       hash: item.hash,\n  //       types,\n  //       _types,\n  //       typeUrl: {},\n  //     }\n  //   })\n  // },\n\n  // hash list filter\n  filterData2(rawList) {\n    // console.log(rawList)\n    let ids = new Set()\n    let list = []\n    rawList.forEach(item => {\n      if (!item) return\n      if (ids.has(item.audio_info.audio_id)) return\n      ids.add(item.audio_info.audio_id)\n      const types = []\n      const _types = {}\n      if (item.audio_info.filesize !== '0') {\n        let size = sizeFormate(parseInt(item.audio_info.filesize))\n        types.push({ type: '128k', size, hash: item.audio_info.hash })\n        _types['128k'] = {\n          size,\n          hash: item.audio_info.hash,\n        }\n      }\n      if (item.audio_info.filesize_320 !== '0') {\n        let size = sizeFormate(parseInt(item.audio_info.filesize_320))\n        types.push({ type: '320k', size, hash: item.audio_info.hash_320 })\n        _types['320k'] = {\n          size,\n          hash: item.audio_info.hash_320,\n        }\n      }\n      if (item.audio_info.filesize_flac !== '0') {\n        let size = sizeFormate(parseInt(item.audio_info.filesize_flac))\n        types.push({ type: 'flac', size, hash: item.audio_info.hash_flac })\n        _types.flac = {\n          size,\n          hash: item.audio_info.hash_flac,\n        }\n      }\n      if (item.audio_info.filesize_high !== '0') {\n        let size = sizeFormate(parseInt(item.audio_info.filesize_high))\n        types.push({ type: 'flac24bit', size, hash: item.audio_info.hash_high })\n        _types.flac24bit = {\n          size,\n          hash: item.audio_info.hash_high,\n        }\n      }\n      list.push({\n        singer: decodeName(item.author_name),\n        name: decodeName(item.songname),\n        albumName: decodeName(item.album_info.album_name),\n        albumId: item.album_info.album_id,\n        songmid: item.audio_info.audio_id,\n        source: 'kg',\n        interval: formatPlayTime(parseInt(item.audio_info.timelength) / 1000),\n        img: null,\n        lrc: null,\n        hash: item.audio_info.hash,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    return list\n  },\n\n  // 获取列表信息\n  getListInfo(tagId, tryNum = 0) {\n    if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId))\n    return this._requestObj_listInfo.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getListInfo(tagId, ++tryNum)\n      return {\n        limit: body.data.params.pagesize,\n        page: body.data.params.p,\n        total: body.data.params.total,\n        source: 'kg',\n      }\n    })\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page) {\n    let tasks = [this.getSongList(sortId, tagId, page)]\n    tasks.push(\n      this.currentTagInfo.id === tagId\n        ? Promise.resolve(this.currentTagInfo.info)\n        : this.getListInfo(tagId).then(info => {\n          this.currentTagInfo.id = tagId\n          this.currentTagInfo.info = Object.assign({}, info)\n          return info\n        }),\n    )\n    if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别，则顺便获取推荐列表\n    return Promise.all(tasks).then(([list, info, recommendList]) => {\n      if (recommendList) list.unshift(...recommendList)\n      return {\n        list,\n        ...info,\n      }\n    })\n  },\n\n  // 获取标签\n  getTags(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.getInfoUrl())\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getTags(++tryNum)\n      return {\n        hotTag: this.filterInfoHotTag(body.data.hotTag),\n        tags: this.filterTagInfo(body.data.tagids),\n        source: 'kg',\n      }\n    })\n  },\n\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') {\n      if (/^https?:\\/\\//.test(id)) return id\n      id = id.replace('id_', '')\n    }\n    return `https://www.kugou.com/yy/special/single/${id}.html`\n  },\n\n  search(text, page, limit = 20) {\n    // 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\n    // 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`)\n    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`)\n      .promise.then(({ body }) => {\n        if (body.errcode != 0) throw new Error('filed')\n        // console.log(body.data.info)\n        return {\n          list: body.data.info.map(item => {\n            return {\n              play_count: formatPlayCount(item.playcount),\n              id: 'id_' + item.specialid,\n              author: item.nickname,\n              name: item.specialname,\n              time: dateFormat(item.publishtime, 'Y-M-D'),\n              img: item.imgurl,\n              grade: item.grade,\n              desc: item.intro,\n              total: item.songcount,\n              source: 'kg',\n            }\n          }),\n          limit,\n          total: body.data.total,\n          source: 'kg',\n        }\n      })\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/temp/musicSearch-new.js",
    "content": "import { decodeName, formatPlayTime, sizeFormate } from '../../index'\nimport { signatureParams, createHttpFetch } from './util'\nimport { formatSingerName } from '../../utils'\n\nexport default {\n  limit: 30,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  musicSearch(str, page, limit) {\n    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)\n    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}`, {\n      headers: {\n        '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',\n        referer: 'https://kugou.com',\n      },\n    }).then(body => body)\n  },\n  filterList(raw) {\n    let ids = new Set()\n    const list = []\n\n    raw.forEach(item => {\n      if (ids.has(item.Audioid)) return\n      ids.add(item.Audioid)\n\n      const types = []\n      const _types = {}\n      if (item.FileSize !== 0) {\n        let size = sizeFormate(item.FileSize)\n        types.push({ type: '128k', size, hash: item.FileHash })\n        _types['128k'] = {\n          size,\n          hash: item.FileHash,\n        }\n      }\n      if (item.HQ != undefined) {\n        let size = sizeFormate(item.HQ.FileSize)\n        types.push({ type: '320k', size, hash: item.HQ.Hash })\n        _types['320k'] = {\n          size,\n          hash: item.HQ.Hash,\n        }\n      }\n      if (item.SQ != undefined) {\n        let size = sizeFormate(item.SQ.FileSize)\n        types.push({ type: 'flac', size, hash: item.SQ.Hash })\n        _types.flac = {\n          size,\n          hash: item.SQ.Hash,\n        }\n      }\n      if (item.Res != undefined) {\n        let size = sizeFormate(item.Res.FileSize)\n        types.push({ type: 'flac24bit', size, hash: item.Res.Hash })\n        _types.flac24bit = {\n          size,\n          hash: item.Res.Hash,\n        }\n      }\n      list.push({\n        singer: decodeName(formatSingerName(item.Singers)),\n        name: decodeName(item.SongName),\n        albumName: decodeName(item.AlbumName),\n        albumId: item.AlbumID,\n        songmid: item.Audioid,\n        source: 'kg',\n        interval: formatPlayTime(item.Duration),\n        _interval: item.Duration,\n        img: null,\n        lrc: null,\n        otherSource: null,\n        hash: item.FileHash,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n\n    return list\n  },\n  handleResult(rawData) {\n    const rawList = []\n    rawData.forEach(item => {\n      rawList.push(item)\n      item.Grp.forEach(e => rawList.push(e))\n    })\n\n    return this.filterList(rawList)\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n\n    return this.musicSearch(str, page, limit).then(data => {\n      let list = this.handleResult(data.lists)\n      if (!list) return this.search(str, page, limit, retryNum)\n\n      this.total = data.total\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return Promise.resolve({\n        list,\n        allPage: this.allPage,\n        limit,\n        total: this.total,\n        source: 'kg',\n      })\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/temp/songList-new.js",
    "content": "import { httpFetch } from '../../../request'\nimport { formatSingerName } from '../../utils'\nimport { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../../index'\nimport { signatureParams, createHttpFetch } from './../util'\nimport { getMusicInfosByList } from '../musicInfo'\nimport album from '../album'\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_listInfo: null,\n  _requestObj_list: null,\n  _requestObj_listRecommend: null,\n  listDetailLimit: 10000,\n  currentTagInfo: {\n    id: undefined,\n    info: undefined,\n  },\n  sortList: [\n    {\n      name: '推荐',\n      id: '5',\n    },\n    {\n      name: '最热',\n      id: '6',\n    },\n    {\n      name: '最新',\n      id: '7',\n    },\n    {\n      name: '热藏',\n      id: '3',\n    },\n    {\n      name: '飙升',\n      id: '8',\n    },\n  ],\n  cache: new Map(),\n  collectionIdListInfoCache: new Map(),\n  regExps: {\n    listData: /global\\.data = (\\[.+\\]);/,\n    listInfo: /global = {[\\s\\S]+?name: \"(.+)\"[\\s\\S]+?pic: \"(.+)\"[\\s\\S]+?};/,\n    // https://www.kugou.com/yy/special/single/1067062.html\n    listDetailLink: /^.+\\/(\\d+)\\.html(?:\\?.*|&.*$|#.*$|$)/,\n  },\n\n  /**\n   * 获取歌曲列表内的音乐\n   * @param {*} id\n   * @param {*} page\n   */\n  async getListDetail(id, page) {\n    id = id.toString()\n\n    if (id.includes('special/single/')) id = id.replace(this.regExps.listDetailLink, '$1')\n    // fix https://www.kugou.com/songlist/xxx/?uid=xxx&chl=qq_client&cover=http%3A%2F%2Fimge.kugou.com%xxx.jpg&iszlist=1\n    if (/https?:/.test(id)) {\n      if (id.includes('#')) id = id.replace(/#.*$/, '')\n      if (id.includes('global_collection_id')) return this.getUserListDetailByCollectionId(id.replace(/^.*?global_collection_id=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (id.includes('chain=')) return this.getUserListDetail3(id.replace(/^.*?chain=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (id.includes('.html')) {\n        if (id.includes('zlist.html')) {\n          id = id.replace(/^(.*)zlist\\.html/, 'https://m3ws.kugou.com/zlist/list')\n          if (id.includes('pagesize')) {\n            id = id.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)\n          } else {\n            id += `&pagesize=${this.listDetailLimit}&page=${page}`\n          }\n        } else if (!id.includes('song.html')) return this.getUserListDetail3(id.replace(/.+\\/(\\w+).html(?:\\?.*|&.*$|#.*$|$)/, '$1'), page)\n      }\n      return this.getUserListDetail(id.replace(/^.*?http/, 'http'), page)\n    }\n    if (/^\\d+$/.test(id)) return this.getUserListDetailByCode(id, page)\n    if (id.startsWith('gid_')) return this.getUserListDetailByCollectionId(id.replace('gid_', ''), page)\n    if (id.startsWith('id_')) return this.getUserListDetailBySpecialId(id.replace('id_', ''), page)\n\n    return new Error('Failed.')\n  },\n\n  /**\n   * 获取SpecialId歌单\n   * @param {*} id\n   */\n  async getUserListDetailBySpecialId(id, page, tryNum = 0) {\n    if (tryNum > 2) throw new Error('try max num')\n\n    const { body } = await httpFetch(this.getSongListDetailUrl(id)).promise\n    let listData = body.match(this.regExps.listData)\n    let listInfo = body.match(this.regExps.listInfo)\n    if (!listData) return this.getListDetailBySpecialId(id, page, ++tryNum)\n    let list = await getMusicInfosByList(JSON.parse(listData[1]))\n    let name\n    let pic\n    if (listInfo) {\n      name = listInfo[1]\n      pic = listInfo[2]\n    }\n    let desc = this.parseHtmlDesc(body)\n\n\n    return {\n      list,\n      page: 1,\n      limit: 10000,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name,\n        img: pic,\n        desc,\n        // author: body.result.info.userinfo.username,\n        // play_count: formatPlayCount(body.result.listen_num),\n      },\n    }\n  },\n  parseHtmlDesc(html) {\n    const prefix = '<div class=\"pc_specail_text pc_singer_tab_content\" id=\"specailIntroduceWrap\">'\n    let index = html.indexOf(prefix)\n    if (index < 0) return null\n    const afterStr = html.substring(index + prefix.length)\n    index = afterStr.indexOf('</div>')\n    if (index < 0) return null\n    return decodeName(afterStr.substring(0, index))\n  },\n\n  /**\n   * 使用SpecialId获取CollectionId\n   * @param {*} specialId\n   */\n  async getCollectionIdBySpecialId(specialId) {\n    return httpFetch(`http://mobilecdnbj.kugou.com/api/v5/special/info?specialid=${specialId}`, {\n      headers: {\n        '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',\n      },\n    }).promise.then(({ body }) => {\n      // console.log('getCollectionIdBySpecialId', body)\n      if (!body.data.global_specialid) return Promise.reject(new Error('Failed to get global collection id.'))\n      return body.data.global_specialid\n    })\n  },\n\n  /**\n   * 获取歌单URL\n   * @param {*} sortId\n   * @param {*} tagId\n   * @param {*} page\n   */\n  getSongListUrl(sortId, tagId, page) {\n    if (tagId == null) tagId = ''\n    return `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_ajax=1&cdn=cdn&t=${sortId}&c=${tagId}&p=${page}`\n  },\n  getInfoUrl(tagId) {\n    return tagId\n      ? `http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&cdn=cdn&t=5&c=${tagId}`\n      : 'http://www2.kugou.kugou.com/yueku/v9/special/getSpecial?is_smarty=1&'\n  },\n  getSongListDetailUrl(id) {\n    return `http://www2.kugou.kugou.com/yueku/v9/special/single/${id}-5-9999.html`\n  },\n\n  filterInfoHotTag(rawData) {\n    const result = []\n    if (rawData.status !== 1) return result\n    for (const key of Object.keys(rawData.data)) {\n      let tag = rawData.data[key]\n      result.push({\n        id: tag.special_id,\n        name: tag.special_name,\n        source: 'kg',\n      })\n    }\n    return result\n  },\n\n  filterTagInfo(rawData) {\n    const result = []\n    for (const name of Object.keys(rawData)) {\n      result.push({\n        name,\n        list: rawData[name].data.map(tag => ({\n          parent_id: tag.parent_id,\n          parent_name: tag.pname,\n          id: tag.id,\n          name: tag.name,\n          source: 'kg',\n        })),\n      })\n    }\n    return result\n  },\n  filterSongList(rawData) {\n    return rawData.map(item => ({\n      play_count: item.total_play_count || formatPlayCount(item.play_count),\n      id: 'id_' + item.specialid,\n      author: item.nickname,\n      name: item.specialname,\n      time: dateFormat(item.publish_time || item.publishtime, 'Y-M-D'),\n      img: item.img || item.imgurl,\n      total: item.songcount,\n      grade: item.grade,\n      desc: item.intro,\n      source: 'kg',\n    }))\n  },\n\n  getSongList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_list = httpFetch(\n      this.getSongListUrl(sortId, tagId, page),\n    )\n    return this._requestObj_list.promise.then(({ body }) => {\n      if (!body || body.status !== 1) return this.getSongList(sortId, tagId, page, ++tryNum)\n      return this.filterSongList(body.special_db)\n    })\n  },\n  getSongListRecommend(tryNum = 0) {\n    if (this._requestObj_listRecommend) this._requestObj_listRecommend.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_listRecommend = httpFetch(\n      'http://everydayrec.service.kugou.com/guess_special_recommend',\n      {\n        method: 'post',\n        headers: {\n          'User-Agent': 'KuGou2012-8275-web_browser_event_handler',\n        },\n        body: {\n          appid: 1001,\n          clienttime: 1566798337219,\n          clientver: 8275,\n          key: 'f1f93580115bb106680d2375f8032d96',\n          mid: '21511157a05844bd085308bc76ef3343',\n          platform: 'pc',\n          userid: '262643156',\n          return_min: 6,\n          return_max: 15,\n        },\n      },\n    )\n    return this._requestObj_listRecommend.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getSongListRecommend(++tryNum)\n      return this.filterSongList(body.data.special_list)\n    })\n  },\n\n  /**\n   * 通过CollectionId获取歌单详情\n   * @param {*} id\n   */\n  async getUserListInfoByCollectionId(id) {\n    if (!id || id.length > 1000) return Promise.reject(new Error('get list error'))\n    if (this.collectionIdListInfoCache.has(id)) return this.collectionIdListInfoCache.get(id)\n\n    const params = `appid=1058&specialid=0&global_specialid=${id}&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-`\n    return createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, {\n      headers: {\n        mid: '1586163242519',\n        Referer: 'https://m3ws.kugou.com/share/index.php',\n        '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',\n        dfid: '-',\n        clienttime: '1586163242519',\n      },\n    }).then(body => {\n      let info = {\n        type: body.type,\n        userName: body.nickname,\n        userAvatar: body.user_avatar,\n        imageUrl: body.imgurl,\n        desc: body.intro,\n        name: body.specialname,\n        globalSpecialid: body.global_specialid,\n        total: body.songcount,\n        playCount: body.playcount,\n      }\n\n      this.collectionIdListInfoCache.set(id, info)\n      return info\n    })\n  },\n  /**\n   * 通过SpecialId获取歌单\n   * @param {*} id\n   */\n  // async getUserListDetailBySpecialId(id, page = 1, limit = 300) {\n  //   if (!id || id.length > 1000) return Promise.reject(new Error('get list error.'))\n  //   const listInfo = await this.getListInfoBySpecialId(id)\n\n  //   const params = `specialid=${id}&need_sort=1&module=CloudMusic&clientver=11589&pagesize=${limit}&userid=0&page=${page}&type=0&area_code=1&appid=1005`\n  //   return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 2)}`, {\n  //     headers: {\n  //       'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi',\n  //     },\n  //   }).then(body => {\n  //     if (!body.info) return Promise.reject(new Error('Get list failed.'))\n  //     const songList = this.filterListByCollectionId(body.info)\n\n  //     return {\n  //       list: songList || [],\n  //       page,\n  //       limit,\n  //       total: body.count,\n  //       source: 'kg',\n  //       info: {\n  //         name: listInfo.name,\n  //         img: listInfo.image,\n  //         desc: listInfo.desc,\n  //         // author: listInfo.userName,\n  //         // play_count: formatPlayCount(listInfo.playCount),\n  //       },\n  //     }\n  //   })\n  // },\n  /**\n   * 通过CollectionId获取歌单\n   * @param {*} id\n   */\n  async getUserListDetailByCollectionId(id, page = 1, limit = 300) {\n    if (!id || id.length > 1000) return Promise.reject(new Error('ID error.'))\n    const listInfo = await this.getUserListInfoByCollectionId(id)\n\n    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`\n    return createHttpFetch(`http://pubsongs.kugou.com/v2/get_other_list_file?${params}&signature=${signatureParams(params, 'android')}`, {\n      headers: {\n        'User-Agent': 'Android10-AndroidPhone-11589-201-0-playlist-wifi',\n      },\n    }).then(body => {\n      if (!body.info) return Promise.reject(new Error('Get list failed.'))\n      const songList = this.filterListByCollectionId(body.info)\n\n      return {\n        list: songList || [],\n        page,\n        limit,\n        total: listInfo.total,\n        source: 'kg',\n        info: {\n          name: listInfo.name,\n          img: listInfo.imageUrl && listInfo.imageUrl.replace('{size}', 240),\n          desc: listInfo.desc,\n          author: listInfo.userName,\n          play_count: formatPlayCount(listInfo.playCount),\n        },\n      }\n    })\n  },\n  /**\n   * 过滤GlobalSpecialId歌单数据\n   * @param {*} rawData\n   */\n  filterListByCollectionId(rawData) {\n    let ids = new Set()\n    let list = []\n    rawData.forEach(item => {\n      if (!item) return\n      if (ids.has(item.hash)) return\n      ids.add(item.hash)\n      const types = []\n      const _types = {}\n\n      item.relate_goods.forEach(data => {\n        let size = sizeFormate(data.size)\n        switch (data.level) {\n          case 2:\n            types.push({ type: '128k', size, hash: data.hash })\n            _types['128k'] = {\n              size,\n              hash: data.hash,\n            }\n            break\n          case 4:\n            types.push({ type: '320k', size, hash: data.hash })\n            _types['320k'] = {\n              size,\n              hash: data.hash,\n            }\n            break\n          case 5:\n            types.push({ type: 'flac', size, hash: data.hash })\n            _types.flac = {\n              size,\n              hash: data.hash,\n            }\n            break\n          case 6:\n            types.push({ type: 'flac24bit', size, hash: data.hash })\n            _types.flac24bit = {\n              size,\n              hash: data.hash,\n            }\n            break\n        }\n      })\n\n      list.push({\n        singer: formatSingerName(item.singerinfo, 'name') || decodeName(item.name).split(' - ')[0].replace(/&/g, '、'),\n        name: decodeName(item.name).split(' - ')[1],\n        albumName: decodeName(item.albuminfo.name),\n        albumId: item.albuminfo.id,\n        songmid: item.audio_id,\n        source: 'kg',\n        interval: formatPlayTime(parseInt(item.timelen) / 1000),\n        img: null,\n        lrc: null,\n        hash: item.hash,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    return list\n  },\n  /**\n   * 通过酷狗码获取歌单\n   * @param {*} id\n   * @param {*} page\n   */\n  async getUserListDetailByCode(id, page = 1) {\n    // type 1单曲，2歌单，3电台，4酷狗码，5别人的播放队列\n    const codeData = await createHttpFetch('http://t.kugou.com/command/', {\n      method: 'POST',\n      headers: {\n        'KG-RC': 1,\n        'KG-THash': 'network_super_call.cpp:3676261689:379',\n        'User-Agent': '',\n      },\n      body: { appid: 1001, clientver: 9020, mid: '21511157a05844bd085308bc76ef3343', clienttime: 640612895, key: '36164c4015e704673c588ee202b9ecb8', data: id },\n    })\n    if (!codeData) return Promise.reject(new Error('Get list failed.'))\n    const codeInfo = codeData.info\n\n    switch (codeInfo.type) {\n      case 2:\n        if (!codeInfo.global_collection_id) return this.getUserListDetailBySpecialId(codeInfo.id, page)\n        break\n      case 3:\n        return album.getAlbumDetail(codeInfo.id, page)\n    }\n    if (codeInfo.global_collection_id) return this.getUserListDetailByCollectionId(codeInfo.global_collection_id, page)\n\n    if (codeInfo.userid != null) {\n      const songList = await createHttpFetch('http://www2.kugou.kugou.com/apps/kucodeAndShare/app/', {\n        method: 'POST',\n        headers: {\n          'KG-RC': 1,\n          'KG-THash': 'network_super_call.cpp:3676261689:379',\n          'User-Agent': '',\n        },\n        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 } },\n      })\n      // console.log(songList)\n      let list = await getMusicInfosByList(songList || codeInfo.list)\n      return {\n        list,\n        page: 1,\n        limit: codeInfo.count,\n        total: list.length,\n        source: 'kg',\n        info: {\n          name: codeInfo.name,\n          img: (codeInfo.img_size && codeInfo.img_size.replace('{size}', 240)) || codeInfo.img,\n          // desc: body.result.info.list_desc,\n          author: codeInfo.username,\n          // play_count: formatPlayCount(info.count),\n        },\n      }\n    }\n  },\n\n  async getUserListDetail3(chain, page) {\n    const songInfo = await createHttpFetch(`http://m.kugou.com/schain/transfer?pagesize=${this.listDetailLimit}&chain=${chain}&su=1&page=${page}&n=0.7928855356604456`, {\n      headers: {\n        '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',\n      },\n    })\n    if (!songInfo.list) {\n      if (songInfo.global_collection_id) return this.getUserListDetailByCollectionId(songInfo.global_collection_id, page)\n      else return this.getUserListDetail4(songInfo, chain, page).catch(() => this.getUserListDetail5(chain))\n    }\n    let list = await getMusicInfosByList(songInfo.list)\n    // console.log(info, songInfo)\n    return {\n      list,\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name: songInfo.info.name,\n        img: songInfo.info.img,\n        // desc: body.result.info.list_desc,\n        author: songInfo.info.username,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetailByLink({ info }, link) {\n    let listInfo = info['0']\n    let total = listInfo.count\n    let tasks = []\n    let page = 0\n    while (total) {\n      const limit = total > 90 ? 90 : total\n      total -= limit\n      page += 1\n      tasks.push(createHttpFetch(link.replace(/pagesize=\\d+/, 'pagesize=' + limit).replace(/page=\\d+/, 'page=' + page), {\n        headers: {\n          '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',\n          Referer: link,\n        },\n      }).then(data => data.list.info))\n    }\n    let result = await Promise.all(tasks).then(([...datas]) => datas.flat())\n    result = await getMusicInfosByList(result)\n    // console.log(result)\n    return {\n      list: result,\n      page,\n      limit: this.listDetailLimit,\n      total: result.length,\n      source: 'kg',\n      info: {\n        name: listInfo.name,\n        img: listInfo.pic && listInfo.pic.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.list_create_username,\n        // play_count: formatPlayCount(listInfo.count),\n      },\n    }\n  },\n  createGetListDetail2Task(id, total) {\n    let tasks = []\n    let page = 0\n    while (total) {\n      const limit = total > 300 ? 300 : total\n      total -= limit\n      page += 1\n      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=-'\n      tasks.push(createHttpFetch(`https://mobiles.kugou.com/api/v5/special/song_v2?${params}&signature=${signatureParams(params, 'web')}`, {\n        headers: {\n          mid: '1586163263991',\n          Referer: 'https://m3ws.kugou.com/share/index.php',\n          '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',\n          dfid: '-',\n          clienttime: '1586163263991',\n        },\n      }).then(data => data.info))\n    }\n    return Promise.all(tasks).then(([...datas]) => datas.flat())\n  },\n  async getUserListDetail2(global_collection_id) {\n    let id = global_collection_id\n    if (id.length > 1000) throw new Error('get list error')\n    const params = 'appid=1058&specialid=0&global_specialid=' + id + '&format=jsonp&srcappid=2919&clientver=20000&clienttime=1586163242519&mid=1586163242519&uuid=1586163242519&dfid=-'\n    let info = await createHttpFetch(`https://mobiles.kugou.com/api/v5/special/info_v2?${params}&signature=${signatureParams(params, 'web')}`, {\n      headers: {\n        mid: '1586163242519',\n        Referer: 'https://m3ws.kugou.com/share/index.php',\n        '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',\n        dfid: '-',\n        clienttime: '1586163242519',\n      },\n    })\n    const songInfo = await this.createGetListDetail2Task(id, info.songcount)\n    let list = await getMusicInfosByList(songInfo)\n    // console.log(info, songInfo, list)\n    return {\n      list,\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length,\n      source: 'kg',\n      info: {\n        name: info.specialname,\n        img: info.imgurl && info.imgurl.replace('{size}', 240),\n        desc: info.intro,\n        author: info.nickname,\n        play_count: formatPlayCount(info.playcount),\n      },\n    }\n  },\n\n  async getListInfoByChain(chain) {\n    if (this.cache.has(chain)) return this.cache.get(chain)\n    const { body } = await httpFetch(`https://m.kugou.com/share/?chain=${chain}&id=${chain}`, {\n      headers: {\n        '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',\n      },\n    }).promise\n    // console.log(body)\n    let result = body.match(/var\\sphpParam\\s=\\s({.+?});/)\n    if (result) result = JSON.parse(result[1])\n    this.cache.set(chain, result)\n    return result\n  },\n\n  async getUserListDetailByPcChain(chain) {\n    let key = `${chain}_pc_list`\n    if (this.cache.has(key)) return this.cache.get(key)\n    const { body } = await httpFetch(`http://www.kugou.com/share/${chain}.html`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n      },\n    }).promise\n    let result = body.match(/var\\sdataFromSmarty\\s=\\s(\\[.+?\\])/)\n    if (result) result = JSON.parse(result[1])\n    this.cache.set(chain, result)\n    result = await getMusicInfosByList(result)\n    // console.log(info, songInfo)\n    return result\n  },\n\n  async getUserListDetail4(songInfo, chain, page) {\n    const limit = 100\n    const [listInfo, list] = await Promise.all([\n      this.getListInfoByChain(chain),\n      this.getUserListDetailBySpecialId(songInfo.id, page, limit),\n    ])\n    return {\n      list: list || [],\n      page,\n      limit,\n      total: list.length ?? 0,\n      source: 'kg',\n      info: {\n        name: listInfo.specialname,\n        img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.nickname,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetail5(chain) {\n    const [listInfo, list] = await Promise.all([\n      this.getListInfoByChain(chain),\n      this.getUserListDetailByPcChain(chain),\n    ])\n    return {\n      list: list || [],\n      page: 1,\n      limit: this.listDetailLimit,\n      total: list.length ?? 0,\n      source: 'kg',\n      info: {\n        name: listInfo.specialname,\n        img: listInfo.imgurl && listInfo.imgurl.replace('{size}', 240),\n        // desc: body.result.info.list_desc,\n        author: listInfo.nickname,\n        // play_count: formatPlayCount(info.count),\n      },\n    }\n  },\n\n  async getUserListDetail(link, page, retryNum = 0) {\n    if (retryNum > 3) return Promise.reject(new Error('link try max num'))\n\n    const requestLink = httpFetch(link, {\n      headers: {\n        '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',\n        Referer: link,\n      },\n      follow_max: 2,\n    })\n    const { headers: { location }, statusCode, body } = await requestLink.promise\n    // console.log(body, location, statusCode)\n    if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)\n    if (typeof body == 'string') {\n      if (body.includes('\"global_collection_id\":')) return this.getUserListDetailByCollectionId(body.replace(/^[\\s\\S]+?\"global_collection_id\":\"(\\w+)\"[\\s\\S]+?$/, '$1'), page)\n      if (body.includes('\"albumid\":')) return album.getAlbumDetail(body.replace(/^[\\s\\S]+?\"albumid\":(\\w+)[\\s\\S]+?$/, '$1'), page)\n      if (body.includes('\"album_id\":') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\\s\\S]+?\"album_id\":(\\w+)[\\s\\S]+?$/, '$1'), page)\n      if (body.includes('list_id = \"') && link.includes('album/info')) return album.getAlbumDetail(body.replace(/^[\\s\\S]+?list_id = \"(\\w+)\"[\\s\\S]+?$/, '$1'), page)\n    }\n    if (location) {\n      // 概念版分享链接 https://t1.kugou.com/xxx\n      if (location.includes('global_specialid')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_specialid=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (location.includes('global_collection_id')) return this.getUserListDetailByCollectionId(location.replace(/^.*?global_collection_id=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\\w+)(?:&.*$|#.*$|$)/, '$1'), page)\n      if (location.includes('.html')) {\n        if (location.includes('zlist.html')) {\n          let link = location.replace(/^(.*)zlist\\.html/, 'https://m3ws.kugou.com/zlist/list')\n          if (link.includes('pagesize')) {\n            link = link.replace('pagesize=30', 'pagesize=' + this.listDetailLimit).replace('page=1', 'page=' + page)\n          } else {\n            link += `&pagesize=${this.listDetailLimit}&page=${page}`\n          }\n          return this.getUserListDetail(link, page, ++retryNum)\n        } else return this.getUserListDetail3(location.replace(/.+\\/(\\w+).html(?:\\?.*|&.*$|#.*$|$)/, '$1'), page)\n      }\n      return this.getUserListDetail(location, page, ++retryNum)\n    }\n    if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum)\n    return this.getUserListDetailByLink(body, link)\n  },\n\n  // 获取列表信息\n  getListInfo(tagId, tryNum = 0) {\n    if (this._requestObj_listInfo) this._requestObj_listInfo.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_listInfo = httpFetch(this.getInfoUrl(tagId))\n    return this._requestObj_listInfo.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getListInfo(tagId, ++tryNum)\n      return {\n        limit: body.data.params.pagesize,\n        page: body.data.params.p,\n        total: body.data.params.total,\n        source: 'kg',\n      }\n    })\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page) {\n    let tasks = [this.getSongList(sortId, tagId, page)]\n    tasks.push(\n      this.currentTagInfo.id === tagId\n        ? Promise.resolve(this.currentTagInfo.info)\n        : this.getListInfo(tagId).then(info => {\n          this.currentTagInfo.id = tagId\n          this.currentTagInfo.info = Object.assign({}, info)\n          return info\n        }),\n    )\n    if (!tagId && page === 1 && sortId === this.sortList[0].id) tasks.push(this.getSongListRecommend()) // 如果是所有类别，则顺便获取推荐列表\n    return Promise.all(tasks).then(([list, info, recommendList]) => {\n      if (recommendList) list.unshift(...recommendList)\n      return {\n        list,\n        ...info,\n      }\n    })\n  },\n\n  // 获取标签\n  getTags(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.getInfoUrl())\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.status !== 1) return this.getTags(++tryNum)\n      return {\n        hotTag: this.filterInfoHotTag(body.data.hotTag),\n        tags: this.filterTagInfo(body.data.tagids),\n        source: 'kg',\n      }\n    })\n  },\n\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') {\n      if (/^https?:\\/\\//.test(id)) return id\n      id = id.replace('id_', '')\n    }\n    return `https://www.kugou.com/yy/special/single/${id}.html`\n  },\n\n  search(text, page, limit = 20) {\n    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`\n    const url = encodeURI(`http://complexsearchretry.kugou.com/v1/search/special?${params}&signature=${signatureParams(params, 'android')}`)\n    return createHttpFetch(url).then(body => {\n      // console.log(body)\n      return {\n        list: body.lists.map(item => {\n          return {\n            play_count: formatPlayCount(item.total_play_count),\n            id: item.gid ? `gid_${item.gid}` : `id_${item.specialid}`,\n            author: item.nickname,\n            name: item.specialname,\n            time: dateFormat(item.publish_time, 'Y-M-D'),\n            img: item.img,\n            grade: item.grade,\n            desc: item.intro,\n            total: item.song_count,\n            source: 'kg',\n          }\n        }),\n        limit,\n        total: body.total,\n        source: 'kg',\n      }\n    })\n    // 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\n    // 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\n    // http://msearchretry.kugou.com/api/v3/search/special?keyword=${encodeURIComponent(text)}&page=${page}&pagesize=${limit}&showtype=10&filter=0&version=7910&sver=2\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/tipSearch.js",
    "content": "import { createHttpFetch } from './util'\n\nexport default {\n  requestObj: null,\n  cancelTipSearch() {\n    if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()\n  },\n  tipSearchBySong(str) {\n    this.cancelTipSearch()\n    this.requestObj = createHttpFetch(`https://searchtip.kugou.com/getSearchTip?MusicTipCount=10&keyword=${encodeURIComponent(str)}`, {\n      headers: {\n        referer: 'https://www.kugou.com/',\n      },\n    })\n    return this.requestObj.then(body => {\n      return body[0].RecordDatas\n    })\n  },\n  handleResult(rawData) {\n    return rawData.map(info => info.HintInfo)\n  },\n  async search(str) {\n    return this.tipSearchBySong(str).then(result => this.handleResult(result))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kg/util.js",
    "content": "import { toMD5 } from '../utils'\nimport { httpFetch } from '../../request'\n\n// s.content[0].lyricContent.forEach(([str]) => {\n//   console.log(str)\n// })\n\n/**\n * 签名\n * @param {*} params\n * @param {*} apiver\n */\nexport const signatureParams = (params, platform = 'android', body = '') => {\n  let keyparam = 'OIlwieks28dk2k092lksi2UIkp'\n  if (platform === 'web') keyparam = 'NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'\n  let param_list = params.split('&')\n  param_list.sort()\n  let sign_params = `${keyparam}${param_list.join('')}${body}${keyparam}`\n  return toMD5(sign_params)\n}\n\n/**\n * 创建一个适用于KG的Http请求\n * @param {*} url\n * @param {*} options\n * @param {*} retryNum\n */\nexport const createHttpFetch = async(url, options, retryNum = 0) => {\n  if (retryNum > 2) throw new Error('try max num')\n  let result\n  try {\n    result = await httpFetch(url, options).promise\n  } catch (err) {\n    console.log(err)\n    return createHttpFetch(url, options, ++retryNum)\n  }\n  // console.log(result.statusCode, result.body)\n  if (result.statusCode !== 200 ||\n    (\n      result.body.error_code ??\n      result.body.errcode ??\n      result.body.err_code) != 0\n  ) return createHttpFetch(url, options, ++retryNum)\n  if (result.body.data) return result.body.data\n  if (Array.isArray(result.body.info)) return result.body\n  return result.body.info\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/album.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName } from '../../index'\nimport { formatSinger, objStr2JSON } from './util'\n\n// let requestObj_list\nexport default {\n  limit_list: 36,\n  limit_song: 1000,\n  filterListDetail(rawList, albumName, albumId) {\n    // console.log(rawList)\n    // console.log(rawList.length, rawList2.length)\n    return rawList.map((item, inedx) => {\n      let formats = item.formats.split('|')\n      let types = []\n      let _types = {}\n      if (formats.includes('MP3128')) {\n        types.push({ type: '128k', size: null })\n        _types['128k'] = {\n          size: null,\n        }\n      }\n      // if (formats.includes('MP3192')) {\n      //   types.push({ type: '192k', size: null })\n      //   _types['192k'] = {\n      //     size: null,\n      //   }\n      // }\n      if (formats.includes('MP3H')) {\n        types.push({ type: '320k', size: null })\n        _types['320k'] = {\n          size: null,\n        }\n      }\n      // if (formats.includes('AL')) {\n      //   types.push({ type: 'ape', size: null })\n      //   _types.ape = {\n      //     size: null,\n      //   }\n      // }\n      if (formats.includes('ALFLAC')) {\n        types.push({ type: 'flac', size: null })\n        _types.flac = {\n          size: null,\n        }\n      }\n      if (formats.includes('HIRFLAC')) {\n        types.push({ type: 'flac24bit', size: null })\n        _types.flac24bit = {\n          size: null,\n        }\n      }\n      // types.reverse()\n      return {\n        singer: formatSinger(decodeName(item.artist)),\n        name: decodeName(item.name),\n        albumName,\n        albumId,\n        songmid: item.id,\n        source: 'kw',\n        interval: null,\n        img: item.pic,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  /**\n   * 格式化播放数量\n   * @param {*} num\n   */\n  formatPlayCount(num) {\n    if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿'\n    if (num > 10000) return parseInt(num / 1000) / 10 + '万'\n    return num\n  },\n  getAlbumListDetail(id, page, retryNum = 0) {\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n    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`)\n    return requestObj_listDetail.promise.then(({ statusCode, body }) => {\n      if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum)\n      body = objStr2JSON(body)\n      // console.log(body)\n      if (!body.musiclist) return this.getAlbumListDetail(id, page, ++retryNum)\n      body.name = decodeName(body.name)\n      return {\n        list: this.filterListDetail(body.musiclist, body.name, body.albumid),\n        page,\n        limit: this.limit_song,\n        total: parseInt(body.songnum),\n        source: 'kw',\n        info: {\n          name: body.name,\n          img: body.img || body.hts_img,\n          desc: decodeName(body.info),\n          author: decodeName(body.artist),\n          // play_count: this.formatPlayCount(body.playnum),\n        },\n      }\n    })\n  },\n  // getAlbumListDetail(id, page, retryNum = 0) {\n  //   if (retryNum > 2) return Promise.reject(new Error('try max num'))\n  //   return tokenRequest(`http://www.kuwo.cn/api/www/album/albumInfo?albumId=${id}&pn=${page}&rn=${this.limit_song}&httpsStatus=1`).then((resp) => {\n  //     return resp.promise.then(({ statusCode, body }) => {\n  //       console.log(body)\n  //       return Promise.reject(new Error('failed'))\n  //       // if (statusCode !== 200) return this.getAlbumListDetail(id, page, ++retryNum)\n  //       // const data = body.data\n  //       // console.log(data)\n  //       // if (!data.musicList) return this.getAlbumListDetail(id, page, ++retryNum)\n  //       // return {\n  //       //   list: this.filterListDetail(data.musiclist),\n  //       //   page,\n  //       //   limit: this.limit_song,\n  //       //   total: data.total,\n  //       //   source: 'kw',\n  //       //   info: {\n  //       //     name: data.album,\n  //       //     img: data.pic,\n  //       //     desc: data.albuminfo,\n  //       //     author: data.artist,\n  //       //     play_count: this.formatPlayCount(data.playCnt),\n  //       //   },\n  //       // }\n  //     })\n  //   })\n  // },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/api-temp.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_temp = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://tm.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, {\n      method: 'get',\n      headers,\n      timeout,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(body.msg))\n      }\n    })\n    return requestObj\n  },\n}\n\nexport default api_temp\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_test = {\n  // getMusicUrl(songInfo, type) {\n  //   const requestObj = httpFetch(`http://45.32.53.128:3002/m/kw/u/${songInfo.songmid}/${type}`, {\n  //     method: 'get',\n  //     headers,\n  //     timeout,\n  //   })\n  //   requestObj.promise = requestObj.promise.then(({ body }) => {\n  //     return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg))\n  //   })\n  //   return requestObj\n  // },\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/kw/${songInfo.songmid}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n}\n\nexport default api_test\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/comment.js",
    "content": "import { httpFetch } from '../../request'\nimport { dateFormat2 } from '../../index'\n\nexport default {\n  _requestObj: null,\n  _requestObj2: null,\n  async getComment({ songmid }, page = 1, limit = 20) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n\n    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`, {\n      headers: {\n        'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.code != '200') throw new Error('获取评论失败')\n    // console.log(body)\n\n    const total = body.comments_counts\n    return {\n      source: 'kw',\n      comments: this.filterComment(body.comments),\n      total,\n      page,\n      limit,\n      maxPage: Math.ceil(total / limit) || 1,\n    }\n  },\n  async getHotComment({ songmid }, page = 1, limit = 100) {\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n\n    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`, {\n      headers: {\n        'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    if (statusCode != 200 || body.code != '200') throw new Error('获取热门评论失败')\n    // console.log(body)\n\n    const total = body.hot_comments_counts\n    return {\n      source: 'kw',\n      comments: this.filterComment(body.hot_comments),\n      total,\n      page,\n      limit,\n      maxPage: Math.ceil(total / limit) || 1,\n    }\n  },\n  filterComment(rawList) {\n    if (!rawList) return []\n    return rawList.map(item => {\n      return {\n        id: item.id,\n        text: item.msg,\n        time: item.time,\n        timeStr: dateFormat2(Number(item.time) * 1000),\n        userName: item.u_name,\n        avatar: item.u_pic,\n        userId: item.u_id,\n        likedCount: item.like_num,\n        images: item.mpic ? [decodeURIComponent(item.mpic)] : [],\n        reply: item.child_comments\n          ? item.child_comments.map(i => {\n            return {\n              id: i.id,\n              text: i.msg,\n              time: i.time,\n              timeStr: dateFormat2(Number(i.time) * 1000),\n              userName: i.u_name,\n              avatar: i.u_pic,\n              userId: i.u_id,\n              likedCount: i.like_num,\n              images: i.mpic ? [i.mpic] : [],\n            }\n          })\n          : [],\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/hotSearch.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    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&notrace=0&uid=0&plat=kwplayer_ar&rformat=json&encoding=utf8&tabid=1', {\n      headers: {\n        'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9;)',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.status !== 'ok') throw new Error('获取热搜词失败')\n    // console.log(body, statusCode)\n    return { source: 'kw', list: this.filterList(body.tagvalue) }\n  },\n  filterList(rawList) {\n    return rawList.map(item => item.key)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/index.js",
    "content": "import { httpFetch } from '../../request'\nimport tipSearch from './tipSearch'\nimport musicSearch from './musicSearch'\nimport { formatSinger } from './util'\nimport leaderboard from './leaderboard'\nimport lyric from './lyric'\nimport pic from './pic'\nimport { apis } from '../api-source'\nimport songList from './songList'\nimport hotSearch from './hotSearch'\nimport comment from './comment'\n\nconst kw = {\n  _musicInfoRequestObj: null,\n  _musicInfoPromiseCancelFn: null,\n  _musicPicRequestObj: null,\n  _musicPicPromiseCancelFn: null,\n  // context: null,\n\n\n  // init(context) {\n  //   if (this.isInited) return\n  //   this.isInited = true\n  //   this.context = context\n\n  //   // this.musicSearch.search('我又想你了').then(res => {\n  //   //   console.log(res)\n  //   // })\n\n  //   // this.getMusicUrl('62355680', '320k').then(url => {\n  //   //   console.log(url)\n  //   // })\n  // },\n\n  tipSearch,\n  musicSearch,\n  leaderboard,\n  songList,\n  hotSearch,\n  comment,\n  getLyric(songInfo, isGetLyricx) {\n    // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer\n    return lyric.getLyric(songInfo, isGetLyricx)\n  },\n  handleMusicInfo(songInfo) {\n    return this.getMusicInfo(songInfo).then(info => {\n      // console.log(JSON.stringify(info))\n      songInfo.name = info.name\n      songInfo.singer = formatSinger(info.artist)\n      songInfo.img = info.pic\n      songInfo.albumName = info.album\n      return songInfo\n      // return Object.assign({}, songInfo, {\n      //   name: info.name,\n      //   singer: formatSinger(info.artist),\n      //   img: info.pic,\n      //   albumName: info.album,\n      // })\n    })\n  },\n\n  getMusicUrl(songInfo, type) {\n    return apis('kw').getMusicUrl(songInfo, type)\n  },\n\n  getMusicInfo(songInfo) {\n    if (this._musicInfoRequestObj) this._musicInfoRequestObj.cancelHttp()\n    this._musicInfoRequestObj = httpFetch(`http://www.kuwo.cn/api/www/music/musicInfo?mid=${songInfo.songmid}`)\n    return this._musicInfoRequestObj.promise.then(({ body }) => {\n      return body.code === 200 ? body.data : Promise.reject(new Error(body.msg))\n    })\n  },\n\n  getMusicUrls(musicInfo, cb) {\n    let tasks = []\n    let songId = musicInfo.songmid\n    musicInfo.types.forEach(type => {\n      tasks.push(kw.getMusicUrl(songId, type.type).promise)\n    })\n    Promise.all(tasks).then(urlInfo => {\n      let typeUrl = {}\n      urlInfo.forEach(info => {\n        typeUrl[info.type] = info.url\n      })\n      cb(typeUrl)\n    })\n  },\n\n  getPic(songInfo) {\n    return pic.getPic(songInfo)\n  },\n\n  getMusicDetailPageUrl(songInfo) {\n    return `http://www.kuwo.cn/play_detail/${songInfo.songmid}`\n  },\n\n  // init() {\n  //   return getToken()\n  // },\n}\n\nexport default kw\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/leaderboard.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, decodeName } from '../../index'\nimport { formatSinger, wbdCrypto } from './util'\n\n\nconst 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' }]\n\nconst sortQualityArray = array => {\n  const qualityMap = {\n    flac24bit: 4,\n    flac: 3,\n    '320k': 2,\n    '128k': 1,\n  }\n  const rawQualityArray = []\n  const newQualityArray = []\n\n  array.forEach((item, index) => {\n    const type = qualityMap[item.type]\n    if (!type) return\n    rawQualityArray.push({ type, index })\n  })\n\n  rawQualityArray.sort((a, b) => a.type - b.type)\n\n  rawQualityArray.forEach(item => {\n    newQualityArray.push(array[item.index])\n  })\n\n  return newQualityArray\n}\n\nexport default {\n  list: [\n    {\n      id: 'kwbiaosb',\n      name: '飙升榜',\n      bangid: 93,\n    },\n    {\n      id: 'kwregb',\n      name: '热歌榜',\n      bangid: 16,\n    },\n    {\n      id: 'kwhuiyb',\n      name: '会员榜',\n      bangid: 145,\n    },\n    {\n      id: 'kwdouyb',\n      name: '抖音榜',\n      bangid: 158,\n    },\n    {\n      id: 'kwqsb',\n      name: '趋势榜',\n      bangid: 187,\n    },\n    {\n      id: 'kwhuaijb',\n      name: '怀旧榜',\n      bangid: 26,\n    },\n    {\n      id: 'kwhuayb',\n      name: '华语榜',\n      bangid: 104,\n    },\n    {\n      id: 'kwyueyb',\n      name: '粤语榜',\n      bangid: 182,\n    },\n    {\n      id: 'kwoumb',\n      name: '欧美榜',\n      bangid: 22,\n    },\n    {\n      id: 'kwhanyb',\n      name: '韩语榜',\n      bangid: 184,\n    },\n    {\n      id: 'kwriyb',\n      name: '日语榜',\n      bangid: 183,\n    },\n  ],\n  // 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`,\n  regExps: {\n    mInfo: /level:(\\w+),bitrate:(\\d+),format:(\\w+),size:([\\w.]+)/,\n  },\n  limit: 100,\n  _requestBoardsObj: null,\n\n  getBoardsData() {\n    if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()\n    this._requestBoardsObj = httpFetch('http://qukudata.kuwo.cn/q.k?op=query&cont=tree&node=2&pn=0&rn=1000&fmt=json&level=2')\n    return this._requestBoardsObj.promise\n  },\n  getData(url) {\n    const requestDataObj = httpFetch(url)\n    return requestDataObj.promise\n  },\n  filterData(rawList) {\n    return rawList.map(item => {\n      let types = []\n      const _types = {}\n      const qualitys = new Set()\n\n      item.n_minfo.split(';').forEach(i => {\n        const info = i.match(this.regExps.mInfo)\n        if (!info) return\n\n        const quality = info[2]\n        const size = info[4].toLocaleUpperCase()\n\n        if (qualitys.has(quality)) return\n        qualitys.add(quality)\n\n        switch (quality) {\n          case '4000':\n            types.push({ type: 'flac24bit', size })\n            _types.flac24bit = { size }\n            break\n          case '2000':\n            types.push({ type: 'flac', size })\n            _types.flac = { size }\n            break\n          case '320':\n            types.push({ type: '320k', size })\n            _types['320k'] = { size }\n            break\n          case '128':\n            types.push({ type: '128k', size })\n            _types['128k'] = { size }\n            break\n        }\n      })\n      types = sortQualityArray(types)\n\n      return {\n        singer: formatSinger(decodeName(item.artist)),\n        name: decodeName(item.name),\n        albumName: decodeName(item.album),\n        albumId: item.albumId,\n        songmid: item.id,\n        source: 'kw',\n        interval: formatPlayTime(parseInt(item.duration)),\n        img: item.pic,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n\n  filterBoardsData(rawList) {\n    // console.log(rawList)\n    let list = []\n    for (const board of rawList) {\n      if (board.source != '1') continue\n      list.push({\n        id: 'kw__' + board.sourceid,\n        name: board.name,\n        bangid: String(board.sourceid),\n      })\n    }\n    return list\n  },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // console.log(response.body)\n    // if (response.statusCode !== 200 || !response.body.child) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.child)\n    // // console.log(list)\n    // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'kw',\n    // }\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'kw',\n    }\n  },\n\n  getList(id, page, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n\n    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 }\n    const requestUrl = `https://wbd.kuwo.cn/api/bd/bang/bang_info?${wbdCrypto.buildParam(requestBody)}`\n    const request = httpFetch(requestUrl).promise\n\n    return request.then(({ statusCode, body }) => {\n      const rawData = wbdCrypto.decodeData(body)\n      // console.log(rawData)\n      const data = rawData.data\n      if (statusCode !== 200 || rawData.code != 200 || !data.musiclist) return this.getList(id, page, retryNum)\n\n      const total = parseInt(data.total)\n      const list = this.filterData(data.musiclist)\n\n      return {\n        total,\n        list,\n        limit: this.limit,\n        page,\n        source: 'kw',\n      }\n    })\n  },\n\n  // getDetailPageUrl(id) {\n  //   return `http://www.kuwo.cn/rankList/${id}`\n  // },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeLyric, lrcTools } from './util'\nimport { decodeName } from '../../index'\n\n/*\nexport default {\n  formatTime(time) {\n    let m = parseInt(time / 60)\n    let s = (time % 60).toFixed(2)\n    return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)\n  },\n  sortLrcArr(arr) {\n    const lrcSet = new Set()\n    let lrc = []\n    let lrcT = []\n\n    for (const item of arr) {\n      if (lrcSet.has(item.time)) {\n        const tItem = lrc.pop()\n        tItem.time = lrc[lrc.length - 1].time\n        lrcT.push(tItem)\n        lrc.push(item)\n      } else {\n        lrc.push(item)\n        lrcSet.add(item.time)\n      }\n    }\n\n    if (lrcT.length && lrc.length > lrcT.length) {\n      const tItem = lrc.pop()\n      tItem.time = lrc[lrc.length - 1].time\n      lrcT.push(tItem)\n    }\n\n    return {\n      lrc,\n      lrcT,\n    }\n  },\n  transformLrc(songinfo, lrclist) {\n    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('') : '暂无歌词'}`\n  },\n  getLyric(songId) {\n    const requestObj = httpFetch(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${songId}`)\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      // console.log(body)\n      if (!body.data?.lrclist?.length) return Promise.reject(new Error('Get lyric failed'))\n      let lrcInfo\n      try {\n        lrcInfo = this.sortLrcArr(body.data.lrclist)\n      } catch {\n        return Promise.reject(new Error('Get lyric failed'))\n      }\n      // console.log(body.data.lrclist)\n      // console.log(lrcInfo.lrc, lrcInfo.lrcT)\n      // console.log({\n      //   lyric: decodeName(this.transformLrc(body.data.songinfo, lrc)),\n      //   tlyric: decodeName(this.transformLrc(body.data.songinfo, lrcT)),\n      // })\n      return {\n        lyric: decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrc)),\n        tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(body.data.songinfo, lrcInfo.lrcT)) : '',\n      }\n    })\n    return requestObj\n  },\n}\n */\n\nconst buf_key = Buffer.from('yeelion')\nconst buf_key_len = buf_key.length\nconst buildParams = (id, isGetLyricx) => {\n  let params = `user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_${id}`\n  if (isGetLyricx) params += '&lrcx=1'\n  const buf_str = Buffer.from(params)\n  const buf_str_len = buf_str.length\n  const output = new Uint16Array(buf_str_len)\n  let i = 0\n  while (i < buf_str_len) {\n    let j = 0\n    while (j < buf_key_len && i < buf_str_len) {\n      output[i] = buf_key[j] ^ buf_str[i]\n      i++\n      j++\n    }\n  }\n  return Buffer.from(output).toString('base64')\n}\n\n// console.log(buildParams('207527604', false))\n// console.log(buildParams('207527604', true))\n\nconst timeExp = /^\\[([\\d:.]*)\\]{1}/g\nconst existTimeExp = /\\[\\d{1,2}:.*\\d{1,4}\\]/\nconst lyricxTag = /^<-?\\d+,-?\\d+>/\nexport default {\n  /* sortLrcArr(arr) {\n    const lrcSet = new Set()\n    let lrc = []\n    let lrcT = []\n    let markIndex = []\n    for (const item of arr) {\n      if (lrcSet.has(item.time)) {\n        if (lrc.length < 2) continue\n        const index = lrc.findIndex(l => l.time == item.time)\n        markIndex.push(index)\n        if (index == lrc.length - 1) {\n          lrcT.push({ ...lrc[index], time: item.time })\n          lrc.push(item)\n        } else {\n          lrcT.push({ ...lrc[index], time: lrc[index + 1].time })\n          if (item.text) {\n            //   const lastIndex = lrc.length - 1\n            //   markIndex.push(lastIndex)\n            //   lrcT.push({ ...lrc[lastIndex], time: lrc[lastIndex - 1].time })\n            lrc.push(item)\n          }\n        }\n      } else {\n        lrc.push(item)\n        lrcSet.add(item.time)\n      }\n    }\n\n    // console.log(markIndex)\n    markIndex = Array.from(new Set(markIndex))\n    for (let index = markIndex.length - 1; index >= 0; index--) {\n      lrc.splice(markIndex[index], 1)\n    }\n\n    // if (lrcT.length) {\n    //   if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍，否则认为没有翻译\n    //     const tItem = lrc.pop()\n    //     tItem.time = lrc[lrc.length - 1].time\n    //     lrcT.push(tItem)\n    //   } else {\n    //     lrc = arr\n    //     lrcT = []\n    //   }\n    // }\n\n    console.log(lrc, lrcT)\n\n    return {\n      lrc,\n      lrcT,\n    }\n  }, */\n  sortLrcArr(arr) {\n    const lrcSet = new Set()\n    let lrc = []\n    let lrcT = []\n\n    let isLyricx = false\n    for (const item of arr) {\n      if (lrcSet.has(item.time)) {\n        if (lrc.length < 2) continue\n        const tItem = lrc.pop()\n        tItem.time = lrc[lrc.length - 1].time\n        lrcT.push(tItem)\n        lrc.push(item)\n      } else {\n        lrc.push(item)\n        lrcSet.add(item.time)\n      }\n      if (!isLyricx && lyricxTag.test(item.text)) isLyricx = true\n    }\n\n    if (!isLyricx && lrcT.length > lrc.length * 0.3 && lrc.length - lrcT.length > 6) {\n      throw new Error('failed')\n      // if (lrc.length * 0.4 < lrcT.length) { // 翻译数量需大于歌词数量的0.4倍，否则认为没有翻译\n      //   const tItem = lrc.pop()\n      //   tItem.time = lrc[lrc.length - 1].time\n      //   lrcT.push(tItem)\n      // } else {\n      //   lrc = arr\n      //   lrcT = []\n      // }\n    }\n\n    return {\n      lrc,\n      lrcT,\n    }\n  },\n  transformLrc(tags, lrclist) {\n    return `${tags.join('\\n')}\\n${lrclist ? lrclist.map(l => `[${l.time}]${l.text}\\n`).join('') : '暂无歌词'}`\n  },\n  parseLrc(lrc) {\n    const lines = lrc.split(/\\r\\n|\\r|\\n/)\n    let tags = []\n    let lrcArr = []\n    for (let i = 0; i < lines.length; i++) {\n      const line = lines[i].trim()\n      let result = timeExp.exec(line)\n      if (result) {\n        const text = line.replace(timeExp, '').trim()\n        let time = RegExp.$1\n        if (/\\.\\d\\d$/.test(time)) time += '0'\n        lrcArr.push({\n          time,\n          text,\n        })\n      } else if (lrcTools.rxps.tagLine.test(line)) {\n        tags.push(line)\n      }\n    }\n    const lrcInfo = this.sortLrcArr(lrcArr)\n    return {\n      lyric: decodeName(this.transformLrc(tags, lrcInfo.lrc)),\n      tlyric: lrcInfo.lrcT.length ? decodeName(this.transformLrc(tags, lrcInfo.lrcT)) : '',\n    }\n  },\n  // getLyric2(musicInfo, isGetLyricx = true) {\n  //   const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)\n  //   requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {\n  //     if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))\n  //     return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {\n  //       let lrcInfo\n  //       console.log(Buffer.from(base64Data, 'base64').toString())\n  //       try {\n  //         lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())\n  //       } catch {\n  //         return Promise.reject(new Error('Get lyric failed'))\n  //       }\n  //       if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')\n  //       lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)\n  //       // console.log(lrcInfo.lyric)\n  //       // console.log(lrcInfo.tlyric)\n  //       // console.log(lrcInfo.lxlyric)\n  //       // console.log(JSON.stringify(lrcInfo))\n  //     })\n  //   })\n  //   return requestObj\n  // },\n  getLyric(musicInfo, isGetLyricx = true) {\n    // this.getLyric2(musicInfo)\n    const requestObj = httpFetch(`http://newlyric.kuwo.cn/newlyric.lrc?${buildParams(musicInfo.songmid, isGetLyricx)}`)\n    requestObj.promise = requestObj.promise.then(({ statusCode, body, raw }) => {\n      if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))\n      return decodeLyric({ lrcBase64: raw.toString('base64'), isGetLyricx }).then(base64Data => {\n        // let lrcInfo\n        // try {\n        //   lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())\n        // } catch {\n        //   return Promise.reject(new Error('Get lyric failed'))\n        // }\n        let lrcInfo\n        // console.log(Buffer.from(base64Data, 'base64').toString())\n        try {\n          lrcInfo = this.parseLrc(Buffer.from(base64Data, 'base64').toString())\n        } catch (err) {\n          return Promise.reject(new Error('Get lyric failed'))\n        }\n        // console.log(lrcInfo)\n        if (lrcInfo.tlyric) lrcInfo.tlyric = lrcInfo.tlyric.replace(lrcTools.rxps.wordTimeAll, '')\n        try {\n          lrcInfo.lxlyric = lrcTools.parse(lrcInfo.lyric)\n        } catch {\n          lrcInfo.lxlyric = ''\n        }\n        lrcInfo.lyric = lrcInfo.lyric.replace(lrcTools.rxps.wordTimeAll, '')\n        if (!existTimeExp.test(lrcInfo.lyric)) return Promise.reject(new Error('Get lyric failed'))\n        // console.log(lrcInfo)\n        return lrcInfo\n      })\n    })\n    return requestObj\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/musicSearch.js",
    "content": "// import '../../polyfill/array.find'\n\nimport { httpFetch } from '../../request'\nimport { formatPlayTime, decodeName } from '../../index'\n// import { debug } from '../../utils/env'\nimport { formatSinger } from './util'\n\nexport default {\n  regExps: {\n    mInfo: /level:(\\w+),bitrate:(\\d+),format:(\\w+),size:([\\w.]+)/,\n  },\n  limit: 30,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  // cancelFn: null,\n  musicSearch(str, page, limit) {\n    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`)\n    return musicSearchRequestObj.promise\n  },\n  // getImg(songId) {\n  //   return httpGet(`http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_${songId}`)\n  // },\n  // getLrc(songId) {\n  //   return httpGet(`http://mobile.kuwo.cn/mpage/html5/songinfoandlrc?mid=${songId}&flag=0`)\n  // },\n  handleResult(rawData) {\n    const result = []\n    if (!rawData) return result\n    // console.log(rawData)\n    for (let i = 0; i < rawData.length; i++) {\n      const info = rawData[i]\n      let songId = info.MUSICRID.replace('MUSIC_', '')\n      // const format = (info.FORMATS || info.formats).split('|')\n\n      if (!info.N_MINFO) {\n        console.log('N_MINFO is undefined')\n        return null\n      }\n\n      const types = []\n      const _types = {}\n\n      let infoArr = info.N_MINFO.split(';')\n      for (let info of infoArr) {\n        info = info.match(this.regExps.mInfo)\n        if (info) {\n          switch (info[2]) {\n            case '4000':\n              types.push({ type: 'flac24bit', size: info[4] })\n              _types.flac24bit = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '2000':\n              types.push({ type: 'flac', size: info[4] })\n              _types.flac = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '320':\n              types.push({ type: '320k', size: info[4] })\n              _types['320k'] = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '128':\n              types.push({ type: '128k', size: info[4] })\n              _types['128k'] = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n          }\n        }\n      }\n      types.reverse()\n\n      let interval = parseInt(info.DURATION)\n\n      result.push({\n        name: decodeName(info.SONGNAME),\n        singer: formatSinger(decodeName(info.ARTIST)),\n        source: 'kw',\n        // img = (info.album.name === '' || info.album.name === '空')\n        //   ? `http://player.kuwo.cn/webmusic/sj/dtflagdate?flag=6&rid=MUSIC_160911.jpg`\n        //   : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${info.album.mid}.jpg`\n        songmid: songId,\n        albumId: decodeName(info.ALBUMID || ''),\n        interval: Number.isNaN(interval) ? 0 : formatPlayTime(interval),\n        albumName: info.ALBUM ? decodeName(info.ALBUM) : '',\n        lrc: null,\n        img: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    }\n    // console.log(result)\n    return result\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n    // http://newlyric.kuwo.cn/newlyric.lrc?62355680\n    return this.musicSearch(str, page, limit).then(({ body: result }) => {\n      // console.log(result)\n      if (!result || (result.TOTAL !== '0' && result.SHOW === '0')) return this.search(str, page, limit, ++retryNum)\n      let list = this.handleResult(result.abslist)\n\n      if (list == null) return this.search(str, page, limit, ++retryNum)\n\n      this.total = parseInt(result.TOTAL)\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return Promise.resolve({\n        list,\n        allPage: this.allPage,\n        total: this.total,\n        limit,\n        source: 'kw',\n      })\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/pic.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  getPic({ songmid }) {\n    const requestObj = httpFetch(`http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=${songmid}`)\n    requestObj.promise = requestObj.promise.then(({ body }) => /^http/.test(body) ? body : null)\n    return requestObj.promise\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, decodeName } from '../../index'\nimport { formatSinger, objStr2JSON } from './util'\nimport album from './album'\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_hotTags: null,\n  _requestObj_list: null,\n  limit_list: 36,\n  limit_song: 1000,\n  successCode: 200,\n  sortList: [\n    {\n      name: '最新',\n      id: 'new',\n    },\n    {\n      name: '最热',\n      id: 'hot',\n    },\n  ],\n  regExps: {\n    mInfo: /level:(\\w+),bitrate:(\\d+),format:(\\w+),size:([\\w.]+)/,\n    // http://www.kuwo.cn/playlist_detail/2886046289\n    // https://m.kuwo.cn/h5app/playlist/2736267853?t=qqfriend\n    listDetailLink: /^.+\\/playlist(?:_detail)?\\/(\\d+)(?:\\?.*|&.*$|#.*$|$)/,\n  },\n  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',\n  hotTagUrl: 'http://wapi.kuwo.cn/api/pc/classify/playlist/getRcmTagList?loginUid=0&loginSid=0&appUid=76039576',\n  getListUrl({ sortId, id, type, page }) {\n    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}`\n    switch (type) {\n      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}`\n      case '43': return `http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=${id}&prod=pc`\n    }\n    // http://wapi.kuwo.cn/api/pc/classify/playlist/getTagPlayList?loginUid=0&loginSid=0&appUid=76039576&id=173&pn=1&rn=100\n  },\n  getListDetailUrl(id, page) {\n    // 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\n    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`\n    // http://mobileinterfaces.kuwo.cn/er.s?type=get_pc_qz_data&f=web&id=140&prod=pc\n  },\n\n  // 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\n  // 获取标签\n  getTag(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.tagsUrl)\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getTag(++tryNum)\n      return this.filterTagInfo(body.data)\n    })\n  },\n  // 获取标签\n  getHotTag(tryNum = 0) {\n    if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_hotTags = httpFetch(this.hotTagUrl)\n    return this._requestObj_hotTags.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getHotTag(++tryNum)\n      return this.filterInfoHotTag(body.data[0].data)\n    })\n  },\n  filterInfoHotTag(rawList) {\n    return rawList.map(item => ({\n      id: `${item.id}-${item.digest}`,\n      name: item.name,\n      source: 'kw',\n    }))\n  },\n  filterTagInfo(rawList) {\n    return rawList.map(type => ({\n      name: type.name,\n      list: type.data.map(item => ({\n        parent_id: type.id,\n        parent_name: type.name,\n        id: `${item.id}-${item.digest}`,\n        name: item.name,\n        source: 'kw',\n      })),\n    }))\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    let id\n    let type\n    if (tagId) {\n      let arr = tagId.split('-')\n      id = arr[0]\n      type = arr[1]\n    } else {\n      id = null\n    }\n    this._requestObj_list = httpFetch(this.getListUrl({ sortId, id, type, page }))\n    return this._requestObj_list.promise.then(({ body }) => {\n      if (!id || type == '10000') {\n        if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)\n        return {\n          list: this.filterList(body.data.data),\n          total: body.data.total,\n          page: body.data.pn,\n          limit: body.data.rn,\n          source: 'kw',\n        }\n      } else if (!body.length) {\n        return this.getList(sortId, tagId, page, ++tryNum)\n      }\n      return {\n        list: this.filterList2(body),\n        total: 1000,\n        page,\n        limit: 1000,\n        source: 'kw',\n      }\n    })\n  },\n\n\n  /**\n   * 格式化播放数量\n   * @param {*} num\n   */\n  formatPlayCount(num) {\n    if (num > 100000000) return parseInt(num / 10000000) / 10 + '亿'\n    if (num > 10000) return parseInt(num / 1000) / 10 + '万'\n    return num\n  },\n  filterList(rawData) {\n    return rawData.map(item => ({\n      play_count: this.formatPlayCount(item.listencnt),\n      id: `digest-${item.digest}__${item.id}`,\n      author: item.uname,\n      name: item.name,\n      // time: item.publish_time,\n      total: item.total,\n      img: item.img,\n      grade: item.favorcnt / 10,\n      desc: item.desc,\n      source: 'kw',\n    }))\n  },\n  filterList2(rawData) {\n    // console.log(rawData)\n    const list = []\n    rawData.forEach(item => {\n      if (!item.label) return\n      list.push(...item.list.map(item => ({\n        play_count: item.play_count && this.formatPlayCount(item.listencnt),\n        id: `digest-${item.digest}__${item.id}`,\n        author: item.uname,\n        name: item.name,\n        total: item.total,\n        // time: item.publish_time,\n        img: item.img,\n        grade: item.favorcnt && item.favorcnt / 10,\n        desc: item.desc,\n        source: 'kw',\n      })))\n    })\n    return list\n  },\n\n  getListDetailDigest8(id, page, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const requestObj = httpFetch(this.getListDetailUrl(id, page))\n    return requestObj.promise.then(({ body }) => {\n      if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum)\n      return {\n        list: this.filterListDetail(body.musiclist),\n        page,\n        limit: body.rn,\n        total: body.total,\n        source: 'kw',\n        info: {\n          name: body.title,\n          img: body.pic,\n          desc: body.info,\n          author: body.uname,\n          play_count: this.formatPlayCount(body.playnum),\n        },\n      }\n    })\n  },\n  getListDetailDigest5Info(id, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    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`)\n    return requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode != 200 || !body.child) return this.getListDetail(id, ++tryNum)\n      // console.log(body)\n      return body.child.length ? body.child[0].sourceid : null\n    })\n  },\n  getListDetailDigest5Music(id, page, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    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`)\n    return requestObj.promise.then(({ body }) => {\n      // console.log(body)\n      if (body.result !== 'ok') return this.getListDetail(id, page, ++tryNum)\n      return {\n        list: this.filterListDetail(body.musiclist),\n        page,\n        limit: body.rn,\n        total: body.total,\n        source: 'kw',\n        info: {\n          name: body.title,\n          img: body.pic,\n          desc: body.info,\n          author: body.uname,\n          play_count: this.formatPlayCount(body.playnum),\n        },\n      }\n    })\n  },\n  async getListDetailDigest5(id, page, retryNum) {\n    const detailId = await this.getListDetailDigest5Info(id, retryNum)\n    return this.getListDetailDigest5Music(detailId, page, retryNum)\n  },\n\n  filterBDListDetail(rawList) {\n    return rawList.map(item => {\n      let types = []\n      let _types = {}\n      for (let info of item.audios) {\n        info.size = info.size?.toLocaleUpperCase()\n        switch (info.bitrate) {\n          case '4000':\n            types.push({ type: 'flac24bit', size: info.size })\n            _types.flac24bit = {\n              size: info.size,\n            }\n            break\n          case '2000':\n            types.push({ type: 'flac', size: info.size })\n            _types.flac = {\n              size: info.size,\n            }\n            break\n          case '320':\n            types.push({ type: '320k', size: info.size })\n            _types['320k'] = {\n              size: info.size,\n            }\n            break\n          case '128':\n            types.push({ type: '128k', size: info.size })\n            _types['128k'] = {\n              size: info.size,\n            }\n            break\n        }\n      }\n      types.reverse()\n\n      return {\n        singer: item.artists.map(s => s.name).join('、'),\n        name: item.name,\n        albumName: item.album,\n        albumId: item.albumId,\n        songmid: item.id,\n        source: 'kw',\n        interval: formatPlayTime(item.duration),\n        img: item.albumPic,\n        releaseDate: item.releaseDate,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  getReqId() {\n    function t() {\n      return (65536 * (1 + Math.random()) | 0).toString(16).substring(1)\n    }\n    return t() + t() + t() + t() + t() + t() + t() + t()\n  },\n  async getListDetailMusicListByBDListInfo(id, source) {\n    const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/service/playlist/info/${id}?reqId=${this.getReqId()}&source=${source}`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n        plat: 'h5',\n      },\n    }).promise.catch(() => ({ code: 0 }))\n\n    if (infoData.code != 200) return null\n\n    return {\n      name: infoData.data.name,\n      img: infoData.data.pic,\n      desc: infoData.data.description,\n      author: infoData.data.creatorName,\n      play_count: infoData.data.playNum,\n    }\n  },\n  async getListDetailMusicListByBDUserPub(id) {\n    const { body: infoData } = await httpFetch(`https://bd-api.kuwo.cn/api/ucenter/users/pub/${id}?reqId=${this.getReqId()}`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n        plat: 'h5',\n      },\n    }).promise.catch(() => ({ code: 0 }))\n\n    if (infoData.code != 200) return null\n\n    // console.log(infoData)\n    return {\n      name: infoData.data.userInfo.nickname + '喜欢的音乐',\n      img: infoData.data.userInfo.headImg,\n      desc: '',\n      author: infoData.data.userInfo.nickname,\n      play_count: '',\n    }\n  },\n  async getListDetailMusicListByBDList(id, source, page, tryNum = 0) {\n    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}`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n        plat: 'h5',\n      },\n    }).promise.catch(() => {\n      if (tryNum > 2) return Promise.reject(new Error('try max num'))\n      return this.getListDetailMusicListByBDList(id, source, page, ++tryNum)\n    })\n\n    if (listData.code !== 200) return Promise.reject(new Error('failed'))\n\n    return {\n      list: this.filterBDListDetail(listData.data.list),\n      page,\n      limit: listData.data.pageSize,\n      total: listData.data.total,\n      source: 'kw',\n    }\n  },\n  async getListDetailMusicListByBD(id, page) {\n    const uid = /uid=(\\d+)/.exec(id)?.[1]\n    const listId = /playlistId=(\\d+)/.exec(id)?.[1]\n    const source = /source=(\\d+)/.exec(id)?.[1]\n    if (!listId) return Promise.reject(new Error('failed'))\n\n    const task = [this.getListDetailMusicListByBDList(listId, source, page)]\n    switch (source) {\n      case '4':\n        task.push(this.getListDetailMusicListByBDListInfo(listId, source))\n        break\n      case '5':\n        task.push(this.getListDetailMusicListByBDUserPub(uid ?? listId))\n        break\n    }\n    const [listData, info] = await Promise.all(task)\n    listData.info = info ?? {\n      name: '',\n      img: '',\n      desc: '',\n      author: '',\n      play_count: '',\n    }\n    // console.log(listData)\n    return listData\n  },\n\n  // 获取歌曲列表内的音乐\n  getListDetail(id, page, retryNum = 0) {\n    // console.log(id)\n    // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=5&ownerId=000\n    // https://h5app.kuwo.cn/m/bodian/collection.html?uid=000&playlistId=000&source=4&ownerId=\n    if (/\\/bodian\\//.test(id)) return this.getListDetailMusicListByBD(id, page)\n    if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')\n    else if (/^digest-/.test(id)) {\n      let [digest, _id] = id.split('__')\n      digest = digest.replace('digest-', '')\n      id = _id\n      switch (digest) {\n        case '8':\n          break\n        case '13': return album.getAlbumListDetail(id, page, retryNum)\n        case '5':\n        default: return this.getListDetailDigest5(id, page, retryNum)\n      }\n    }\n    return this.getListDetailDigest8(id, page, retryNum)\n  },\n  filterListDetail(rawData) {\n    // console.log(rawData)\n    return rawData.map(item => {\n      let infoArr = item.N_MINFO.split(';')\n      let types = []\n      let _types = {}\n      for (let info of infoArr) {\n        info = info.match(this.regExps.mInfo)\n        if (info) {\n          switch (info[2]) {\n            case '4000':\n              types.push({ type: 'flac24bit', size: info[4] })\n              _types.flac24bit = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '2000':\n              types.push({ type: 'flac', size: info[4] })\n              _types.flac = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '320':\n              types.push({ type: '320k', size: info[4] })\n              _types['320k'] = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n            case '128':\n              types.push({ type: '128k', size: info[4] })\n              _types['128k'] = {\n                size: info[4].toLocaleUpperCase(),\n              }\n              break\n          }\n        }\n      }\n      types.reverse()\n\n      return {\n        singer: formatSinger(decodeName(item.artist)),\n        name: decodeName(item.name),\n        albumName: decodeName(item.album),\n        albumId: item.albumid,\n        songmid: item.id,\n        source: 'kw',\n        interval: formatPlayTime(parseInt(item.duration)),\n        img: null,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  getTags() {\n    return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'kw' }))\n  },\n  getDetailPageUrl(id) {\n    if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')\n    else if (/^digest-/.test(id)) {\n      let result = id.split('__')\n      id = result[1]\n    }\n    return `http://www.kuwo.cn/playlist_detail/${id}`\n  },\n\n  search(text, page, limit = 20) {\n    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`)\n      .promise.then(({ body }) => {\n        body = objStr2JSON(body)\n        // console.log(body)\n        return {\n          list: body.abslist.map(item => {\n            return {\n              play_count: this.formatPlayCount(item.playcnt),\n              id: String(item.playlistid),\n              author: decodeName(item.nickname),\n              name: decodeName(item.name),\n              total: item.songnum,\n              // time: item.publish_time,\n              img: item.pic,\n              desc: decodeName(item.intro),\n              source: 'kw',\n            }\n          }),\n          limit,\n          total: parseInt(body.TOTAL),\n          source: 'kw',\n        }\n      })\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/tipSearch.js",
    "content": "// import { decodeName } from '../../index'\n// import { tokenRequest } from './util'\nimport { httpFetch } from '../../request'\n\nexport default {\n  regExps: {\n    relWord: /RELWORD=(.+)/,\n  },\n  requestObj: null,\n  async tipSearchBySong(str) {\n    // 报错403，加了referer还是有问题（直接换一个\n    // this.requestObj = await tokenRequest(`http://www.kuwo.cn/api/www/search/searchKey?key=${encodeURIComponent(str)}`)\n\n    this.cancelTipSearch()\n    this.requestObj = httpFetch(`https://tips.kuwo.cn/t.s?corp=kuwo&newver=3&p2p=1&notrace=0&c=mbox&w=${encodeURIComponent(str)}&encoding=utf8&rformat=json`, {\n      Referer: 'http://www.kuwo.cn/',\n    })\n    return this.requestObj.promise.then(({ body, statusCode }) => {\n      if (statusCode != 200 || !body.WORDITEMS) return Promise.reject(new Error('请求失败'))\n      return body.WORDITEMS\n    })\n  },\n  handleResult(rawData) {\n    return rawData.map(item => item.RELWORD)\n  },\n  cancelTipSearch() {\n    if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()\n  },\n  async search(str) {\n    return this.tipSearchBySong(str).then(result => this.handleResult(result))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/kw/util.js",
    "content": "// import { httpGet, httpFetch } from '../../request'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\nimport { rendererInvoke } from '@common/rendererIpc'\nimport { createCipheriv, createDecipheriv } from 'crypto'\nimport { toMD5 } from '../utils'\n\n// const kw_token = {\n//   token: null,\n//   isGetingToken: false,\n// }\n\n// const translationMap = {\n//   \"{'\": '{\"',\n//   \"'}\\n\": '\"}',\n//   \"'}\": '\"}',\n//   \"':'\": '\":\"',\n//   \"','\": '\",\"',\n//   \"':{'\": '\":{\"',\n//   \"':['\": '\":[\"',\n//   \"'}],'\": '\"}],\"',\n//   \"':[{'\": '\":[{\"',\n//   \"'},'\": '\"},\"',\n//   \"'},{'\": '\"},{\"',\n//   \"':[],'\": '\":[],\"',\n//   \"':{},'\": '\":{},\"',\n//   \"'}]}\": '\"}]}',\n// }\n\n// export const objStr2JSON = str => {\n//   return JSON.parse(str.replace(/(^{'|'}\\n$|'}$|':'|','|':\\[{'|'}\\],'|':{'|'},'|'},{'|':\\['|':\\[\\],'|':{},'|'}]})/g, s => translationMap[s]))\n// }\n\nexport const objStr2JSON = str => {\n  return JSON.parse(str.replace(/('(?=(,\\s*')))|('(?=:))|((?<=([:,]\\s*))')|((?<={)')|('(?=}))/g, '\"'))\n}\n\n\nexport const formatSinger = rawData => rawData.replace(/&/g, '、')\n\nexport const matchToken = headers => {\n  try {\n    return headers['set-cookie'][0].match(/kw_token=(\\w+)/)[1]\n  } catch (err) {\n    return null\n  }\n}\n\n// const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))\n\n\n// export const getToken = (retryNum = 0) => new Promise((resolve, reject) => {\n//   if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n//   if (kw_token.isGetingToken) return wait(1000).then(() => getToken(retryNum).then(token => resolve(token)))\n//   if (kw_token.token) return resolve(kw_token.token)\n//   kw_token.isGetingToken = true\n//   httpGet('http://www.kuwo.cn/', (err, resp) => {\n//     kw_token.isGetingToken = false\n//     if (err) return getToken(++retryNum)\n//     if (resp.statusCode != 200) return reject(new Error('获取失败'))\n//     const token = kw_token.token = matchToken(resp.headers)\n//     resolve(token)\n//   })\n// })\n\nexport const decodeLyric = base64Data => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, base64Data)\n\n// export const tokenRequest = async(url, options = {}) => {\n//   let token = kw_token.token\n//   if (!token) token = await getToken()\n//   if (!options.headers) {\n//     options.headers = {\n//       Referer: 'http://www.kuwo.cn/',\n//       csrf: token,\n//       cookie: 'kw_token=' + token,\n//     }\n//   }\n//   const requestObj = httpFetch(url, options)\n//   requestObj.promise = requestObj.promise.then(resp => {\n//     // console.log(resp)\n//     if (resp.statusCode == 200) {\n//       kw_token.token = matchToken(resp.headers)\n//     }\n//     return resp\n//   })\n//   return requestObj\n// }\n\nexport const lrcTools = {\n  rxps: {\n    wordLine: /^(\\[\\d{1,2}:.*\\d{1,4}\\])\\s*(\\S+(?:\\s+\\S+)*)?\\s*/,\n    tagLine: /\\[(ver|ti|ar|al|offset|by|kuwo):\\s*(\\S+(?:\\s+\\S+)*)\\s*\\]/,\n    wordTimeAll: /<(-?\\d+),(-?\\d+)(?:,-?\\d+)?>/g,\n    wordTime: /<(-?\\d+),(-?\\d+)(?:,-?\\d+)?>/,\n  },\n  offset: 1,\n  offset2: 1,\n  isOK: false,\n  lines: [],\n  tags: [],\n  getWordInfo(str, str2, prevWord) {\n    const offset = parseInt(str)\n    const offset2 = parseInt(str2)\n    let startTime = Math.abs((offset + offset2) / (this.offset * 2))\n    let endTime = Math.abs((offset - offset2) / (this.offset2 * 2)) + startTime\n    if (prevWord) {\n      if (startTime < prevWord.endTime) {\n        prevWord.endTime = startTime\n        if (prevWord.startTime > prevWord.endTime) {\n          prevWord.startTime = prevWord.endTime\n        }\n\n        prevWord.newTimeStr = `<${prevWord.startTime},${prevWord.endTime - prevWord.startTime}>`\n        // console.log(prevWord)\n      }\n    }\n    return {\n      startTime,\n      endTime,\n      timeStr: `<${startTime},${endTime - startTime}>`,\n    }\n  },\n  parseLine(line) {\n    if (line.length < 6) return\n    let result = this.rxps.wordLine.exec(line)\n    if (result) {\n      const time = result[1]\n      let words = result[2]\n      if (words == null) {\n        words = ''\n      }\n      const wordTimes = words.match(this.rxps.wordTimeAll)\n      if (!wordTimes) return\n      // console.log(wordTimes)\n      let preTimeInfo\n      for (const timeStr of wordTimes) {\n        const result = this.rxps.wordTime.exec(timeStr)\n        const wordInfo = this.getWordInfo(result[1], result[2], preTimeInfo)\n        words = words.replace(timeStr, wordInfo.timeStr)\n        if (preTimeInfo?.newTimeStr) words = words.replace(preTimeInfo.timeStr, preTimeInfo.newTimeStr)\n        preTimeInfo = wordInfo\n      }\n      this.lines.push(time + words)\n      return\n    }\n    result = this.rxps.tagLine.exec(line)\n    if (!result) return\n    if (result[1] == 'kuwo') {\n      let content = result[2]\n      if (content != null && content.includes('][')) {\n        content = content.substring(0, content.indexOf(']['))\n      }\n      const valueOf = parseInt(content, 8)\n      this.offset = Math.trunc(valueOf / 10)\n      this.offset2 = Math.trunc(valueOf % 10)\n      if (this.offset == 0 || Number.isNaN(this.offset) || this.offset2 == 0 || Number.isNaN(this.offset2)) {\n        this.isOK = false\n      }\n    } else {\n      this.tags.push(line)\n    }\n  },\n  parse(lrc) {\n    // console.log(lrc)\n    const lines = lrc.split(/\\r\\n|\\r|\\n/)\n    const tools = Object.create(this)\n    tools.isOK = true\n    tools.offset = 1\n    tools.offset2 = 1\n    tools.lines = []\n    tools.tags = []\n\n    for (const line of lines) {\n      if (!tools.isOK) throw new Error('failed')\n      tools.parseLine(line)\n    }\n    if (!tools.lines.length) return ''\n    let lrcs = tools.lines.join('\\n')\n    if (tools.tags.length) lrcs = `${tools.tags.join('\\n')}\\n${lrcs}`\n    // console.log(lrcs)\n    return lrcs\n  },\n}\n\n\nconst createAesEncrypt = (buffer, mode, key, iv) => {\n  const cipher = createCipheriv(mode, key, iv)\n  return Buffer.concat([cipher.update(buffer), cipher.final()])\n}\n\nconst createAesDecrypt = (buffer, mode, key, iv) => {\n  const cipher = createDecipheriv(mode, key, iv)\n  return Buffer.concat([cipher.update(buffer), cipher.final()])\n}\n\nexport const wbdCrypto = {\n  aesMode: 'aes-128-ecb',\n  aesKey: Buffer.from([112, 87, 39, 61, 199, 250, 41, 191, 57, 68, 45, 114, 221, 94, 140, 228], 'binary'),\n  aesIv: '',\n  appId: 'y67sprxhhpws',\n  decodeData(base64Result) {\n    const data = Buffer.from(decodeURIComponent(base64Result), 'base64')\n    return JSON.parse(createAesDecrypt(data, this.aesMode, this.aesKey, this.aesIv).toString())\n  },\n  createSign(data, time) {\n    const str = `${this.appId}${data}${time}`\n    return toMD5(str).toUpperCase()\n  },\n  buildParam(jsonData) {\n    const data = Buffer.from(JSON.stringify(jsonData))\n    const time = Date.now()\n\n    const encodeData = createAesEncrypt(data, this.aesMode, this.aesKey, this.aesIv).toString('base64')\n    const sign = this.createSign(encodeData, time)\n\n    return `data=${encodeURIComponent(encodeData)}&time=${time}&appId=${this.appId}&sign=${sign}`\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/album.js",
    "content": "import { createHttpFetch } from './utils'\nimport { filterMusicInfoList } from './musicInfo'\nimport { formatPlayCount } from '../../index'\n\nexport default {\n  /**\n   * 通过AlbumId获取专辑\n   * @param {*} id\n   * @param {*} page\n   */\n  async getAlbumDetail(id, page = 1) {\n    const list = await createHttpFetch(`http://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/queryAlbumSong?albumId=${id}&pageNo=${page}`)\n    if (!list.songList) return Promise.reject(new Error('Get album list error.'))\n\n    const songList = filterMusicInfoList(list.songList)\n    const listInfo = await this.getAlbumInfo(id)\n\n    return {\n      list: songList || [],\n      page,\n      limit: listInfo.total,\n      total: listInfo.total,\n      source: 'mg',\n      info: {\n        name: listInfo.name,\n        img: listInfo.image,\n        desc: listInfo.desc,\n        author: listInfo.author,\n        play_count: listInfo.play_count,\n      },\n    }\n  },\n  /**\n   * 通过AlbumId获取专辑信息\n   * @param {*} id\n   * @param {*} page\n   */\n  async getAlbumInfo(id) {\n    const info = await createHttpFetch(`https://app.c.nf.migu.cn/MIGUM3.0/resource/album/v2.0?albumId=${id}`)\n    if (!info) return Promise.reject(new Error('Get album info error.'))\n\n    return {\n      name: info.title,\n      image: info.imgItems.length ? info.imgItems[0].img : null,\n      desc: info.summary,\n      author: info.singer,\n      play_count: formatPlayCount(info.opNumItem.playNum),\n      total: info.totalCount,\n    }\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_test = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/mg/${songInfo.copyrightId}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n}\n\nexport default api_test\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/comment.js",
    "content": "import { httpFetch } from '../../request'\nimport getSongId from './songId'\nimport { dateFormat2 } from '../../index'\n\nexport default {\n  _requestObj: null,\n  _requestObj2: null,\n  _requestObj3: null,\n  lastCommentIds: new Map(),\n  async getComment(musicInfo, page = 1, limit = 20) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (!musicInfo.songId) {\n      let id = await getSongId(musicInfo)\n      if (!id) throw new Error('获取评论失败')\n      musicInfo.songId = id\n    }\n    if (page === 1) this.lastCommentIds.clear()\n    const lastCommentId = this.lastCommentIds.get(String(page)) || ''\n    if (!lastCommentId && page > 1) throw new Error('获取评论失败')\n    // const _requestObj = httpFetch(`https://music.migu.cn/v3/api/comment/listComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, {\n    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}`, {\n      headers: {\n        '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',\n        // Referer: 'https://music.migu.cn',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    // console.log(body)\n    if (statusCode != 200 || body.code !== '000000') throw new Error('获取评论失败')\n    const total = parseInt(body.data.commentNums)\n    const list = this.filterComment(body.data.comments)\n    this.lastCommentIds.set(String(page + 1), list.length ? list[list.length - 1].id : '')\n    return { source: 'mg', comments: list, total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  async getHotComment(musicInfo, page = 1, limit = 20) {\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n\n    if (!musicInfo.songId) {\n      let id = await getSongId(musicInfo)\n      if (!id) throw new Error('获取评论失败')\n      musicInfo.songId = id\n    }\n\n    // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listTopComments?targetId=${musicInfo.songId}&pageSize=${limit}&pageNo=${page}`, {\n    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}`, {\n      headers: {\n        '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',\n        // Referer: 'https://music.migu.cn',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    // console.log(body)\n    if (statusCode != 200 || body.code !== '000000') throw new Error('获取热门评论失败')\n    const total = parseInt(body.data.cfgHotCount)\n    return { source: 'mg', comments: this.filterComment(body.data.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  async getReplyComment(musicInfo, replyId, page = 1, limit = 10) {\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n\n    // const _requestObj2 = httpFetch(`https://music.migu.cn/v3/api/comment/listCommentsById?commentId=${replyId}&pageSize=${limit}&pageNo=${page}`, {\n    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}`, {\n      headers: {\n        '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',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    // console.log(body)\n    if (statusCode != 200 || body.code !== '000000') throw new Error('获取回复评论失败')\n    const total = parseInt(body.data.replyTotalCount)\n    return { source: 'mg', comments: this.filterComment(body.data.mainCommentItem.replyComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  filterComment(rawList) {\n    return rawList.map(item => ({\n      id: item.commentId,\n      text: item.commentInfo,\n      time: item.commentTime,\n      timeStr: dateFormat2(new Date(item.commentTime).getTime()),\n      userName: item.user.nickName,\n      avatar: item.user.middleIcon || item.user.bigIcon || item.user.smallIcon,\n      userId: item.user.userId,\n      likedCount: item.opNumItem.thumbNum,\n      replyNum: item.replyTotalCount,\n      reply: item.replyComments.map(c => ({\n        id: c.replyId,\n        text: c.replyInfo,\n        time: c.replyTime,\n        timeStr: dateFormat2(new Date(c.replyTime).getTime()),\n        userName: c.user.nickName,\n        avatar: c.user.middleIcon || c.user.bigIcon || c.user.smallIcon,\n        userId: c.user.userId,\n        likedCount: null,\n        replyNum: null,\n      })),\n    }))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/hotSearch.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const _requestObj = httpFetch('http://jadeite.migu.cn:7090/music_search/v3/search/hotword')\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.code !== '000000') throw new Error('获取热搜词失败')\n    // console.log(body, statusCode)\n    return { source: 'mg', list: this.filterList(body.data.hotwords[0].hotwordList) }\n  },\n  filterList(rawList) {\n    return rawList.filter(item => item.resourceType == 'song').map(item => item.word)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/index.js",
    "content": "import { apis } from '../api-source'\nimport leaderboard from './leaderboard'\nimport songList from './songList'\nimport musicSearch from './musicSearch'\nimport pic from './pic'\nimport lyric from './lyric'\nimport hotSearch from './hotSearch'\nimport comment from './comment'\n// import tipSearch from './tipSearch'\n\nconst mg = {\n  // tipSearch,\n  songList,\n  musicSearch,\n  leaderboard,\n  hotSearch,\n  comment,\n  getMusicUrl(songInfo, type) {\n    return apis('mg').getMusicUrl(songInfo, type)\n  },\n  getLyric(songInfo) {\n    return lyric.getLyric(songInfo)\n  },\n  getPic(songInfo) {\n    return pic.getPic(songInfo)\n  },\n  getMusicDetailPageUrl(songInfo) {\n    return `http://music.migu.cn/v3/music/song/${songInfo.copyrightId}`\n  },\n}\n\nexport default mg\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/leaderboard.js",
    "content": "import { httpFetch } from '../../request'\nimport { filterMusicInfoList } from './musicInfo'\n\n// 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' }]\n\n// const boardList = [\n//   { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' },\n//   { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' },\n//   { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' },\n//   { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' },\n//   { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' },\n//   { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' },\n//   { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' },\n//   { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' },\n//   { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' },\n//   { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' },\n//   // { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' },\n//   // { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' },\n//   // { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' },\n//   // { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' },\n//   // { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' },\n// ]\nconst boardList = [\n  {\n    id: 'mg__27553319',\n    name: '新歌榜',\n    bangid: '27553319',\n    source: 'mg',\n  },\n  {\n    id: 'mg__27186466',\n    name: '热歌榜',\n    bangid: '27186466',\n    source: 'mg',\n  },\n  {\n    id: 'mg__27553408',\n    name: '原创榜',\n    bangid: '27553408',\n    source: 'mg',\n  },\n  {\n    id: 'mg__75959118',\n    name: '音乐风向榜',\n    bangid: '75959118',\n    source: 'mg',\n  },\n  {\n    id: 'mg__76557036',\n    name: '彩铃分贝榜',\n    bangid: '76557036',\n    source: 'mg',\n  },\n  {\n    id: 'mg__76557745',\n    name: '会员臻爱榜',\n    bangid: '76557745',\n    source: 'mg',\n  },\n  {\n    id: 'mg__23189800',\n    name: '港台榜',\n    bangid: '23189800',\n    source: 'mg',\n  },\n  {\n    id: 'mg__23189399',\n    name: '内地榜',\n    bangid: '23189399',\n    source: 'mg',\n  },\n  {\n    id: 'mg__19190036',\n    name: '欧美榜',\n    bangid: '19190036',\n    source: 'mg',\n  },\n  {\n    id: 'mg__83176390',\n    name: '国风金曲榜',\n    bangid: '83176390',\n    source: 'mg',\n  },\n]\nexport default {\n  limit: 200,\n  list: [\n    {\n      id: 'mgyyb',\n      name: '音乐榜',\n      bangid: '27553319',\n    },\n    {\n      id: 'mgysb',\n      name: '影视榜',\n      bangid: '23603721',\n    },\n    {\n      id: 'mghybnd',\n      name: '华语内地榜',\n      bangid: '23603926',\n    },\n    {\n      id: 'mghyjqbgt',\n      name: '华语港台榜',\n      bangid: '23603954',\n    },\n    {\n      id: 'mgomb',\n      name: '欧美榜',\n      bangid: '23603974',\n    },\n    {\n      id: 'mgrhb',\n      name: '日韩榜',\n      bangid: '23603982',\n    },\n    {\n      id: 'mgwlb',\n      name: '网络榜',\n      bangid: '23604058',\n    },\n    {\n      id: 'mgclb',\n      name: '彩铃榜',\n      bangid: '23604023',\n    },\n    {\n      id: 'mgktvb',\n      name: 'KTV榜',\n      bangid: '23604040',\n    },\n    {\n      id: 'mgrcb',\n      name: '原创榜',\n      bangid: '23604032',\n    },\n  ],\n  getUrl(id, page) {\n    return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0`\n    // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}`\n  },\n  successCode: '000000',\n  requestBoardsObj: null,\n  getBoardsData() {\n    if (this.requestBoardsObj) this._requestBoardsObj.cancelHttp()\n    this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/pc/bmw/rank/rank-index/v1.0', {\n    // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', {\n    // this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {\n      headers: {\n        Referer: 'https://app.c.nf.migu.cn/',\n        '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',\n        channel: '0146921',\n      },\n    })\n    return this.requestBoardsObj.promise\n  },\n  getData(url) {\n    const requestObj = httpFetch(url)\n    return requestObj.promise\n  },\n  // filterBoardsData(listData, list = [], ids = new Set()) {\n  //   for (const item of listData) {\n  //     if (item.rankId && !ids.has(item.rankId)) {\n  //       ids.add(item.rankId)\n  //       list.push({\n  //         id: 'mg__' + item.rankId,\n  //         name: item.rankName,\n  //         bangid: String(item.rankId),\n  //         source: 'mg',\n  //       })\n  //     } else if (item.contents) this.filterBoardsData(item.contents, list, ids)\n  //   }\n  //   return list\n  // },\n  // filterBoardsData(rawList) {\n  //   // console.log(rawList)\n  //   let list = []\n  //   for (const board of rawList) {\n  //     if (board.template != 'group1') continue\n  //     for (const item of board.itemList) {\n  //       if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue\n\n  //       let data = item.displayLogId.param\n  //       list.push({\n  //         id: 'mg__' + data.rankId,\n  //         name: data.rankName,\n  //         bangid: String(data.rankId),\n  //       })\n  //     }\n  //   }\n  //   return list\n  // },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // // console.log(response.body.data.contentItemList)\n    // if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.data.contents)\n    // console.log(list)\n    // // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'mg',\n    // }\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'mg',\n    }\n  },\n  getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => {\n      // console.log(body)\n      if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum)\n      const list = filterMusicInfoList(body.columnInfo.contents.map(m => m.objectInfo))\n      return {\n        total: list.length,\n        list,\n        limit: this.limit,\n        page,\n        source: 'mg',\n      }\n    })\n  },\n\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') id = id.replace('mg__', '')\n    for (const item of boardList) {\n      if (item.bangid == id) {\n        return `https://music.migu.cn/v3/music/top/${item.webId}`\n      }\n    }\n    return null\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { getMusicInfo } from './musicInfo'\nimport { decrypt } from './utils/mrc'\n\nconst mrcTools = {\n  rxps: {\n    lineTime: /^\\s*\\[(\\d+),\\d+\\]/,\n    wordTime: /\\(\\d+,\\d+\\)/,\n    wordTimeAll: /(\\(\\d+,\\d+\\))/g,\n  },\n  parseLyric(str) {\n    str = str.replace(/\\r/g, '')\n    const lines = str.split('\\n')\n    const lxlrcLines = []\n    const lrcLines = []\n\n    for (const line of lines) {\n      if (line.length < 6) continue\n      let result = this.rxps.lineTime.exec(line)\n      if (!result) continue\n\n      const startTime = parseInt(result[1])\n      let time = startTime\n      let ms = time % 1000\n      time /= 1000\n      let m = parseInt(time / 60).toString().padStart(2, '0')\n      time %= 60\n      let s = parseInt(time).toString().padStart(2, '0')\n      time = `${m}:${s}.${ms}`\n\n      let words = line.replace(this.rxps.lineTime, '')\n\n      lrcLines.push(`[${time}]${words.replace(this.rxps.wordTimeAll, '')}`)\n\n      let times = words.match(this.rxps.wordTimeAll)\n      if (!times) continue\n      times = times.map(time => {\n        const result = /\\((\\d+),(\\d+)\\)/.exec(time)\n        return `<${parseInt(result[1]) - startTime},${result[2]}>`\n      })\n      const wordArr = words.split(this.rxps.wordTime)\n      const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')\n      lxlrcLines.push(`[${time}]${newWords}`)\n    }\n    return {\n      lyric: lrcLines.join('\\n'),\n      lxlyric: lxlrcLines.join('\\n'),\n    }\n  },\n  getText(url, tryNum = 0) {\n    const requestObj = httpFetch(url, {\n      headers: {\n        Referer: 'https://app.c.nf.migu.cn/',\n        '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',\n        channel: '0146921',\n      },\n    })\n    return requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 200) return body\n      if (tryNum > 5 || statusCode == 404) return Promise.reject(new Error('歌词获取失败'))\n      return this.getText(url, ++tryNum)\n    })\n  },\n  getMrc(url) {\n    return this.getText(url).then(text => {\n      return this.parseLyric(decrypt(text))\n    })\n  },\n  getLrc(url) {\n    return this.getText(url).then(text => ({ lxlyric: '', lyric: text }))\n  },\n  getTrc(url) {\n    if (!url) return Promise.resolve('')\n    return this.getText(url)\n  },\n  async getMusicInfo(songInfo) {\n    return songInfo.mrcUrl == null\n      ? getMusicInfo(songInfo.copyrightId)\n      : songInfo\n  },\n  getLyric(songInfo) {\n    return {\n      promise: this.getMusicInfo(songInfo).then(info => {\n        let p\n        if (info.mrcUrl) p = this.getMrc(info.mrcUrl)\n        else if (info.lrcUrl) p = this.getLrc(info.lrcUrl)\n        if (p == null) return Promise.reject(new Error('获取歌词失败'))\n        return Promise.all([p, this.getTrc(info.trcUrl)]).then(([lrcInfo, tlyric]) => {\n          lrcInfo.tlyric = tlyric\n          return lrcInfo\n        })\n      }),\n      cancelHttp() {},\n    }\n  },\n}\n\nexport default {\n  getLyric(songInfo) {\n    let requestObj = mrcTools.getLyric(songInfo)\n    return requestObj\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/musicInfo.js",
    "content": "import { sizeFormate, formatPlayTime } from '../../index'\nimport { createHttpFetch } from './utils'\nimport { formatSingerName } from '../utils'\n\nconst createGetMusicInfosTask = (ids) => {\n  let list = ids\n  let tasks = []\n  while (list.length) {\n    tasks.push(list.slice(0, 100))\n    if (list.length < 100) break\n    list = list.slice(100)\n  }\n  let url = 'https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?resourceType=2'\n  return Promise.all(tasks.map(task => createHttpFetch(url, {\n    method: 'POST',\n    form: {\n      resourceId: task.join('|'),\n    },\n  }).then(data => data.resource)))\n}\n\nexport const filterMusicInfoList = (rawList) => {\n  // console.log(rawList)\n  let ids = new Set()\n  const list = []\n  rawList.forEach(item => {\n    if (!item.songId || ids.has(item.songId)) return\n    ids.add(item.songId)\n    const types = []\n    const _types = {}\n    item.newRateFormats?.forEach(type => {\n      let size\n      switch (type.formatType) {\n        case 'PQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: '128k', size })\n          _types['128k'] = {\n            size,\n          }\n          break\n        case 'HQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: '320k', size })\n          _types['320k'] = {\n            size,\n          }\n          break\n        case 'SQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: 'flac', size })\n          _types.flac = {\n            size,\n          }\n          break\n        case 'ZQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: 'flac24bit', size })\n          _types.flac24bit = {\n            size,\n          }\n          break\n      }\n    })\n\n    const intervalTest = /(\\d\\d:\\d\\d)$/.test(item.length)\n\n    list.push({\n      singer: formatSingerName(item.artists, 'name'),\n      name: item.songName,\n      albumName: item.album,\n      albumId: item.albumId,\n      songmid: item.songId,\n      copyrightId: item.copyrightId,\n      source: 'mg',\n      interval: intervalTest ? RegExp.$1 : null,\n      img: item.albumImgs?.length ? item.albumImgs[0].img : null,\n      lrc: null,\n      lrcUrl: item.lrcUrl,\n      mrcUrl: item.mrcUrl,\n      trcUrl: item.trcUrl,\n      otherSource: null,\n      types,\n      _types,\n      typeUrl: {},\n    })\n  })\n  return list\n}\n\nexport const filterMusicInfoListV5 = (rawList) => {\n  // console.log(rawList)\n  let ids = new Set()\n  const list = []\n  rawList.forEach(item => {\n    if (!item.songId || ids.has(item.songId)) return\n    ids.add(item.songId)\n    const types = []\n    const _types = {}\n    item.audioFormats?.forEach(type => {\n      let size\n      switch (type.formatType) {\n        case 'PQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: '128k', size })\n          _types['128k'] = {\n            size,\n          }\n          break\n        case 'HQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: '320k', size })\n          _types['320k'] = {\n            size,\n          }\n          break\n        case 'SQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: 'flac', size })\n          _types.flac = {\n            size,\n          }\n          break\n        case 'ZQ':\n          size = sizeFormate(type.size ?? type.androidSize)\n          types.push({ type: 'flac24bit', size })\n          _types.flac24bit = {\n            size,\n          }\n          break\n      }\n    })\n\n    list.push({\n      singer: formatSingerName(item.singerList, 'name'),\n      name: item.songName,\n      albumName: item.album,\n      albumId: item.albumId,\n      songmid: item.songId,\n      copyrightId: item.copyrightId,\n      source: 'mg',\n      interval: formatPlayTime(item.duration),\n      img: item.img3 || item.img2 || item.img1 || null,\n      lrc: null,\n      lrcUrl: item.lrcUrl,\n      mrcUrl: item.mrcUrl,\n      trcUrl: item.trcUrl,\n      otherSource: null,\n      types,\n      _types,\n      typeUrl: {},\n    })\n  })\n  return list\n}\n\nexport const getMusicInfo = async(copyrightId) => {\n  return getMusicInfos([copyrightId]).then(data => data[0])\n}\n\nexport const getMusicInfos = async(copyrightIds) => {\n  return filterMusicInfoList(await Promise.all(createGetMusicInfosTask(copyrightIds)).then(data => data.flat()))\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/musicSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { sizeFormate, formatPlayTime } from '../../index'\nimport { toMD5, formatSingerName } from '../utils'\n\nexport const createSignature = (time, str) => {\n  const deviceId = '963B7AA0D21511ED807EE5846EC87D20'\n  const signatureMd5 = '6cdc72a439cef99a3418d2a78aa28c73'\n  const sign = toMD5(`${str}${signatureMd5}yyapp2d16148780a1dcc7408e06336b98cfd50${deviceId}${time}`)\n  return { sign, deviceId }\n}\n\nexport default {\n  limit: 20,\n  total: 0,\n  page: 0,\n  allPage: 1,\n\n  // 旧版API\n  // musicSearch(str, page, limit) {\n  //   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`, {\n  // 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`, {\n  // 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`, {\n  // 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)}`)\n  //   // 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`, {\n  //     headers: {\n  //       // sign: 'c3b7ae985e2206e97f1b2de8f88691e2',\n  //       // timestamp: 1578225871982,\n  //       // appId: 'yyapp2',\n  //       // mode: 'android',\n  //       // ua: 'Android_migu',\n  //       // version: '6.9.4',\n  //       osVersion: 'android 7.0',\n  //       'User-Agent': 'okhttp/3.9.1',\n  //     },\n  //   })\n  //   // 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)}`)\n  //   return searchRequest.promise.then(({ body }) => body)\n  // },\n  // handleResult(rawData) {\n  //   // console.log(rawData)\n  //   let ids = new Set()\n  //   const list = []\n  //   rawData.forEach(item => {\n  //     if (ids.has(item.id)) return\n  //     ids.add(item.id)\n  //     const types = []\n  //     const _types = {}\n  //     item.newRateFormats && item.newRateFormats.forEach(type => {\n  //       let size\n  //       switch (type.formatType) {\n  //         case 'PQ':\n  //           size = sizeFormate(type.size ?? type.androidSize)\n  //           types.push({ type: '128k', size })\n  //           _types['128k'] = {\n  //             size,\n  //           }\n  //           break\n  //         case 'HQ':\n  //           size = sizeFormate(type.size ?? type.androidSize)\n  //           types.push({ type: '320k', size })\n  //           _types['320k'] = {\n  //             size,\n  //           }\n  //           break\n  //         case 'SQ':\n  //           size = sizeFormate(type.size ?? type.androidSize)\n  //           types.push({ type: 'flac', size })\n  //           _types.flac = {\n  //             size,\n  //           }\n  //           break\n  //         case 'ZQ':\n  //           size = sizeFormate(type.size ?? type.androidSize)\n  //           types.push({ type: 'flac24bit', size })\n  //           _types.flac24bit = {\n  //             size,\n  //           }\n  //           break\n  //       }\n  //     })\n\n  //     const albumNInfo = item.albums && item.albums.length\n  //       ? {\n  //           id: item.albums[0].id,\n  //           name: item.albums[0].name,\n  //         }\n  //       : {}\n\n  //     list.push({\n  //       singer: this.getSinger(item.singers),\n  //       name: item.name,\n  //       albumName: albumNInfo.name,\n  //       albumId: albumNInfo.id,\n  //       songmid: item.songId,\n  //       copyrightId: item.copyrightId,\n  //       source: 'mg',\n  //       interval: null,\n  //       img: item.imgItems && item.imgItems.length ? item.imgItems[0].img : null,\n  //       lrc: null,\n  //       lrcUrl: item.lyricUrl,\n  //       mrcUrl: item.mrcurl,\n  //       trcUrl: item.trcUrl,\n  //       otherSource: null,\n  //       types,\n  //       _types,\n  //       typeUrl: {},\n  //     })\n  //   })\n  //   return list\n  // },\n\n  musicSearch(str, page, limit) {\n    const time = Date.now().toString()\n    const signData = createSignature(time, str)\n    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`, {\n      headers: {\n        uiVersion: 'A_music_3.6.1',\n        deviceId: signData.deviceId,\n        timestamp: time,\n        sign: signData.sign,\n        channel: '0146921',\n        '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',\n      },\n    })\n    return searchRequest.promise.then(({ body }) => body)\n  },\n  filterData(rawData) {\n    // console.log(rawData)\n    const list = []\n    const ids = new Set()\n\n    rawData.forEach(item => {\n      item.forEach(data => {\n        if (!data.songId || !data.copyrightId || ids.has(data.copyrightId)) return\n        ids.add(data.copyrightId)\n\n        const types = []\n        const _types = {}\n        data.audioFormats && data.audioFormats.forEach(type => {\n          let size\n          switch (type.formatType) {\n            case 'PQ':\n              size = sizeFormate(type.asize ?? type.isize)\n              types.push({ type: '128k', size })\n              _types['128k'] = {\n                size,\n              }\n              break\n            case 'HQ':\n              size = sizeFormate(type.asize ?? type.isize)\n              types.push({ type: '320k', size })\n              _types['320k'] = {\n                size,\n              }\n              break\n            case 'SQ':\n              size = sizeFormate(type.asize ?? type.isize)\n              types.push({ type: 'flac', size })\n              _types.flac = {\n                size,\n              }\n              break\n            case 'ZQ24':\n              size = sizeFormate(type.asize ?? type.isize)\n              types.push({ type: 'flac24bit', size })\n              _types.flac24bit = {\n                size,\n              }\n              break\n          }\n        })\n\n        let img = data.img3 || data.img2 || data.img1 || null\n        if (img && !/https?:/.test(data.img3)) img = 'http://d.musicapp.migu.cn' + img\n\n        list.push({\n          singer: formatSingerName(data.singerList),\n          name: data.name,\n          albumName: data.album,\n          albumId: data.albumId,\n          songmid: data.songId,\n          copyrightId: data.copyrightId,\n          source: 'mg',\n          interval: formatPlayTime(data.duration),\n          img,\n          lrc: null,\n          lrcUrl: data.lrcUrl,\n          mrcUrl: data.mrcurl,\n          trcUrl: data.trcUrl,\n          types,\n          _types,\n          typeUrl: {},\n        })\n      })\n    })\n    return list\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n    // http://newlyric.kuwo.cn/newlyric.lrc?62355680\n    return this.musicSearch(str, page, limit).then(result => {\n      // console.log(result)\n      if (!result || result.code !== '000000') return Promise.reject(new Error(result ? result.info : '搜索失败'))\n      const songResultData = result.songResultData || { resultList: [], totalCount: 0 }\n\n      let list = this.filterData(songResultData.resultList)\n      if (list == null) return this.search(str, page, limit, retryNum)\n\n      this.total = parseInt(songResultData.totalCount)\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return {\n        list,\n        allPage: this.allPage,\n        limit,\n        total: this.total,\n        source: 'mg',\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/pic.js",
    "content": "import { httpFetch } from '../../request'\nimport getSongId from './songId'\n\nexport default {\n  async getPicUrl(songId, tryNum = 0) {\n    let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songId}`, {\n      headers: {\n        Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu',\n      },\n    })\n    requestObj.promise.then(({ body }) => {\n      if (body.returnCode !== '000000') {\n        if (tryNum > 5) return Promise.reject(new Error('图片获取失败'))\n        let tryRequestObj = this.getPic(songId, ++tryNum)\n        requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj)\n        return tryRequestObj.promise\n      }\n      let url = body.largePic || body.mediumPic || body.smallPic\n      if (!/https?:/.test(url)) url = 'http:' + url\n      return url\n    })\n    return requestObj\n  },\n  async getPic(songInfo) {\n    const songId = await getSongId(songInfo)\n    return this.getPicUrl(songId)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/songId.js",
    "content": "// import { httpFetch } from '../../request'\nimport { getMusicInfo } from './musicInfo'\n\nconst getSongId = async(mInfo) => {\n  if (mInfo.songmid != mInfo.copyrightId) return mInfo.songmid\n  const musicInfo = await getMusicInfo(mInfo.copyrightId)\n  return musicInfo.songmid\n}\n\n\n// export const getSongId = async(musicInfo, retry = 0) => {\n//   if (musicInfo.songmid != musicInfo.copyrightId) return musicInfo.songmid\n//   if (++retry > 2) return Promise.reject(new Error('max retry'))\n\n//   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`, {\n//     headers: {\n//       'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n//       channel: '0146921',\n//     },\n//   })\n\n//   return requestObj.promise.then(({ body }) => {\n//     console.log(body)\n//     if (!body || body.code !== '000000') return this.getSongId(musicInfo, retry)\n//     const id = body.data.songItem.songId\n//     if (!id) throw new Error('failed')\n//     return id\n//   })\n// }\n\nexport default getSongId\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayCount } from '../../index'\nimport { filterMusicInfoListV5 } from './musicInfo'\nimport { createSignature } from './musicSearch'\nimport { createHttpFetch } from './utils/index'\n\n// 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: [] } }\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_list: null,\n  limit_list: 30,\n  limit_song: 50,\n  successCode: '000000',\n  cachedDetailInfo: {},\n  cachedUrl: {},\n  sortList: [\n    {\n      name: '推荐',\n      id: '15127315',\n      // id: '1',\n    },\n    // {\n    //   name: '最新',\n    //   id: '15127272',\n    //   // id: '2',\n    // },\n  ],\n  regExps: {\n    list: /<li><div class=\"thumb\">.+?<\\/li>/g,\n    listInfo: /.+data-original=\"(.+?)\".*data-id=\"(\\d+)\".*<div class=\"song-list-name\"><a\\s.*?>(.+?)<\\/a>.+<i class=\"iconfont cf-bofangliang\"><\\/i>(.+?)<\\/div>/,\n\n    // https://music.migu.cn/v3/music/playlist/161044573?page=1\n    listDetailLink: /^.+\\/playlist\\/(\\d+)(?:\\?.*|&.*$|#.*$|$)/,\n  },\n  tagsUrl: 'https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-taglist/release',\n  // tagsUrl: 'https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/indexTagPage.do?needAll=0',\n  getSongListUrl(sortId, tagId, page) {\n    // if (tagId == null) {\n    //   return sortId == 'recommend'\n    //     ? `https://music.migu.cn/v3/music/playlist?page=${page}&from=migu`\n    //     : `https://music.migu.cn/v3/music/playlist?sort=${sortId}&page=${page}&from=migu`\n    // }\n    // return `https://music.migu.cn/v3/music/playlist?tagId=${tagId}&page=${page}&from=migu`\n    if (!tagId) {\n      // return `https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=1`\n      // return `https://c.musicapp.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=${this.limit_list}&start=${page}&templateVersion=5&type=${sortId}`\n      // https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/getMusicData.do?count=50&start=2&templateVersion=5&type=1\n      // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&columnId=${sortId}&startIndex=${(page - 1) * 10}`\n      return `https://app.c.nf.migu.cn/pc/bmw/page-data/playlist-square-recommend/v1.0?templateVersion=2&pageNo=${page}`\n    }\n    // 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`\n    return `https://app.c.nf.migu.cn/pc/v1.0/template/musiclistplaza-listbytag/release?pageNumber=${page}&templateVersion=2&tagId=${tagId}`\n    // return `https://m.music.migu.cn/migu/remoting/playlist_bycolumnid_tag?playListType=2&type=1&tagId=${tagId}&startIndex=${(page - 1) * 10}`\n  },\n  getSongListDetailUrl(id, page) {\n    return `https://app.c.nf.migu.cn/MIGUM3.0/resource/playlist/song/v2.0?pageNo=${page}&pageSize=${this.limit_song}&playlistId=${id}`\n    // return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/user/queryMusicListSongs.do?musicListId=${id}&pageNo=${page}&pageSize=${this.limit_song}`\n  },\n  defaultHeaders: {\n    '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',\n    Referer: 'https://m.music.migu.cn/',\n    // language: 'Chinese',\n    // ua: 'Android_migu',\n    // mode: 'android',\n    // version: '6.8.5',\n  },\n\n  getListDetailList(id, page, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921\n\n    // if (/playlist\\/index\\.html\\?/.test(id)) {\n    //   id = id.replace(/.*(?:\\?|&)id=(\\d+)(?:&.*|$)/, '$1')\n    // } else if ((/[?&:/]/.test(id))) id = id.replace(this.regExps.listDetailLink, '$1')\n\n    const requestObj_listDetail = httpFetch(this.getSongListDetailUrl(id, page), { headers: this.defaultHeaders })\n    return requestObj_listDetail.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getListDetailList(id, page, ++tryNum)\n      // console.log(JSON.stringify(body))\n      // console.log(body)\n      return {\n        list: filterMusicInfoListV5(body.data.songList),\n        page,\n        limit: this.limit_song,\n        total: body.data.totalCount,\n        source: 'mg',\n      }\n    })\n  },\n\n  getListDetailInfo(id, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n\n    if (this.cachedDetailInfo[id]) return Promise.resolve(this.cachedDetailInfo[id])\n    const requestObj_listDetailInfo = httpFetch(`https://c.musicapp.migu.cn/MIGUM3.0/resource/playlist/v2.0?playlistId=${id}`, {\n      headers: this.defaultHeaders,\n    })\n    return requestObj_listDetailInfo.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)\n      // console.log(JSON.stringify(body))\n      // console.log(body)\n      const cachedDetailInfo = this.cachedDetailInfo[id] = {\n        name: body.data.title,\n        img: body.data.imgItem.img,\n        desc: body.data.summary,\n        author: body.data.ownerName,\n        play_count: formatPlayCount(body.data.opNumItem.playNum),\n      }\n      return cachedDetailInfo\n    })\n  },\n\n  async getDetailUrl(link, page, retryNum = 0) {\n    if (retryNum > 3) return Promise.reject(new Error('link try max num'))\n\n    const requestObj_listDetailLink = httpFetch(link, {\n      headers: {\n        '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',\n        Referer: link,\n      },\n    })\n    const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(body, location)\n    if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)\n    if (location) {\n      this.cachedUrl[link] = location\n      return this.getListDetail(location, page)\n    }\n    return Promise.reject(new Error('link get failed'))\n  },\n\n  getListDetail(id, page, retryNum = 0) { // 获取歌曲列表内的音乐\n    // https://h5.nf.migu.cn/app/v4/p/share/playlist/index.html?id=184187437&channel=0146921\n    // http://c.migu.cn/00bTY6?ifrom=babddaadfde4ebeda289d671ab62f236\n    // https://music.migu.cn/v5/#/playlist?playlistId=221573417\n    if (/\\/playlist[/?]/.test(id)) {\n      id = /(?:playlistId|id)=(\\d+)/.exec(id)?.[1]\n      if (!id) throw new Error('list detail id parse failed')\n    } else if (this.regExps.listDetailLink.test(id)) {\n      id = id.replace(this.regExps.listDetailLink, '$1')\n    } else if ((/[?&:/]/.test(id))) {\n      const url = this.cachedUrl[id]\n      return url ? this.getListDetail(url, page) : this.getDetailUrl(id, page)\n    }\n\n    return Promise.all([\n      this.getListDetailList(id, page, retryNum),\n      this.getListDetailInfo(id, retryNum),\n    ]).then(([listData, info]) => {\n      listData.info = info\n      return listData\n    })\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_list = httpFetch(this.getSongListUrl(sortId, tagId, page), {\n      headers: this.defaultHeaders,\n      // headers: {\n      //   sign: 'c3b7ae985e2206e97f1b2de8f88691e2',\n      //   timestamp: 1578225871982,\n      //   appId: 'yyapp2',\n      //   mode: 'android',\n      //   ua: 'Android_migu',\n      //   version: '6.9.4',\n      //   osVersion: 'android 7.0',\n      //   'User-Agent': 'okhttp/3.9.1',\n      // },\n    })\n    // return this._requestObj_list.promise.then(({ statusCode, body }) => {\n    //   if (statusCode !== 200) return this.getList(sortId, tagId, page)\n    //   let list = body.replace(/[\\r\\n]/g, '').match(this.regExps.list)\n    //   if (!list) return Promise.reject(new Error('获取列表失败'))\n    //   return list.map(item => {\n    //     let info = item.match(this.regExps.listInfo)\n    //     return {\n    //       play_count: info[4],\n    //       id: info[2],\n    //       author: '',\n    //       name: info[3],\n    //       time: '',\n    //       img: info[1],\n    //       grade: 0,\n    //       desc: '',\n    //       source: 'mg',\n    //     }\n    //   })\n    // })\n    // return this._requestObj_list.promise.then(({ body }) => {\n    //   console.log(body)\n    //   if (body.retCode !== '100000' || body.retMsg.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)\n    //   return {\n    //     list: this.filterList(body.retMsg.playlist),\n    //     total: parseInt(body.retMsg.countSize),\n    //     page,\n    //     limit: this.limit_list,\n    //     source: 'mg',\n    //   }\n    // })\n    return this._requestObj_list.promise.then(({ body }) => {\n      // console.log(body)\n      // if (body.retCode !== '000000') return this.getList(sortId, tagId, page, ++tryNum)\n      if (body.code !== '000000') return this.getList(sortId, tagId, page, ++tryNum)\n      const list = body.data.contents ? this.filterList2(body.data.contents) : this.filterList(body.data.contentItemList[1].itemList)\n      return {\n        list,\n        total: 99999,\n        page,\n        limit: this.limit_list,\n        source: 'mg',\n      }\n    })\n  },\n  filterList2(listData, list = [], ids = new Set()) {\n    for (const item of listData) {\n      if (item.contents) this.filterList2(item.contents, list, ids)\n      else if (item.resType == '2021' && !ids.has(item.resId)) {\n        ids.add(item.resId)\n        list.push({\n          id: String(item.resId),\n          author: '',\n          name: item.txt,\n          // time: dateFormat(item.createTime, 'Y-M-D'),\n          img: item.img,\n          // grade: item.grade,\n          // total: item.contentCount,\n          desc: item.txt2,\n          source: 'mg',\n        })\n      }\n    }\n    return list\n  },\n  filterList(rawData) {\n    // console.log(rawData)\n    return rawData.map(item => ({\n      play_count: item.barList[0]?.title,\n      id: String(item.logEvent.contentId),\n      author: '',\n      name: item.title,\n      // time: dateFormat(item.createTime, 'Y-M-D'),\n      img: item.imageUrl,\n      // grade: item.grade,\n      // total: item.contentCount,\n      desc: '',\n      source: 'mg',\n    }))\n  },\n\n  // 获取标签\n  getTag(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.tagsUrl, { headers: this.defaultHeaders })\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getTag(++tryNum)\n      // console.log(body)\n      return this.filterTagInfo(body.data)\n    })\n    // return Promise.resolve(this.filterTagInfo(tagData.columnInfo.contents))\n  },\n  filterTagInfo(rawList) {\n    return {\n      hotTag: rawList[0].content.map(({ texts: [name, id] }) => ({\n        id,\n        name,\n        source: 'mg',\n      })),\n      tags: rawList.slice(1).map(({ header, content }) => ({\n        name: header.title,\n        list: content.map(({ texts: [name, id] }) => ({\n          // parent_id: objectInfo.columnId,\n          // parent_name: objectInfo.columnTitle,\n          id,\n          name,\n          source: 'mg',\n        })),\n      })),\n      source: 'mg',\n    }\n    // return {\n    //   hotTag: rawList[0].objectInfo.contents.map(item => ({\n    //     id: item.objectInfo.tagId,\n    //     name: item.objectInfo.tagName,\n    //     source: 'mg',\n    //   })),\n    //   tags: rawList.slice(1).map(({ objectInfo }) => ({\n    //     name: objectInfo.columnTitle,\n    //     list: objectInfo.contents.map(item => ({\n    //       parent_id: objectInfo.columnId,\n    //       parent_name: objectInfo.columnTitle,\n    //       id: item.objectInfo.tagId,\n    //       name: item.objectInfo.tagName,\n    //       source: 'mg',\n    //     })),\n    //   })),\n    //   source: 'mg',\n    // }\n  },\n  getTags() {\n    return this.getTag()\n  },\n\n  getDetailPageUrl(id) {\n    if (/playlist\\/index\\.html\\?/.test(id)) {\n      id = id.replace(/.*(?:\\?|&)id=(\\d+)(?:&.*|$)/, '$1')\n    } else if (this.regExps.listDetailLink.test(id)) {\n      id = id.replace(this.regExps.listDetailLink, '$1')\n    }\n    return `https://music.migu.cn/v3/music/playlist/${id}`\n  },\n\n  filterSongListResult(raw) {\n    const list = []\n    raw.forEach(item => {\n      if (!item.id) return\n\n      const playCount = parseInt(item.playNum)\n      list.push({\n        play_count: isNaN(playCount) ? 0 : formatPlayCount(playCount),\n        id: item.id,\n        author: item.userName,\n        name: item.name,\n        img: item.musicListPicUrl,\n        total: item.musicNum,\n        source: 'mg',\n      })\n    })\n    return list\n  },\n  search(text, page, limit = 20) {\n    const timeStr = Date.now().toString()\n    const signResult = createSignature(timeStr, text)\n    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`, {\n      headers: {\n        uiVersion: 'A_music_3.6.1',\n        deviceId: signResult.deviceId,\n        timestamp: timeStr,\n        sign: signResult.sign,\n        channel: '0146921',\n        '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',\n      },\n    }).then(body => {\n      if (!body.songListResultData) throw new Error('get song list faild.')\n\n      const list = this.filterSongListResult(body.songListResultData.result)\n      return {\n        list,\n        limit,\n        total: parseInt(body.songListResultData.totalCount),\n        source: 'mg',\n      }\n    })\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/temp/leaderboard-old.js",
    "content": "import { httpFetch } from '../../../request'\nimport { formatPlayTime } from '../../../index'\n// import { sizeFormate } from '../../index'\n\n\n// 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' }]\nconst boardList = [\n  { id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319', webId: 'jianjiao_newsong' },\n  { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466', webId: 'jianjiao_hotsong' },\n  { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408', webId: 'jianjiao_original' },\n  { id: 'mg__migumusic', name: '音乐榜', bangid: 'migumusic', webId: 'migumusic' },\n  { id: 'mg__movies', name: '影视榜', bangid: 'movies', webId: 'movies' },\n  { id: 'mg__23189800', name: '港台榜', bangid: '23189800', webId: 'hktw' },\n  { id: 'mg__23189399', name: '内地榜', bangid: '23189399', webId: 'mainland' },\n  { id: 'mg__19190036', name: '欧美榜', bangid: '19190036', webId: 'eur_usa' },\n  { id: 'mg__23189813', name: '日韩榜', bangid: '23189813', webId: 'jpn_kor' },\n  { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126', webId: 'coloring' },\n  { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045', webId: 'ktv' },\n  { id: 'mg__15140034', name: '网络榜', bangid: '15140034', webId: 'network' },\n  { id: 'mg__23217754', name: 'MV榜', bangid: '23217754', webId: 'mv' },\n  { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151', webId: 'newalbum' },\n  { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042', webId: 'itunes' },\n  { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570', webId: 'billboard' },\n  { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815', webId: 'hito' },\n  { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' },\n  { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943', webId: 'mnet' },\n  { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437', webId: 'uk' },\n]\n// const boardList = [\n//   { id: 'mg__jianjiao_newsong', bangid: 'jianjiao_newsong', name: '尖叫新歌榜' },\n//   { id: 'mg__jianjiao_hotsong', bangid: 'jianjiao_hotsong', name: '尖叫热歌榜' },\n//   { id: 'mg__jianjiao_original', bangid: 'jianjiao_original', name: '尖叫原创榜' },\n//   { id: 'mg__migumusic', bangid: 'migumusic', name: '音乐榜' },\n//   { id: 'mg__movies', bangid: 'movies', name: '影视榜' },\n//   { id: 'mg__mainland', bangid: 'mainland', name: '内地榜' },\n//   { id: 'mg__hktw', bangid: 'hktw', name: '港台榜' },\n//   { id: 'mg__eur_usa', bangid: 'eur_usa', name: '欧美榜' },\n//   { id: 'mg__jpn_kor', bangid: 'jpn_kor', name: '日韩榜' },\n//   { id: 'mg__coloring', bangid: 'coloring', name: '彩铃榜' },\n//   { id: 'mg__ktv', bangid: 'ktv', name: 'KTV榜' },\n//   { id: 'mg__network', bangid: 'network', name: '网络榜' },\n//   { id: 'mg__newalbum', bangid: 'newalbum', name: '新专辑榜' },\n//   { id: 'mg__mv', bangid: 'mv', name: 'MV榜' },\n//   { id: 'mg__itunes', bangid: 'itunes', name: '美国iTunes榜' },\n//   { id: 'mg__billboard', bangid: 'billboard', name: '美国billboard榜' },\n//   { id: 'mg__hito', bangid: 'hito', name: 'Hito中文榜' },\n//   { id: 'mg__mnet', bangid: 'mnet', name: '韩国Melon榜' },\n//   { id: 'mg__uk', bangid: 'uk', name: '英国UK榜' },\n// ]\n\nexport default {\n  limit: 10000,\n  getUrl(id, page) {\n    const targetBoard = boardList.find(board => board.bangid == id)\n    return `https://music.migu.cn/v3/music/top/${targetBoard.webId}`\n    // return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}`\n  },\n  successCode: '000000',\n  requestBoardsObj: null,\n  regExps: {\n    listData: /var listData = (\\{.+\\})<\\/script>/,\n  },\n  getData(url) {\n    const requestObj = httpFetch(url)\n    return requestObj.promise\n  },\n  getSinger(singers) {\n    let arr = []\n    singers.forEach(singer => {\n      arr.push(singer.name)\n    })\n    return arr.join('、')\n  },\n  getIntv(interval) {\n    if (!interval) return 0\n    let intvArr = interval.split(':')\n    let intv = 0\n    let unit = 1\n    while (intvArr.length) {\n      intv += (intvArr.pop()) * unit\n      unit *= 60\n    }\n    return parseInt(intv)\n  },\n  formateIntv() {\n\n  },\n  filterData(rawData) {\n    // console.log(JSON.stringify(rawData))\n    // console.log(rawData)\n    let ids = new Set()\n    const list = []\n    rawData.forEach(item => {\n      if (ids.has(item.copyrightId)) return\n      ids.add(item.copyrightId)\n\n      const types = []\n      const _types = {}\n\n      const size = null\n      types.push({ type: '128k', size })\n      _types['128k'] = { size }\n\n      if (item.hq) {\n        const size = null\n        types.push({ type: '320k', size })\n        _types['320k'] = { size }\n      }\n      if (item.sq) {\n        const size = null\n        types.push({ type: 'flac', size })\n        _types.flac = { size }\n      }\n\n      list.push({\n        singer: this.getSinger(item.singers),\n        name: item.name,\n        albumName: item.album && item.album.albumName,\n        albumId: item.album && item.album.albumId,\n        songmid: item.id,\n        copyrightId: item.copyrightId,\n        source: 'mg',\n        interval: item.duration ? formatPlayTime(this.getIntv(item.duration)) : null,\n        img: item.mediumPic ? `https:${item.mediumPic}` : null,\n        lrc: null,\n        // lrcUrl: item.lrcUrl,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    return list\n  },\n  filterBoardsData(rawList) {\n    // console.log(rawList)\n    let list = []\n    for (const board of rawList) {\n      if (board.template != 'group1') continue\n      for (const item of board.itemList) {\n        if ((item.template != 'row1' && item.template != 'grid1' && !item.actionUrl) || !item.actionUrl.includes('rank-info')) continue\n\n        let data = item.displayLogId.param\n        list.push({\n          id: 'mg__' + data.rankId,\n          name: data.rankName,\n          bangid: String(data.rankId),\n        })\n      }\n    }\n    return list\n  },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // // console.log(response.body.data.contentItemList)\n    // if (response.statusCode !== 200 || response.body.code !== this.successCode) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.data.contentItemList)\n    // // console.log(list)\n    // // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'mg',\n    // }\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'mg',\n    }\n  },\n  getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => {\n      if (statusCode !== 200) return this.getList(bangid, page, retryNum)\n      let listData = body.match(this.regExps.listData)\n      if (!listData) return this.getList(bangid, page, retryNum)\n      const datas = JSON.parse(RegExp.$1)\n      // console.log(datas)\n      listData = this.filterData(datas.songs.items)\n      return {\n        total: datas.songs.itemTotal,\n        list: this.filterData(datas.songs.items),\n        limit: this.limit,\n        page,\n        source: 'mg',\n      }\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/tipSearch.js",
    "content": "import { createHttpFetch } from './utils'\n\nexport default {\n  requestObj: null,\n  cancelTipSearch() {\n    if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()\n  },\n  tipSearchBySong(str) {\n    this.cancelTipSearch()\n    this.requestObj = createHttpFetch(`https://music.migu.cn/v3/api/search/suggest?keyword=${encodeURIComponent(str)}`, {\n      headers: {\n        referer: 'https://music.migu.cn/v3',\n      },\n    })\n    return this.requestObj.then(body => {\n      return body.songs\n    })\n  },\n  handleResult(rawData) {\n    return rawData.map(info => `${info.name} - ${info.singerName}`)\n  },\n  async search(str) {\n    return this.tipSearchBySong(str).then(result => this.handleResult(result))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/utils/index.js",
    "content": "import { httpFetch } from '../../../request'\n\n/**\n * 创建一个适用于MG的Http请求\n * @param {*} url\n * @param {*} options\n * @param {*} retryNum\n */\nexport const createHttpFetch = async(url, options, retryNum = 0) => {\n  if (retryNum > 2) throw new Error('try max num')\n  let result\n  try {\n    result = await httpFetch(url, options).promise\n  } catch (err) {\n    console.log(err)\n    return createHttpFetch(url, options, ++retryNum)\n  }\n  if (result.statusCode !== 200 ||\n    (\n      (result.body.code !== undefined\n        ? result.body.code\n        : result.body.returnCode !== undefined\n          ? result.body.returnCode\n          : result.body.code\n      ) !== '000000')\n  ) return createHttpFetch(url, options, ++retryNum)\n  if (result.body.data) return result.body.data\n  return result.body\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/mg/utils/mrc.js",
    "content": "\n// const key = 'karakal@123Qcomyidongtiantianhaoting'\nconst DELTA = 2654435769n\nconst MIN_LENGTH = 32\n// const SPECIAL_CHAR = '0'\nconst keyArr = [\n  27303562373562475n,\n  18014862372307051n,\n  22799692160172081n,\n  34058940340699235n,\n  30962724186095721n,\n  27303523720101991n,\n  27303523720101998n,\n  31244139033526382n,\n  28992395054481524n,\n]\n\n\nconst teaDecrypt = (data, key) => {\n  const length = data.length\n  const lengthBitint = BigInt(length)\n  if (length >= 1) {\n    // let j = data[data.length - 1];\n    let j2 = data[0]\n    let j3 = toLong((6n + (52n / lengthBitint)) * DELTA)\n    while (true) {\n      let j4 = j3\n      if (j4 == 0n) break\n      let j5 = toLong(3n & toLong(j4 >> 2n))\n      let j6 = lengthBitint\n      while (true) {\n        j6--\n        if (j6 > 0n) {\n          let j7 = data[(j6 - 1n)]\n          let i = j6\n          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)))))\n          data[i] = j2\n        } else break\n      }\n      let j8 = data[lengthBitint - 1n]\n      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)))))\n      data[0] = j2\n      j3 = toLong(j4 - DELTA)\n    }\n  }\n  return data\n}\n\nconst longArrToString = (data) => {\n  const arrayList = []\n  for (const j of data) arrayList.push(longToBytes(j).toString('utf16le'))\n  return arrayList.join('')\n}\n\n// https://stackoverflow.com/a/29132118\nconst longToBytes = (l) => {\n  const result = Buffer.alloc(8)\n  for (let i = 0; i < 8; i++) {\n    result[i] = parseInt(l & 0xFFn)\n    l >>= 8n\n  }\n  return result\n}\n\n\nconst toBigintArray = (data) => {\n  const length = Math.floor(data.length / 16)\n  const jArr = Array(length)\n  for (let i = 0; i < length; i++) {\n    jArr[i] = toLong(data.substring(i * 16, (i * 16) + 16))\n  }\n  return jArr\n}\n\n// https://github.com/lyswhut/lx-music-desktop/issues/445#issuecomment-1139338682\nconst MAX = 9223372036854775807n\nconst MIN = -9223372036854775808n\nconst toLong = str => {\n  const num = typeof str == 'string' ? BigInt('0x' + str) : str\n  if (num > MAX) return toLong(num - (1n << 64n))\n  else if (num < MIN) return toLong(num + (1n << 64n))\n  return num\n}\n\nexport const decrypt = (data) => {\n  // console.log(data.length)\n  // -3551594764563790630\n  // console.log(toLongArrayFromArr(Buffer.from(key)))\n  // console.log(teaDecrypt(toBigintArray(data), keyArr))\n  // console.log(longArrToString(teaDecrypt(toBigintArray(data), keyArr)))\n  // console.log(toByteArray(teaDecrypt(toBigintArray(data), keyArr)))\n  return (data == null || data.length < MIN_LENGTH)\n    ? data\n    : longArrToString(teaDecrypt(toBigintArray(data), keyArr))\n}\n\n// console.log(14895149309145760986n - )\n// console.log(toLong('14895149309145760986'))\n// console.log(decrypt(str))\n// console.log(decrypt(str))\n// console.log(toByteArray([6048138644744000495n]))\n// console.log(toByteArray([16325999628386395n]))\n// console.log(toLong(90994076459972177136n))\n\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/options.js",
    "content": "export const bHh = '624868746c'\n\nexport const headers = {\n  'User-Agent': 'lx-music request',\n  [bHh]: [bHh],\n}\n\n\nexport const timeout = 15000\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_messoer = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/tx/${songInfo.songmid}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n  getPic(songInfo) {\n    return Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`)\n  },\n}\n\nexport default api_messoer\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/comment.js",
    "content": "import { httpFetch } from '../../request'\nimport { dateFormat2 } from '../../index'\nimport getMusicInfo from './musicInfo'\n\nconst emojis = {\n  e400846: '😘',\n  e400874: '😴',\n  e400825: '😃',\n  e400847: '😙',\n  e400835: '😍',\n  e400873: '😳',\n  e400836: '😎',\n  e400867: '😭',\n  e400832: '😊',\n  e400837: '😏',\n  e400875: '😫',\n  e400831: '😉',\n  e400855: '😡',\n  e400823: '😄',\n  e400862: '😨',\n  e400844: '😖',\n  e400841: '😓',\n  e400830: '😈',\n  e400828: '😆',\n  e400833: '😋',\n  e400822: '😀',\n  e400843: '😕',\n  e400829: '😇',\n  e400824: '😂',\n  e400834: '😌',\n  e400877: '😷',\n  e400132: '🍉',\n  e400181: '🍺',\n  e401067: '☕️',\n  e400186: '🥧',\n  e400343: '🐷',\n  e400116: '🌹',\n  e400126: '🍃',\n  e400613: '💋',\n  e401236: '❤️',\n  e400622: '💔',\n  e400637: '💣',\n  e400643: '💩',\n  e400773: '🔪',\n  e400102: '🌛',\n  e401328: '🌞',\n  e400420: '👏',\n  e400914: '🙌',\n  e400408: '👍',\n  e400414: '👎',\n  e401121: '✋',\n  e400396: '👋',\n  e400384: '👉',\n  e401115: '✊',\n  e400402: '👌',\n  e400905: '🙈',\n  e400906: '🙉',\n  e400907: '🙊',\n  e400562: '👻',\n  e400932: '🙏',\n  e400644: '💪',\n  e400611: '💉',\n  e400185: '🎁',\n  e400655: '💰',\n  e400325: '🐥',\n  e400612: '💊',\n  e400198: '🎉',\n  e401685: '⚡️',\n  e400631: '💝',\n  e400768: '🔥',\n  e400432: '👑',\n}\n\nconst songIdMap = new Map()\nconst promises = new Map()\n\nexport default {\n  _requestObj: null,\n  _requestObj2: null,\n  async getSongId({ songId, songmid }) {\n    if (songId) return songId\n    if (songIdMap.has(songmid)) return songIdMap.get(songmid)\n    if (promises.has(songmid)) return (await promises.get(songmid)).songId\n    const promise = getMusicInfo(songmid)\n    promises.set(promise)\n    const info = await promise\n    songIdMap.set(songmid, info.songId)\n    promises.delete(songmid)\n    return info.songId\n  },\n  async getComment(mInfo, page = 1, limit = 20) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    const songId = await this.getSongId(mInfo)\n\n    const _requestObj = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', {\n      method: 'POST',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n      },\n      form: {\n        uin: '0',\n        format: 'json',\n        cid: '205360772',\n        reqtype: '2',\n        biztype: '1',\n        topid: songId,\n        cmd: '8',\n        needmusiccrit: '1',\n        pagenum: page - 1,\n        pagesize: limit,\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.code !== 0) throw new Error('获取评论失败')\n    // console.log(body, statusCode)\n    const comment = body.comment\n    return {\n      source: 'tx',\n      comments: this.filterNewComment(comment.commentlist),\n      total: comment.commenttotal,\n      page,\n      limit,\n      maxPage: Math.ceil(comment.commenttotal / limit) || 1,\n    }\n  },\n  async getHotComment(mInfo, page = 1, limit = 20) {\n    // const _requestObj2 = httpFetch('http://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg', {\n    //   method: 'POST',\n    //   headers: {\n    //     'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n    //   },\n    //   form: {\n    //     uin: '0',\n    //     format: 'json',\n    //     cid: '205360772',\n    //     reqtype: '2',\n    //     biztype: '1',\n    //     topid: songId,\n    //     cmd: '9',\n    //     needmusiccrit: '1',\n    //     pagenum: page - 1,\n    //     pagesize: limit,\n    //   },\n    // })\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n    const songId = await this.getSongId(mInfo)\n\n    const _requestObj2 = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n      method: 'POST',\n      body: {\n        comm: {\n          cv: 4747474,\n          ct: 24,\n          format: 'json',\n          inCharset: 'utf-8',\n          outCharset: 'utf-8',\n          notice: 0,\n          platform: 'yqq.json',\n          needNewCode: 1,\n          uin: 0,\n        },\n        req: {\n          module: 'music.globalComment.CommentRead',\n          method: 'GetHotCommentList',\n          param: {\n            BizType: 1,\n            BizId: String(songId),\n            LastCommentSeqNo: '',\n            PageSize: limit,\n            PageNum: page - 1,\n            HotType: 1,\n            WithAirborne: 0,\n            PicEnable: 1,\n          },\n        },\n      },\n      headers: {\n        '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',\n        referer: 'https://y.qq.com/',\n        origin: 'https://y.qq.com',\n      },\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    // console.log('body', body)\n    if (statusCode != 200 || body.code !== 0 || body.req.code !== 0) throw new Error('获取热门评论失败')\n    const comment = body.req.data.CommentList\n    return {\n      source: 'tx',\n      comments: this.filterHotComment(comment.Comments),\n      total: comment.Total,\n      page,\n      limit,\n      maxPage: Math.ceil(comment.Total / limit) || 1,\n    }\n  },\n  filterNewComment(rawList) {\n    return rawList.map(item => {\n      let time = this.formatTime(item.time)\n      let timeStr = time ? dateFormat2(time) : null\n      if (item.middlecommentcontent) {\n        let firstItem = item.middlecommentcontent[0]\n        firstItem.avatarurl = item.avatarurl\n        firstItem.praisenum = item.praisenum\n        item.avatarurl = null\n        item.praisenum = null\n        item.middlecommentcontent.reverse()\n      }\n      return {\n        id: `${item.rootcommentid}_${item.commentid}`,\n        rootId: item.rootcommentid,\n        text: item.rootcommentcontent ? this.replaceEmoji(item.rootcommentcontent).replace(/\\\\n/g, '\\n') : '',\n        time: item.rootcommentid == item.commentid ? time : null,\n        timeStr: item.rootcommentid == item.commentid ? timeStr : null,\n        userName: item.rootcommentnick ? item.rootcommentnick.substring(1) : '',\n        avatar: item.avatarurl,\n        userId: item.encrypt_rootcommentuin,\n        likedCount: item.praisenum,\n        reply: item.middlecommentcontent\n          ? item.middlecommentcontent.map(c => {\n            // let index = c.subcommentid.lastIndexOf('_')\n            return {\n              id: `sub_${item.rootcommentid}_${c.subcommentid}`,\n              text: this.replaceEmoji(c.subcommentcontent).replace(/\\\\n/g, '\\n'),\n              time: c.subcommentid == item.commentid ? time : null,\n              timeStr: c.subcommentid == item.commentid ? timeStr : null,\n              userName: c.replynick.substring(1),\n              avatar: c.avatarurl,\n              userId: c.encrypt_replyuin,\n              likedCount: c.praisenum,\n            }\n          })\n          : [],\n      }\n    })\n  },\n  filterHotComment(rawList) {\n    return rawList.map(item => {\n      return {\n        id: `${item.SeqNo}_${item.CmId}`,\n        rootId: item.SeqNo,\n        text: item.Content ? this.replaceEmoji(item.Content).replace(/\\\\n/g, '\\n') : '',\n        time: item.PubTime ? this.formatTime(item.PubTime) : null,\n        timeStr: item.PubTime ? dateFormat2(this.formatTime(item.PubTime)) : null,\n        userName: item.Nick ?? '',\n        images: item.Pic ? [item.Pic] : [],\n        avatar: item.Avatar,\n        location: item.Location ? item.Location : '',\n        userId: item.EncryptUin,\n        likedCount: item.PraiseNum,\n        reply: item.SubComments\n          ? item.SubComments.map(c => {\n            return {\n              id: `sub_${c.SeqNo}_${c.CmId}`,\n              text: this.replaceEmoji(c.Content).replace(/\\\\n/g, '\\n'),\n              time: c.PubTime ? this.formatTime(c.PubTime) : null,\n              timeStr: c.PubTime ? dateFormat2(this.formatTime(c.PubTime)) : null,\n              userName: c.Nick ?? '',\n              avatar: c.Avatar,\n              images: c.Pic ? [c.Pic] : [],\n              userId: c.EncryptUin,\n              likedCount: c.PraiseNum,\n            }\n          })\n          : [],\n      }\n    })\n  },\n  replaceEmoji(msg) {\n    let rxp = /^\\[em\\](e\\d+)\\[\\/em\\]$/\n    let result = msg.match(/\\[em\\]e\\d+\\[\\/em\\]/g)\n    if (!result) return msg\n    result = Array.from(new Set(result))\n    for (let item of result) {\n      let code = item.replace(rxp, '$1')\n      msg = msg.replace(new RegExp(item.replace('[em]', '\\\\[em\\\\]').replace('[/em]', '\\\\[\\\\/em\\\\]'), 'g'), emojis[code] || '')\n    }\n    return msg\n  },\n  formatTime(time) {\n    return String(time).length < 10 ? null : parseInt(time + '000')\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/hotSearch.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    // const _requestObj = httpFetch('https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg', {\n    //   method: 'get',\n    //   headers: {\n    //     Referer: 'https://y.qq.com/portal/player.html',\n    //   },\n    // })\n    const _requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n      method: 'post',\n      body: {\n        comm: {\n          ct: '19',\n          cv: '1803',\n          guid: '0',\n          patch: '118',\n          psrf_access_token_expiresAt: 0,\n          psrf_qqaccess_token: '',\n          psrf_qqopenid: '',\n          psrf_qqunionid: '',\n          tmeAppID: 'qqmusic',\n          tmeLoginType: 0,\n          uin: '0',\n          wid: '0',\n        },\n        hotkey: {\n          method: 'GetHotkeyForQQMusicPC',\n          module: 'tencent_musicsoso_hotkey.HotkeyService',\n          param: {\n            search_id: '',\n            uin: 0,\n          },\n        },\n      },\n      headers: {\n        Referer: 'https://y.qq.com/portal/player.html',\n      },\n    })\n    const { body, statusCode } = await _requestObj.promise\n    // console.log(body)\n    if (statusCode != 200 || body.code !== 0) throw new Error('获取热搜词失败')\n    // console.log(body)\n    return { source: 'tx', list: this.filterList(body.hotkey.data.vec_hotkey) }\n  },\n  filterList(rawList) {\n    return rawList.map(item => item.query)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/index.js",
    "content": "import leaderboard from './leaderboard'\nimport lyric from './lyric'\nimport songList from './songList'\nimport musicSearch from './musicSearch'\nimport { apis } from '../api-source'\nimport hotSearch from './hotSearch'\nimport comment from './comment'\n// import tipSearch from './tipSearch'\n\nconst tx = {\n  // tipSearch,\n  leaderboard,\n  songList,\n  musicSearch,\n  hotSearch,\n  comment,\n\n  getMusicUrl(songInfo, type) {\n    return apis('tx').getMusicUrl(songInfo, type)\n  },\n  getLyric(songInfo) {\n    // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer\n    return lyric.getLyric(songInfo)\n  },\n  async getPic(songInfo) {\n    return `https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`\n  },\n  getMusicDetailPageUrl(songInfo) {\n    return `https://y.qq.com/n/yqq/song/${songInfo.songmid}.html`\n  },\n}\n\nexport default tx\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/leaderboard.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\nlet 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' }]\n\nexport default {\n  limit: 300,\n  list: [\n    {\n      id: 'txlxzsb',\n      name: '流行榜',\n      bangid: 4,\n    },\n    {\n      id: 'txrgb',\n      name: '热歌榜',\n      bangid: 26,\n    },\n    {\n      id: 'txwlhgb',\n      name: '网络榜',\n      bangid: 28,\n    },\n    {\n      id: 'txdyb',\n      name: '抖音榜',\n      bangid: 60,\n    },\n    {\n      id: 'txndb',\n      name: '内地榜',\n      bangid: 5,\n    },\n    {\n      id: 'txxgb',\n      name: '香港榜',\n      bangid: 59,\n    },\n    {\n      id: 'txtwb',\n      name: '台湾榜',\n      bangid: 61,\n    },\n    {\n      id: 'txoumb',\n      name: '欧美榜',\n      bangid: 3,\n    },\n    {\n      id: 'txhgb',\n      name: '韩国榜',\n      bangid: 16,\n    },\n    {\n      id: 'txrbb',\n      name: '日本榜',\n      bangid: 17,\n    },\n    {\n      id: 'txtybb',\n      name: 'YouTube榜',\n      bangid: 128,\n    },\n  ],\n  listDetailRequest(id, period, limit) {\n    // console.log(id, period, limit)\n    return httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n      method: 'post',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n      },\n      body: {\n        toplist: {\n          module: 'musicToplist.ToplistInfoServer',\n          method: 'GetDetail',\n          param: {\n            topid: id,\n            num: limit,\n            period,\n          },\n        },\n        comm: {\n          uin: 0,\n          format: 'json',\n          ct: 20,\n          cv: 1859,\n        },\n      },\n    }).promise\n  },\n  regExps: {\n    periodList: /<i class=\"play_cover__btn c_tx_link js_icon_play\" data-listkey=\".+?\" data-listname=\".+?\" data-tid=\".+?\" data-date=\".+?\" .+?<\\/i>/g,\n    period: /data-listname=\"(.+?)\" data-tid=\".*?\\/(.+?)\" data-date=\"(.+?)\" .+?<\\/i>/,\n  },\n  periods: {},\n  periodUrl: 'https://c.y.qq.com/node/pc/wk_v15/top.html',\n  _requestBoardsObj: null,\n  getBoardsData() {\n    if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()\n    this._requestBoardsObj = httpFetch('https://c.y.qq.com/v8/fcg-bin/fcg_myqq_toplist.fcg?g_tk=1928093487&inCharset=utf-8&outCharset=utf-8&notice=0&format=json&uin=0&needNewCode=1&platform=h5')\n    return this._requestBoardsObj.promise\n  },\n  getData(url) {\n    const requestDataObj = httpFetch(url)\n    return requestDataObj.promise\n  },\n  filterData(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      let types = []\n      let _types = {}\n      if (item.file.size_128mp3 !== 0) {\n        let size = sizeFormate(item.file.size_128mp3)\n        types.push({ type: '128k', size })\n        _types['128k'] = {\n          size,\n        }\n      }\n      if (item.file.size_320mp3 !== 0) {\n        let size = sizeFormate(item.file.size_320mp3)\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n      }\n      if (item.file.size_flac !== 0) {\n        let size = sizeFormate(item.file.size_flac)\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      if (item.file.size_hires !== 0) {\n        let size = sizeFormate(item.file.size_hires)\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      // types.reverse()\n      return {\n        singer: formatSingerName(item.singer, 'name'),\n        name: item.title,\n        albumName: item.album.name,\n        albumId: item.album.mid,\n        source: 'tx',\n        interval: formatPlayTime(item.interval),\n        songId: item.id,\n        albumMid: item.album.mid,\n        strMediaMid: item.file.media_mid,\n        songmid: item.mid,\n        img: (item.album.name === '' || item.album.name === '空')\n          ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''\n          : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  getPeriods(bangid) {\n    return this.getData(this.periodUrl).then(({ body: html }) => {\n      let result = html.match(this.regExps.periodList)\n      if (!result) return Promise.reject(new Error('get data failed'))\n      result.forEach(item => {\n        let result = item.match(this.regExps.period)\n        if (!result) return\n        this.periods[result[2]] = {\n          name: result[1],\n          bangid: result[2],\n          period: result[3],\n        }\n      })\n      const info = this.periods[bangid]\n      return info && info.period\n    })\n  },\n  filterBoardsData(rawList) {\n    // console.log(rawList)\n    let list = []\n    for (const board of rawList) {\n      // 排除 MV榜\n      if (board.id == 201) continue\n\n      if (board.topTitle.startsWith('巅峰榜·')) {\n        board.topTitle = board.topTitle.substring(4, board.topTitle.length)\n      }\n      if (!board.topTitle.endsWith('榜')) board.topTitle += '榜'\n      list.push({\n        id: 'tx__' + board.id,\n        name: board.topTitle,\n        bangid: String(board.id),\n      })\n    }\n    return list\n  },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // // console.log(response.body)\n    // if (response.statusCode !== 200 || response.body.code !== 0) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.data.topList)\n    // console.log(list)\n    // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'tx',\n    // }\n    this.list = boardList\n    return {\n      list: boardList,\n      source: 'tx',\n    }\n  },\n  getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    bangid = parseInt(bangid)\n    let info = this.periods[bangid]\n    let p = info ? Promise.resolve(info.period) : this.getPeriods(bangid)\n    return p.then(period => {\n      return this.listDetailRequest(bangid, period, this.limit).then(resp => {\n        if (resp.body.code !== 0) return this.getList(bangid, page, retryNum)\n        return {\n          total: resp.body.toplist.data.songInfoList.length,\n          list: this.filterData(resp.body.toplist.data.songInfoList),\n          limit: this.limit,\n          page: 1,\n          source: 'tx',\n        }\n      })\n    })\n  },\n\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') id = id.replace('tx__', '')\n    return `https://y.qq.com/n/ryqq/toplist/${id}`\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport getMusicInfo from './musicInfo'\nimport { rendererInvoke } from '@common/rendererIpc'\nimport { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'\n\nconst songIdMap = new Map()\nconst promises = new Map()\nexport const decodeLyric = (lrc, tlrc, rlrc) => rendererInvoke(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, { lrc, tlrc, rlrc })\n\n\nconst parseTools = {\n  rxps: {\n    info: /^{\"/,\n    lineTime: /^\\[(\\d+),\\d+\\]/,\n    lineTime2: /^\\[([\\d:.]+)\\]/,\n    wordTime: /\\(\\d+,\\d+\\)/,\n    wordTimeAll: /(\\(\\d+,\\d+\\))/g,\n    timeLabelFixRxp: /(?:\\.0+|0+)$/,\n  },\n  msFormat(timeMs) {\n    if (Number.isNaN(timeMs)) return ''\n    let ms = timeMs % 1000\n    timeMs /= 1000\n    let m = parseInt(timeMs / 60).toString().padStart(2, '0')\n    timeMs %= 60\n    let s = parseInt(timeMs).toString().padStart(2, '0')\n    return `[${m}:${s}.${String(ms).padStart(3, '0')}]`\n  },\n  parseLyric(lrc) {\n    lrc = lrc.trim()\n    lrc = lrc.replace(/\\r/g, '')\n    if (!lrc) return { lyric: '', lxlyric: '' }\n    const lines = lrc.split('\\n')\n\n    const lxlrcLines = []\n    const lrcLines = []\n\n    for (let line of lines) {\n      line = line.trim()\n      let result = this.rxps.lineTime.exec(line)\n      if (!result) {\n        if (line.startsWith('[offset')) {\n          lxlrcLines.push(line)\n          lrcLines.push(line)\n        }\n        if (this.rxps.lineTime2.test(line)) {\n          // lxlrcLines.push(line)\n          lrcLines.push(line)\n        }\n        continue\n      }\n\n      const startMsTime = parseInt(result[1])\n      const startTimeStr = this.msFormat(startMsTime)\n      if (!startTimeStr) continue\n\n      let words = line.replace(this.rxps.lineTime, '')\n\n      lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)\n\n      let times = words.match(this.rxps.wordTimeAll)\n      if (!times) continue\n      times = times.map(time => {\n        const result = /\\((\\d+),(\\d+)\\)/.exec(time)\n        return `<${Math.max(parseInt(result[1]) - startMsTime, 0)},${result[2]}>`\n      })\n      const wordArr = words.split(this.rxps.wordTime)\n      const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')\n      lxlrcLines.push(`${startTimeStr}${newWords}`)\n    }\n    return {\n      lyric: lrcLines.join('\\n'),\n      lxlyric: lxlrcLines.join('\\n'),\n    }\n  },\n  parseRlyric(lrc) {\n    lrc = lrc.trim()\n    lrc = lrc.replace(/\\r/g, '')\n    if (!lrc) return { lyric: '', lxlyric: '' }\n    const lines = lrc.split('\\n')\n\n    const lrcLines = []\n\n    for (let line of lines) {\n      line = line.trim()\n      let result = this.rxps.lineTime.exec(line)\n      if (!result) continue\n\n      const startMsTime = parseInt(result[1])\n      const startTimeStr = this.msFormat(startMsTime)\n      if (!startTimeStr) continue\n\n      let words = line.replace(this.rxps.lineTime, '')\n\n      lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)\n    }\n    return lrcLines.join('\\n')\n  },\n  removeTag(str) {\n    return str.replace(/^[\\S\\s]*?LyricContent=\"/, '').replace(/\"\\/>[\\S\\s]*?$/, '')\n  },\n  getIntv(interval) {\n    if (!interval) return 0\n    if (!interval.includes('.')) interval += '.0'\n    let arr = interval.split(/:|\\./)\n    while (arr.length < 3) arr.unshift('0')\n    const [m, s, ms] = arr\n    return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)\n  },\n  fixRlrcTimeTag(rlrc, lrc) {\n    // console.log(lrc)\n    // console.log(rlrc)\n    const rlrcLines = rlrc.split('\\n')\n    let lrcLines = lrc.split('\\n')\n    // let temp = []\n    let newLrc = []\n    rlrcLines.forEach((line) => {\n      const result = this.rxps.lineTime2.exec(line)\n      if (!result) return\n      const words = line.replace(this.rxps.lineTime2, '')\n      if (!words.trim()) return\n      const t1 = this.getIntv(result[1])\n\n      while (lrcLines.length) {\n        const lrcLine = lrcLines.shift()\n        const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)\n        if (!lrcLineResult) continue\n        const t2 = this.getIntv(lrcLineResult[1])\n        if (Math.abs(t1 - t2) < 100) {\n          newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))\n          break\n        }\n        // temp.push(line)\n      }\n      // lrcLines = [...temp, ...lrcLines]\n      // temp = []\n    })\n    return newLrc.join('\\n')\n  },\n  fixTlrcTimeTag(tlrc, lrc) {\n    // console.log(lrc)\n    // console.log(tlrc)\n    const tlrcLines = tlrc.split('\\n')\n    let lrcLines = lrc.split('\\n')\n    // let temp = []\n    let newLrc = []\n    tlrcLines.forEach((line) => {\n      const result = this.rxps.lineTime2.exec(line)\n      if (!result) return\n      const words = line.replace(this.rxps.lineTime2, '')\n      if (!words.trim()) return\n      let time = result[1]\n      if (time.includes('.')) {\n        time += ''.padStart(3 - time.split('.')[1].length, '0')\n      }\n      const t1 = this.getIntv(time)\n\n      while (lrcLines.length) {\n        const lrcLine = lrcLines.shift()\n        const lrcLineResult = this.rxps.lineTime2.exec(lrcLine)\n        if (!lrcLineResult) continue\n        const t2 = this.getIntv(lrcLineResult[1])\n        if (Math.abs(t1 - t2) < 100) {\n          newLrc.push(line.replace(this.rxps.lineTime2, lrcLineResult[0]))\n          break\n        }\n        // temp.push(line)\n      }\n      // lrcLines = [...temp, ...lrcLines]\n      // temp = []\n    })\n    return newLrc.join('\\n')\n  },\n  parse(lrc, tlrc, rlrc) {\n    const info = {\n      lyric: '',\n      tlyric: '',\n      rlyric: '',\n      lxlyric: '',\n    }\n    if (lrc) {\n      let { lyric, lxlyric } = this.parseLyric(this.removeTag(lrc))\n      info.lyric = lyric\n      info.lxlyric = lxlyric\n      // console.log(lyric)\n      // console.log(lxlyric)\n    }\n    if (rlrc) info.rlyric = this.fixRlrcTimeTag(this.parseRlyric(this.removeTag(rlrc)), info.lyric)\n    if (tlrc) info.tlyric = this.fixTlrcTimeTag(tlrc, info.lyric)\n    // console.log(info.lyric)\n    // console.log(info.tlyric)\n    // console.log(info.rlyric)\n\n    return info\n  },\n}\n\n\nexport default {\n  successCode: 0,\n  async getSongId({ songId, songmid }) {\n    if (songId) return songId\n    if (songIdMap.has(songmid)) return songIdMap.get(songmid)\n    if (promises.has(songmid)) return (await promises.get(songmid)).songId\n    const promise = getMusicInfo(songmid)\n    promises.set(promise)\n    const info = await promise\n    songIdMap.set(songmid, info.songId)\n    promises.delete(songmid)\n    return info.songId\n  },\n  async parseLyric(lrc, tlrc, rlrc) {\n    const { lyric, tlyric, rlyric } = await decodeLyric(lrc, tlrc, rlrc)\n    // return {\n\n    // }\n    // console.log(lyric)\n    // console.log(tlyric)\n    // console.log(rlyric)\n    return parseTools.parse(lyric, tlyric, rlyric)\n  },\n  getLyric(mInfo, retryNum = 0) {\n    if (retryNum > 3) return Promise.reject(new Error('Get lyric failed'))\n\n    return {\n      cancelHttp() {\n\n      },\n      promise: this.getSongId(mInfo).then(songId => {\n        const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n          method: 'post',\n          headers: {\n            referer: 'https://y.qq.com',\n            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36',\n          },\n          body: {\n            comm: {\n              ct: '19',\n              cv: '1859',\n              uin: '0',\n            },\n            req: {\n              method: 'GetPlayLyricInfo',\n              module: 'music.musichallSong.PlayLyricInfo',\n              param: {\n                format: 'json',\n                crypt: 1,\n                ct: 19,\n                cv: 1873,\n                interval: 0,\n                lrc_t: 0,\n                qrc: 1,\n                qrc_t: 0,\n                roma: 1,\n                roma_t: 0,\n                songID: songId,\n                trans: 1,\n                trans_t: 0,\n                type: -1,\n              },\n            },\n          },\n        })\n        return requestObj.promise.then(({ body }) => {\n          // console.log(body)\n          if (body.code != this.successCode || body.req.code != this.successCode) return this.getLyric(songId, ++retryNum)\n          const data = body.req.data\n          return this.parseLyric(data.lyric, data.trans, data.roma)\n        })\n      }),\n    }\n  },\n}\n\n// export default {\n//   regexps: {\n//     matchLrc: /.+\"lyric\":\"([\\w=+/]*)\".+/,\n//   },\n//   getLyric(songmid) {\n//     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`, {\n//       headers: {\n//         Referer: 'https://y.qq.com/portal/player.html',\n//       },\n//     })\n//     requestObj.promise = requestObj.promise.then(({ body }) => {\n//       if (body.code != 0 || !body.lyric) return Promise.reject(new Error('Get lyric failed'))\n//       return {\n//         lyric: decodeName(b64DecodeUnicode(body.lyric)),\n//         tlyric: decodeName(b64DecodeUnicode(body.trans)),\n//       }\n//     })\n//     return requestObj\n//   },\n// }\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/musicInfo.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, sizeFormate } from '../../index'\n\nconst getSinger = (singers) => {\n  let arr = []\n  singers.forEach(singer => {\n    arr.push(singer.name)\n  })\n  return arr.join('、')\n}\n\nexport default (songmid) => {\n  const requestObj = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n    method: 'post',\n    headers: {\n      'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n    },\n    body: {\n      comm: {\n        ct: '19',\n        cv: '1859',\n        uin: '0',\n      },\n      req: {\n        module: 'music.pf_song_detail_svr',\n        method: 'get_song_detail_yqq',\n        param: {\n          song_type: 0,\n          song_mid: songmid,\n        },\n      },\n    },\n  })\n  return requestObj.promise.then(({ body }) => {\n    // console.log(body)\n    if (body.code != 0 || body.req.code != 0) return Promise.reject(new Error('获取歌曲信息失败'))\n    const item = body.req.data.track_info\n    if (!item.file?.media_mid) return null\n\n    let types = []\n    let _types = {}\n    const file = item.file\n    if (file.size_128mp3 != 0) {\n      let size = sizeFormate(file.size_128mp3)\n      types.push({ type: '128k', size })\n      _types['128k'] = {\n        size,\n      }\n    }\n    if (file.size_320mp3 !== 0) {\n      let size = sizeFormate(file.size_320mp3)\n      types.push({ type: '320k', size })\n      _types['320k'] = {\n        size,\n      }\n    }\n    if (file.size_flac !== 0) {\n      let size = sizeFormate(file.size_flac)\n      types.push({ type: 'flac', size })\n      _types.flac = {\n        size,\n      }\n    }\n    if (file.size_hires !== 0) {\n      let size = sizeFormate(file.size_hires)\n      types.push({ type: 'flac24bit', size })\n      _types.flac24bit = {\n        size,\n      }\n    }\n    // types.reverse()\n    let albumId = ''\n    let albumName = ''\n    if (item.album) {\n      albumName = item.album.name\n      albumId = item.album.mid\n    }\n    return {\n      singer: getSinger(item.singer),\n      name: item.title,\n      albumName,\n      albumId,\n      source: 'tx',\n      interval: formatPlayTime(item.interval),\n      songId: item.id,\n      albumMid: item.album?.mid ?? '',\n      strMediaMid: item.file.media_mid,\n      songmid: item.mid,\n      img: (albumId === '' || albumId === '空')\n        ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''\n        : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`,\n      types,\n      _types,\n      typeUrl: {},\n    }\n  })\n}\n\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/musicSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\nexport default {\n  limit: 50,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  successCode: 0,\n  musicSearch(str, page, limit, retryNum = 0) {\n    if (retryNum > 5) return Promise.reject(new Error('搜索失败'))\n    // searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=sizer.yqq.song_next&searchid=49252838123499591&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0`)\n    // const searchRequest = httpFetch(`https://shc.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&remoteplace=txt.yqq.top&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${limit}&w=${encodeURIComponent(str)}&cv=4747474&ct=24&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&uin=0&hostUin=0&loginUin=0`)\n    const searchRequest = httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n      method: 'post',\n      headers: {\n        'User-Agent': 'QQMusic 14090508(android 12)',\n      },\n      body: {\n        comm: {\n          ct: '11',\n          cv: '14090508',\n          v: '14090508',\n          tmeAppID: 'qqmusic',\n          phonetype: 'EBG-AN10',\n          deviceScore: '553.47',\n          devicelevel: '50',\n          newdevicelevel: '20',\n          rom: 'HuaWei/EMOTION/EmotionUI_14.2.0',\n          os_ver: '12',\n          OpenUDID: '0',\n          OpenUDID2: '0',\n          QIMEI36: '0',\n          udid: '0',\n          chid: '0',\n          aid: '0',\n          oaid: '0',\n          taid: '0',\n          tid: '0',\n          wid: '0',\n          uid: '0',\n          sid: '0',\n          modeSwitch: '6',\n          teenMode: '0',\n          ui_mode: '2',\n          nettype: '1020',\n          v4ip: '',\n        },\n        req: {\n          module: 'music.search.SearchCgiService',\n          method: 'DoSearchForQQMusicMobile',\n          param: {\n            search_type: 0,\n            query: str,\n            page_num: page,\n            num_per_page: limit,\n            highlight: 0,\n            nqc_flag: 0,\n            multi_zhida: 0,\n            cat: 2,\n            grp: 1,\n            sin: 0,\n            sem: 0,\n          },\n        },\n      },\n    })\n    // searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`)\n    return searchRequest.promise.then(({ body }) => {\n      // console.log(body)\n      if (body.code != this.successCode || body.req.code != this.successCode) return this.musicSearch(str, page, limit, ++retryNum)\n      return body.req.data\n    })\n  },\n  handleResult(rawList) {\n    // console.log(rawList)\n    const list = []\n    rawList.forEach(item => {\n      if (!item.file?.media_mid) return\n\n      let types = []\n      let _types = {}\n      const file = item.file\n      if (file.size_128mp3 != 0) {\n        let size = sizeFormate(file.size_128mp3)\n        types.push({ type: '128k', size })\n        _types['128k'] = {\n          size,\n        }\n      }\n      if (file.size_320mp3 !== 0) {\n        let size = sizeFormate(file.size_320mp3)\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n      }\n      if (file.size_flac !== 0) {\n        let size = sizeFormate(file.size_flac)\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      if (file.size_hires !== 0) {\n        let size = sizeFormate(file.size_hires)\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      // types.reverse()\n      let albumId = ''\n      let albumName = ''\n      if (item.album) {\n        albumName = item.album.name\n        albumId = item.album.mid\n      }\n      list.push({\n        singer: formatSingerName(item.singer, 'name'),\n        name: item.name + (item.title_extra ?? ''),\n        albumName,\n        albumId,\n        source: 'tx',\n        interval: formatPlayTime(item.interval),\n        songId: item.id,\n        albumMid: item.album?.mid ?? '',\n        strMediaMid: item.file.media_mid,\n        songmid: item.mid,\n        img: (albumId === '' || albumId === '空')\n          ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''\n          : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumId}.jpg`,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    // console.log(list)\n    return list\n  },\n  search(str, page = 1, limit) {\n    if (limit == null) limit = this.limit\n    // http://newlyric.kuwo.cn/newlyric.lrc?62355680\n    return this.musicSearch(str, page, limit).then(({ body, meta }) => {\n      let list = this.handleResult(body.item_song)\n\n      this.total = meta.estimate_sum\n      this.page = page\n      this.allPage = Math.ceil(this.total / limit)\n\n      return Promise.resolve({\n        list,\n        allPage: this.allPage,\n        limit,\n        total: this.total,\n        source: 'tx',\n      })\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/singer.js",
    "content": "import { httpFetch } from '../../request'\n\nimport { formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\nexport const filterMusicInfoItem = item => {\n  const types = []\n  const _types = {}\n  if (item.file.size_128mp3 != 0) {\n    let size = sizeFormate(item.file.size_128mp3)\n    types.push({ type: '128k', size })\n    _types['128k'] = {\n      size,\n    }\n  }\n  if (item.file.size_320mp3 !== 0) {\n    let size = sizeFormate(item.file.size_320mp3)\n    types.push({ type: '320k', size })\n    _types['320k'] = {\n      size,\n    }\n  }\n  if (item.file.size_flac !== 0) {\n    let size = sizeFormate(item.file.size_flac)\n    types.push({ type: 'flac', size })\n    _types.flac = {\n      size,\n    }\n  }\n  if (item.file.size_hires !== 0) {\n    let size = sizeFormate(item.file.size_hires)\n    types.push({ type: 'flac24bit', size })\n    _types.flac24bit = {\n      size,\n    }\n  }\n\n  const albumId = item.album.id ?? ''\n  const albumMid = item.album.mid ?? ''\n  const albumName = item.album.name ?? ''\n  return {\n    source: 'tx',\n    singer: formatSingerName(item.singer, 'name'),\n    name: item.title,\n    albumName,\n    albumId,\n    albumMid,\n    interval: formatPlayTime(item.interval),\n    songId: item.id,\n    songmid: item.mid,\n    strMediaMid: item.file.media_mid,\n    img: (albumId === '' || albumId === '空')\n      ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''\n      : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${albumMid}.jpg`,\n    types,\n    _types,\n    typeUrl: {},\n  }\n}\n\n\n/**\n * 创建一个适用于TX的Http请求\n * @param {*} url\n * @param {*} options\n * @param {*} retryNum\n */\nconst createMusicuFetch = async(data, options, retryNum = 0) => {\n  if (retryNum > 2) throw new Error('try max num')\n\n  let result\n  try {\n    result = await httpFetch('https://u.y.qq.com/cgi-bin/musicu.fcg', {\n      method: 'POST',\n      body: {\n        comm: {\n          cv: 4747474,\n          ct: 24,\n          format: 'json',\n          inCharset: 'utf-8',\n          outCharset: 'utf-8',\n          uin: 0,\n        },\n        ...data,\n      },\n      headers: {\n        'User-Angent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n      },\n    }).promise\n  } catch (err) {\n    console.log(err)\n    return createMusicuFetch(data, options, ++retryNum)\n  }\n  if (result.statusCode !== 200 || result.body.code != 0) return createMusicuFetch(data, options, ++retryNum)\n\n  return result.body\n}\n\nexport default {\n  /**\n   * 获取歌手信息\n   * @param {*} id\n   */\n  getInfo(id) {\n    return createMusicuFetch({\n      req_1: {\n        module: 'music.musichallSinger.SingerInfoInter',\n        method: 'GetSingerDetail',\n        param: {\n          singer_mid: [id],\n          ex_singer: 1,\n          wiki_singer: 1,\n          group_singer: 0,\n          pic: 1,\n          photos: 0,\n        },\n      },\n      req_2: {\n        module: 'music.musichallAlbum.AlbumListServer',\n        method: 'GetAlbumList',\n        param: {\n          singerMid: id,\n          order: 0,\n          begin: 0,\n          num: 1,\n          songNumTag: 0,\n          singerID: 0,\n        },\n      },\n      req_3: {\n        module: 'musichall.song_list_server',\n        method: 'GetSingerSongList',\n        param: {\n          singerMid: id,\n          order: 1,\n          begin: 0,\n          num: 1,\n        },\n      },\n    }).then(body => {\n      if (body.req_1.code != 0 || body.req_2 != 0 || body.req_3 != 0) throw new Error('get singer info faild.')\n\n      const info = body.req_1.data.singer_list[0]\n      const music = body.req_3.data\n      const album = body.req_3.data\n      return {\n        source: 'tx',\n        id: info.basic_info.singer_mid,\n        info: {\n          name: info.basic_info.name,\n          desc: info.ex_info.desc,\n          avatar: info.pic.pic,\n          gender: info.ex_info.genre === 1 ? 'man' : 'woman',\n        },\n        count: {\n          music: music.totalNum,\n          album: album.total,\n        },\n      }\n    })\n  },\n  /**\n   * 获取歌手专辑列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  getAlbumList(id, page = 1, limit = 10) {\n    if (page === 1) page = 0\n    return createMusicuFetch({\n      req: {\n        module: 'music.musichallAlbum.AlbumListServer',\n        method: 'GetAlbumList',\n        param: {\n          singerMid: id,\n          order: 0,\n          begin: page * limit,\n          num: limit,\n          songNumTag: 0,\n          singerID: 0,\n        },\n      },\n    }).then(body => {\n      if (body.req.code != 0) throw new Error('get singer album faild.')\n\n      const list = this.filterAlbumList(body.req.data.albumList)\n      return {\n        source: 'tx',\n        list,\n        limit,\n        page,\n        total: body.req.data.total,\n      }\n    })\n  },\n  /**\n   * 获取歌手歌曲列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  async getSongList(id, page = 1, limit = 100) {\n    if (page === 1) page = 0\n    return createMusicuFetch({\n      req: {\n        module: 'musichall.song_list_server',\n        method: 'GetSingerSongList',\n        param: {\n          singerMid: id,\n          order: 1,\n          begin: page * limit,\n          num: limit,\n        },\n      },\n    }).then(body => {\n      if (body.req.code != 0) throw new Error('get singer song list faild.')\n\n      const list = this.filterSongList(body.req.data.songList)\n      return {\n        source: 'tx',\n        list,\n        limit,\n        page,\n        total: body.req.data.totalNum,\n      }\n    })\n  },\n  filterAlbumList(raw) {\n    return raw.map(item => {\n      return {\n        id: item.albumID,\n        mid: item.albumMid,\n        count: item.totalNum,\n        info: {\n          name: item.albumName,\n          author: item.singerName,\n          img: `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.albumMid}.jpg`,\n          desc: null,\n        },\n      }\n    })\n  },\n  filterSongList(raw) {\n    raw.map(item => {\n      return filterMusicInfoItem(item.songInfo)\n    })\n  },\n}\n\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index'\nimport { formatSingerName } from '../utils'\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_hotTags: null,\n  _requestObj_list: null,\n  limit_list: 36,\n  limit_song: 100000,\n  successCode: 0,\n  sortList: [\n    {\n      name: '最热',\n      id: 5,\n    },\n    {\n      name: '最新',\n      id: 2,\n    },\n  ],\n  regExps: {\n    hotTagHtml: /class=\"c_bg_link js_tag_item\" data-id=\"\\w+\">.+?<\\/a>/g,\n    hotTag: /data-id=\"(\\w+)\">(.+?)<\\/a>/,\n\n    // https://y.qq.com/n/yqq/playlist/7217720898.html\n    // https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=9050006&id=7217720898&ADTAG=qfshare\n    listDetailLink: /\\/playlist\\/(\\d+)/,\n    listDetailLink2: /id=(\\d+)/,\n  },\n  tagsUrl: 'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=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',\n  hotTagUrl: 'https://c.y.qq.com/node/pc/wk_v15/category_playlist.html',\n  getListUrl(sortId, id, page) {\n    if (id) {\n      id = parseInt(id)\n      return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({\n        comm: { cv: 1602, ct: 20 },\n        playlist: {\n          method: 'get_category_content',\n          param: {\n            titleid: id,\n            caller: '0',\n            category_id: id,\n            size: this.limit_list,\n            page: page - 1,\n            use_page: 1,\n          },\n          module: 'playlist.PlayListCategoryServer',\n        },\n        }))}`\n    }\n    return `https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=json&inCharset=utf-8&outCharset=utf-8&notice=0&platform=wk_v15.json&needNewCode=0&data=${encodeURIComponent(JSON.stringify({\n          comm: { cv: 1602, ct: 20 },\n          playlist: {\n            method: 'get_playlist_by_tag',\n            param: { id: 10000000, sin: this.limit_list * (page - 1), size: this.limit_list, order: sortId, cur_page: page },\n            module: 'playlist.PlayListPlazaServer',\n          },\n      }))}`\n  },\n  getListDetailUrl(id) {\n    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&notice=0&platform=yqq.json&needNewCode=0`\n  },\n\n  // 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\n  // 获取标签\n  getTag(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch(this.tagsUrl)\n    return this._requestObj_tags.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getTag(++tryNum)\n      return this.filterTagInfo(body.tags.data.v_group)\n    })\n  },\n  // 获取标签\n  getHotTag(tryNum = 0) {\n    if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_hotTags = httpFetch(this.hotTagUrl)\n    return this._requestObj_hotTags.promise.then(({ statusCode, body }) => {\n      if (statusCode !== 200) return this.getHotTag(++tryNum)\n      return this.filterInfoHotTag(body)\n    })\n  },\n  filterInfoHotTag(html) {\n    let hotTag = html.match(this.regExps.hotTagHtml)\n    const hotTags = []\n    if (!hotTag) return hotTags\n\n    hotTag.forEach(tagHtml => {\n      let result = tagHtml.match(this.regExps.hotTag)\n      if (!result) return\n      hotTags.push({\n        id: parseInt(result[1]),\n        name: result[2],\n        source: 'tx',\n      })\n    })\n    return hotTags\n  },\n  filterTagInfo(rawList) {\n    return rawList.map(type => ({\n      name: type.group_name,\n      list: type.v_item.map(item => ({\n        parent_id: type.group_id,\n        parent_name: type.group_name,\n        id: item.id,\n        name: item.name,\n        source: 'tx',\n      })),\n    }))\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page, tryNum = 0) {\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_list = httpFetch(\n      this.getListUrl(sortId, tagId, page),\n    )\n    // console.log(this.getListUrl(sortId, tagId, page))\n    return this._requestObj_list.promise.then(({ body }) => {\n      if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)\n      return tagId ? this.filterList2(body.playlist.data, page) : this.filterList(body.playlist.data, page)\n    })\n  },\n\n  filterList(data, page) {\n    return {\n      list: data.v_playlist.map(item => ({\n        play_count: formatPlayCount(item.access_num),\n        id: String(item.tid),\n        author: item.creator_info.nick,\n        name: item.title,\n        time: item.modify_time ? dateFormat(item.modify_time * 1000, 'Y-M-D') : '',\n        img: item.cover_url_medium,\n        // grade: item.favorcnt / 10,\n        total: item.song_ids?.length,\n        desc: decodeName(item.desc).replace(/<br>/g, '\\n'),\n        source: 'tx',\n      })),\n      total: data.total,\n      page,\n      limit: this.limit_list,\n      source: 'tx',\n    }\n  },\n  filterList2({ content }, page) {\n    // console.log(content.v_item)\n    return {\n      list: content.v_item.map(({ basic }) => ({\n        play_count: formatPlayCount(basic.play_cnt),\n        id: String(basic.tid),\n        author: basic.creator.nick,\n        name: basic.title,\n        // time: basic.publish_time,\n        img: basic.cover.medium_url || basic.cover.default_url,\n        // grade: basic.favorcnt / 10,\n        desc: decodeName(basic.desc).replace(/<br>/g, '\\n'),\n        source: 'tx',\n      })),\n      total: content.total_cnt,\n      page,\n      limit: this.limit_list,\n      source: 'tx',\n    }\n  },\n\n  async handleParseId(link, retryNum = 0) {\n    if (retryNum > 2) return Promise.reject(new Error('link try max num'))\n\n    const requestObj_listDetailLink = httpFetch(link)\n    const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(headers)\n    if (statusCode > 400) return this.handleParseId(link, ++retryNum)\n    return location == null ? link : location\n  },\n\n  async getListId(id) {\n    if ((/[?&:/]/.test(id))) {\n      if (!this.regExps.listDetailLink.test(id)) {\n        id = await this.handleParseId(id)\n      }\n      let result = this.regExps.listDetailLink.exec(id)\n      if (!result) {\n        result = this.regExps.listDetailLink2.exec(id)\n        if (!result) throw new Error('failed')\n      }\n      id = result[1]\n      // console.log(id)\n    }\n    return id\n  },\n  // 获取歌曲列表内的音乐\n  async getListDetail(id, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n\n    id = await this.getListId(id)\n\n    const requestObj_listDetail = httpFetch(this.getListDetailUrl(id), {\n      headers: {\n        Origin: 'https://y.qq.com',\n        Referer: `https://y.qq.com/n/yqq/playsquare/${id}.html`,\n      },\n    })\n    const { body } = await requestObj_listDetail.promise\n\n    if (body.code !== this.successCode) return this.getListDetail(id, ++tryNum)\n    const cdlist = body.cdlist[0]\n    return {\n      list: this.filterListDetail(cdlist.songlist),\n      page: 1,\n      limit: cdlist.songlist.length + 1,\n      total: cdlist.songlist.length,\n      source: 'tx',\n      info: {\n        name: cdlist.dissname,\n        img: cdlist.logo,\n        desc: decodeName(cdlist.desc).replace(/<br>/g, '\\n'),\n        author: cdlist.nickname,\n        play_count: formatPlayCount(cdlist.visitnum),\n      },\n    }\n  },\n  filterListDetail(rawList) {\n    // console.log(rawList)\n    return rawList.map(item => {\n      let types = []\n      let _types = {}\n      if (item.file.size_128mp3 !== 0) {\n        let size = sizeFormate(item.file.size_128mp3)\n        types.push({ type: '128k', size })\n        _types['128k'] = {\n          size,\n        }\n      }\n      if (item.file.size_320mp3 !== 0) {\n        let size = sizeFormate(item.file.size_320mp3)\n        types.push({ type: '320k', size })\n        _types['320k'] = {\n          size,\n        }\n      }\n      if (item.file.size_flac !== 0) {\n        let size = sizeFormate(item.file.size_flac)\n        types.push({ type: 'flac', size })\n        _types.flac = {\n          size,\n        }\n      }\n      if (item.file.size_hires !== 0) {\n        let size = sizeFormate(item.file.size_hires)\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      // types.reverse()\n      return {\n        singer: formatSingerName(item.singer, 'name'),\n        name: item.title,\n        albumName: item.album.name,\n        albumId: item.album.mid,\n        source: 'tx',\n        interval: formatPlayTime(item.interval),\n        songId: item.id,\n        albumMid: item.album.mid,\n        strMediaMid: item.file.media_mid,\n        songmid: item.mid,\n        img: (item.album.name === '' || item.album.name === '空')\n          ? item.singer?.length ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` : ''\n          : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  getTags() {\n    return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'tx' }))\n  },\n\n  async getDetailPageUrl(id) {\n    id = await this.getListId(id)\n\n    return `https://y.qq.com/n/ryqq/playlist/${id}`\n  },\n\n  search(text, page, limit = 20, retryNum = 0) {\n    if (retryNum > 5) throw new Error('max retry')\n    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`, {\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',\n        Referer: 'http://y.qq.com/portal/search.html',\n      },\n    })\n      .promise.then(({ body }) => {\n        if (body.code != 0) return this.search(text, page, limit, ++retryNum)\n        // console.log(body.data.list)\n        return {\n          list: body.data.list.map(item => {\n            return {\n              play_count: formatPlayCount(item.listennum),\n              id: String(item.dissid),\n              author: decodeName(item.creator.name),\n              name: decodeName(item.dissname),\n              time: dateFormat(item.createtime, 'Y-M-D'),\n              img: item.imgurl,\n              // grade: item.favorcnt / 10,\n              total: item.song_count,\n              desc: decodeName(decodeName(item.introduction)).replace(/<br>/g, '\\n'),\n              source: 'tx',\n            }\n          }),\n          limit,\n          total: body.data.sum,\n          source: 'tx',\n        }\n      })\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/tx/tipSearch.js",
    "content": "import { httpFetch } from '../../request'\n\nexport default {\n  // regExps: {\n  //   relWord: /RELWORD=(.+)/,\n  // },\n  requestObj: null,\n  tipSearch(str) {\n    this.cancelTipSearch()\n    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&notice=0&platform=yqq&needNewCode=0`, {\n      headers: {\n        Referer: 'https://y.qq.com/portal/player.html',\n      },\n    })\n    return this.requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode != 200 || body.code != 0) return Promise.reject(new Error('请求失败'))\n      return body.data\n    })\n  },\n  handleResult(rawData) {\n    return rawData.map(info => `${info.name} - ${info.singer}`)\n  },\n  cancelTipSearch() {\n    if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()\n  },\n  async search(str) {\n    return this.tipSearch(str).then(result => this.handleResult(result.song.itemlist))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/utils.js",
    "content": "import crypto from 'crypto'\nimport dns from 'dns'\nimport { decodeName } from '@renderer/utils'\n\nexport const toMD5 = str => crypto.createHash('md5').update(str).digest('hex')\n\n\nconst ipMap = new Map()\nexport const getHostIp = hostname => {\n  const result = ipMap.get(hostname)\n  if (typeof result === 'object') return result\n  if (result === true) return\n  ipMap.set(hostname, true)\n  // console.log(hostname)\n  dns.lookup(hostname, {\n    // family: 4,\n    all: false,\n  }, (err, address, family) => {\n    if (err) return console.log(err)\n    // console.log(address, family)\n    ipMap.set(hostname, { address, family })\n  })\n}\n\nexport const dnsLookup = (hostname, options, callback) => {\n  const result = getHostIp(hostname)\n  if (result) return callback(null, result.address, result.family)\n\n  dns.lookup(hostname, options, callback)\n}\n\n\n/**\n * 格式化歌手\n * @param singers 歌手数组\n * @param nameKey 歌手名键值\n * @param join 歌手分割字符\n */\nexport const formatSingerName = (singers, nameKey = 'name', join = '、') => {\n  if (Array.isArray(singers)) {\n    const singer = []\n    singers.forEach(item => {\n      let name = item[nameKey]\n      if (!name) return\n      singer.push(name)\n    })\n    return decodeName(singer.join(join))\n  }\n  return decodeName(String(singers ?? ''))\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/api-test.js",
    "content": "import { httpFetch } from '../../request'\nimport { requestMsg } from '../../message'\nimport { headers, timeout } from '../options'\nimport { dnsLookup } from '../utils'\n\nconst api_test = {\n  getMusicUrl(songInfo, type) {\n    const requestObj = httpFetch(`http://ts.tempmusics.tk/url/wy/${songInfo.songmid}/${type}`, {\n      method: 'get',\n      timeout,\n      headers,\n      lookup: dnsLookup,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode == 429) return Promise.reject(new Error(requestMsg.tooManyRequests))\n      switch (body.code) {\n        case 0: return Promise.resolve({ type, url: body.data })\n        default: return Promise.reject(new Error(requestMsg.fail))\n      }\n    })\n    return requestObj\n  },\n/*   getPic(songInfo) {\n    const requestObj = httpFetch(`http://localhost:3100/pic/wy/${songInfo.songmid}`, {\n      method: 'get',\n      timeout,\n      headers,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))\n    })\n    return requestObj\n  },\n  getLyric(songInfo) {\n    const requestObj = httpFetch(`http://localhost:3100/lrc/wy/${songInfo.songmid}`, {\n      method: 'get',\n      timeout,\n      headers,\n      family: 4,\n    })\n    requestObj.promise = requestObj.promise.then(({ body }) => {\n      return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail))\n    })\n    return requestObj\n  }, */\n}\n\nexport default api_test\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/comment.js",
    "content": "import { httpFetch } from '../../request'\nimport { weapi } from './utils/crypto'\nimport { dateFormat2 } from '../../index'\n\nconst emojis = [\n  ['大笑', '😃'],\n  ['可爱', '😊'],\n  ['憨笑', '☺️'],\n  ['色', '😍'],\n  ['亲亲', '😙'],\n  ['惊恐', '😱'],\n  ['流泪', '😭'],\n  ['亲', '😚'],\n  ['呆', '😳'],\n  ['哀伤', '😔'],\n  ['呲牙', '😁'],\n  ['吐舌', '😝'],\n  ['撇嘴', '😒'],\n  ['怒', '😡'],\n  ['奸笑', '😏'],\n  ['汗', '😓'],\n  ['痛苦', '😖'],\n  ['惶恐', '😰'],\n  ['生病', '😨'],\n  ['口罩', '😷'],\n  ['大哭', '😂'],\n  ['晕', '😵'],\n  ['发怒', '👿'],\n  ['开心', '😄'],\n  ['鬼脸', '😜'],\n  ['皱眉', '😞'],\n  ['流感', '😢'],\n  ['爱心', '❤️'],\n  ['心碎', '💔'],\n  ['钟情', '💘'],\n  ['星星', '⭐️'],\n  ['生气', '💢'],\n  ['便便', '💩'],\n  ['强', '👍'],\n  ['弱', '👎'],\n  ['拜', '🙏'],\n  ['牵手', '👫'],\n  ['跳舞', '👯‍♀️'],\n  ['禁止', '🙅‍♀️'],\n  ['这边', '💁‍♀️'],\n  ['爱意', '💏'],\n  ['示爱', '👩‍❤️‍👨'],\n  ['嘴唇', '👄'],\n  ['狗', '🐶'],\n  ['猫', '🐱'],\n  ['猪', '🐷'],\n  ['兔子', '🐰'],\n  ['小鸡', '🐤'],\n  ['公鸡', '🐔'],\n  ['幽灵', '👻'],\n  ['圣诞', '🎅'],\n  ['外星', '👽'],\n  ['钻石', '💎'],\n  ['礼物', '🎁'],\n  ['男孩', '👦'],\n  ['女孩', '👧'],\n  ['蛋糕', '🎂'],\n  ['18', '🔞'],\n  ['圈', '⭕'],\n  ['叉', '❌'],\n]\n\nconst applyEmoji = text => {\n  for (const e of emojis) text = text.replaceAll(`[${e[0]}]`, e[1])\n  return text\n}\n\nlet cursorTools = {\n  cache: {},\n  getCursor(id, page, limit) {\n    let cacheData = this.cache[id]\n    if (!cacheData) cacheData = this.cache[id] = {}\n    let orderType\n    let cursor\n    let offset\n    if (page == 1) {\n      cacheData.page = 1\n      cursor = cacheData.cursor = cacheData.prevCursor = Date.now()\n      orderType = 1\n      offset = 0\n    } else if (cacheData.page) {\n      cursor = cacheData.cursor\n      if (page > cacheData.page) {\n        orderType = 1\n        offset = (page - cacheData.page - 1) * limit\n      } else if (page < cacheData.page) {\n        orderType = 0\n        offset = (cacheData.page - page - 1) * limit\n      } else {\n        cursor = cacheData.cursor = cacheData.prevCursor\n        offset = cacheData.offset\n        orderType = cacheData.orderType\n      }\n    }\n    return {\n      orderType,\n      cursor,\n      offset,\n    }\n  },\n  setCursor(id, cursor, orderType, offset, page) {\n    let cacheData = this.cache[id]\n    if (!cacheData) cacheData = this.cache[id] = {}\n    cacheData.prevCursor = cacheData.cursor\n    cacheData.cursor = cursor\n    cacheData.orderType = orderType\n    cacheData.offset = offset\n    cacheData.page = page\n  },\n}\n\nexport default {\n  _requestObj: null,\n  _requestObj2: null,\n  async getComment({ songmid }, page = 1, limit = 20) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n\n    const id = 'R_SO_4_' + songmid\n\n    const cursorInfo = cursorTools.getCursor(songmid, page, limit)\n\n    const _requestObj = httpFetch('https://music.163.com/weapi/comment/resource/comments/get', {\n      method: 'post',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n        origin: 'https://music.163.com',\n        Refere: 'http://music.163.com/',\n      },\n      form: weapi({\n        cursor: cursorInfo.cursor,\n        offset: cursorInfo.offset,\n        orderType: cursorInfo.orderType,\n        pageNo: page,\n        pageSize: limit,\n        rid: id,\n        threadId: id,\n      }),\n    })\n    const { body, statusCode } = await _requestObj.promise\n    // console.log(body)\n    if (statusCode != 200 || body.code !== 200) throw new Error('获取评论失败')\n    cursorTools.setCursor(songmid, body.data.cursor, cursorInfo.orderType, cursorInfo.offset, page)\n    return { source: 'wy', comments: this.filterComment(body.data.comments), total: body.data.totalCount, page, limit, maxPage: Math.ceil(body.data.totalCount / limit) || 1 }\n  },\n  async getHotComment({ songmid }, page = 1, limit = 100) {\n    if (this._requestObj2) this._requestObj2.cancelHttp()\n\n    const id = 'R_SO_4_' + songmid\n    page = page - 1\n\n    const _requestObj2 = httpFetch(`https://music.163.com/weapi/v1/resource/hotcomments/${id}`, {\n      method: 'post',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n        origin: 'https://music.163.com',\n        Refere: 'http://music.163.com/',\n      },\n      form: weapi({\n        rid: id,\n        limit,\n        offset: limit * page,\n        beforeTime: Date.now().toString(),\n      }),\n    })\n    const { body, statusCode } = await _requestObj2.promise\n    if (statusCode != 200 || body.code !== 200) throw new Error('获取热门评论失败')\n    const total = body.total ?? 0\n    return { source: 'wy', comments: this.filterComment(body.hotComments), total, page, limit, maxPage: Math.ceil(total / limit) || 1 }\n  },\n  filterComment(rawList) {\n    return rawList.map(item => {\n      let data = {\n        id: item.commentId,\n        text: item.content ? applyEmoji(item.content) : '',\n        time: item.time ? item.time : '',\n        timeStr: item.time ? dateFormat2(item.time) : '',\n        location: item.ipLocation?.location,\n        userName: item.user.nickname,\n        avatar: item.user.avatarUrl,\n        userId: item.user.userId,\n        likedCount: item.likedCount,\n        reply: [],\n      }\n\n      let replyData = item.beReplied && item.beReplied[0]\n      return replyData\n        ? {\n            id: item.commentId,\n            rootId: replyData.beRepliedCommentId,\n            text: replyData.content ? applyEmoji(replyData.content) : '',\n            time: item.time,\n            timeStr: null,\n            location: replyData.ipLocation?.location,\n            userName: replyData.user.nickname,\n            avatar: replyData.user.avatarUrl,\n            userId: replyData.user.userId,\n            likedCount: null,\n            reply: [data],\n          }\n        : data\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/hotSearch.js",
    "content": "import { eapiRequest } from './utils/index'\n\nexport default {\n  _requestObj: null,\n  async getList(retryNum = 0) {\n    if (this._requestObj) this._requestObj.cancelHttp()\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const _requestObj = eapiRequest('/api/search/chart/detail', {\n      id: 'HOT_SEARCH_SONG#@#',\n    })\n    const { body, statusCode } = await _requestObj.promise\n    if (statusCode != 200 || body.code !== 200) throw new Error('获取热搜词失败')\n\n    return { source: 'wy', list: this.filterList(body.data.itemList) }\n  },\n  filterList(rawList) {\n    return rawList.map(item => item.searchWord)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/index.js",
    "content": "import leaderboard from './leaderboard'\nimport { apis } from '../api-source'\nimport getLyric from './lyric'\nimport getMusicInfo from './musicInfo'\nimport musicSearch from './musicSearch'\nimport songList from './songList'\nimport hotSearch from './hotSearch'\nimport comment from './comment'\n// import tipSearch from './tipSearch'\n\nconst wy = {\n  // tipSearch,\n  leaderboard,\n  musicSearch,\n  songList,\n  hotSearch,\n  comment,\n  getMusicUrl(songInfo, type) {\n    return apis('wy').getMusicUrl(songInfo, type)\n  },\n  getLyric(songInfo) {\n    return getLyric(songInfo.songmid)\n  },\n  getPic(songInfo) {\n    const requestObj = getMusicInfo(songInfo.songmid)\n    return requestObj.promise.then(info => info.al.picUrl)\n  },\n  getMusicDetailPageUrl(songInfo) {\n    return `https://music.163.com/#/song?id=${songInfo.songmid}`\n  },\n}\n\nexport default wy\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/leaderboard.js",
    "content": "import { weapi } from './utils/crypto'\nimport { httpFetch } from '../../request'\nimport musicDetailApi from './musicDetail'\n\nconst topList = [{ id: 'wy__19723756', name: '飙升榜', bangid: '19723756' },\n  { id: 'wy__3779629', name: '新歌榜', bangid: '3779629' },\n  { id: 'wy__2884035', name: '原创榜', bangid: '2884035' },\n  { id: 'wy__3778678', name: '热歌榜', bangid: '3778678' },\n  { id: 'wy__991319590', name: '说唱榜', bangid: '991319590' },\n  { id: 'wy__71384707', name: '古典榜', bangid: '71384707' },\n  { id: 'wy__1978921795', name: '电音榜', bangid: '1978921795' },\n  { id: 'wy__5453912201', name: '黑胶VIP爱听榜', bangid: '5453912201' },\n  { id: 'wy__71385702', name: 'ACG榜', bangid: '71385702' },\n  { id: 'wy__745956260', name: '韩语榜', bangid: '745956260' },\n  { id: 'wy__10520166', name: '国电榜', bangid: '10520166' },\n  { id: 'wy__180106', name: 'UK排行榜周榜', bangid: '180106' },\n  { id: 'wy__60198', name: '美国Billboard榜', bangid: '60198' },\n  { id: 'wy__3812895', name: 'Beatport全球电子舞曲榜', bangid: '3812895' },\n  { id: 'wy__21845217', name: 'KTV唛榜', bangid: '21845217' },\n  { id: 'wy__60131', name: '日本Oricon榜', bangid: '60131' },\n  { id: 'wy__2809513713', name: '欧美热歌榜', bangid: '2809513713' },\n  { id: 'wy__2809577409', name: '欧美新歌榜', bangid: '2809577409' },\n  { id: 'wy__27135204', name: '法国 NRJ Vos Hits 周榜', bangid: '27135204' },\n  { id: 'wy__3001835560', name: 'ACG动画榜', bangid: '3001835560' },\n  { id: 'wy__3001795926', name: 'ACG游戏榜', bangid: '3001795926' },\n  { id: 'wy__3001890046', name: 'ACG VOCALOID榜', bangid: '3001890046' },\n  { id: 'wy__3112516681', name: '中国新乡村音乐排行榜', bangid: '3112516681' },\n  { id: 'wy__5059644681', name: '日语榜', bangid: '5059644681' },\n  { id: 'wy__5059633707', name: '摇滚榜', bangid: '5059633707' },\n  { id: 'wy__5059642708', name: '国风榜', bangid: '5059642708' },\n  { id: 'wy__5338990334', name: '潜力爆款榜', bangid: '5338990334' },\n  { id: 'wy__5059661515', name: '民谣榜', bangid: '5059661515' },\n  { id: 'wy__6688069460', name: '听歌识曲榜', bangid: '6688069460' },\n  { id: 'wy__6723173524', name: '网络热歌榜', bangid: '6723173524' },\n  { id: 'wy__6732051320', name: '俄语榜', bangid: '6732051320' },\n  { id: 'wy__6732014811', name: '越南语榜', bangid: '6732014811' },\n  { id: 'wy__6886768100', name: '中文DJ榜', bangid: '6886768100' },\n  { id: 'wy__6939992364', name: '俄罗斯top hit流行音乐榜', bangid: '6939992364' },\n  { id: 'wy__7095271308', name: '泰语榜', bangid: '7095271308' },\n  { id: 'wy__7356827205', name: 'BEAT排行榜', bangid: '7356827205' },\n  { id: 'wy__7325478166', name: '编辑推荐榜VOL.44 天才女子摇滚乐队boygenius剖白卑微心迹', bangid: '7325478166' },\n  { id: 'wy__7603212484', name: 'LOOK直播歌曲榜', bangid: '7603212484' },\n  { id: 'wy__7775163417', name: '赏音榜', bangid: '7775163417' },\n  { id: 'wy__7785123708', name: '黑胶VIP新歌榜', bangid: '7785123708' },\n  { id: 'wy__7785066739', name: '黑胶VIP热歌榜', bangid: '7785066739' },\n  { id: 'wy__7785091694', name: '黑胶VIP爱搜榜', bangid: '7785091694' },\n]\n\nexport default {\n  limit: 100000,\n  list: [\n    {\n      id: 'wybsb',\n      name: '飙升榜',\n      bangid: '19723756',\n    },\n    {\n      id: 'wyrgb',\n      name: '热歌榜',\n      bangid: '3778678',\n    },\n    {\n      id: 'wyxgb',\n      name: '新歌榜',\n      bangid: '3779629',\n    },\n    {\n      id: 'wyycb',\n      name: '原创榜',\n      bangid: '2884035',\n    },\n    {\n      id: 'wygdb',\n      name: '古典榜',\n      bangid: '71384707',\n    },\n    {\n      id: 'wydouyb',\n      name: '抖音榜',\n      bangid: '2250011882',\n    },\n    {\n      id: 'wyhyb',\n      name: '韩语榜',\n      bangid: '745956260',\n    },\n    {\n      id: 'wydianyb',\n      name: '电音榜',\n      bangid: '1978921795',\n    },\n    {\n      id: 'wydjb',\n      name: '电竞榜',\n      bangid: '2006508653',\n    },\n    {\n      id: 'wyktvbb',\n      name: 'KTV唛榜',\n      bangid: '21845217',\n    },\n  ],\n  getUrl(id) {\n    return `https://music.163.com/discover/toplist?id=${id}`\n  },\n  regExps: {\n    list: /<textarea id=\"song-list-pre-data\" style=\"display:none;\">(.+?)<\\/textarea>/,\n  },\n  _requestBoardsObj: null,\n  getBoardsData() {\n    if (this._requestBoardsObj) this._requestBoardsObj.cancelHttp()\n    this._requestBoardsObj = httpFetch('https://music.163.com/weapi/toplist', {\n      method: 'post',\n      form: weapi({}),\n    })\n    return this._requestBoardsObj.promise\n  },\n  getData(id) {\n    const requestBoardsDetailObj = httpFetch('https://music.163.com/weapi/v3/playlist/detail', {\n      method: 'post',\n      form: weapi({\n        id,\n        n: 100000,\n        p: 1,\n      }),\n    })\n    return requestBoardsDetailObj.promise\n  },\n\n  filterBoardsData(rawList) {\n    // console.log(rawList)\n    let list = []\n    for (const board of rawList) {\n      // 排除 MV榜\n      // if (board.id == 201) continue\n      list.push({\n        id: 'wy__' + board.id,\n        name: board.name,\n        bangid: String(board.id),\n      })\n    }\n    return list\n  },\n  async getBoards(retryNum = 0) {\n    // if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    // let response\n    // try {\n    //   response = await this.getBoardsData()\n    // } catch (error) {\n    //   return this.getBoards(retryNum)\n    // }\n    // console.log(response.body)\n    // if (response.statusCode !== 200 || response.body.code !== 200) return this.getBoards(retryNum)\n    // const list = this.filterBoardsData(response.body.list)\n    // console.log(list)\n    // console.log(JSON.stringify(list))\n    // this.list = list\n    // return {\n    //   list,\n    //   source: 'wy',\n    // }\n    this.list = topList\n    return {\n      list: topList,\n      source: 'wy',\n    }\n  },\n  async getList(bangid, page, retryNum = 0) {\n    if (++retryNum > 6) return Promise.reject(new Error('try max num'))\n    // console.log(bangid)\n    let resp\n    try {\n      resp = await this.getData(bangid)\n    } catch (err) {\n      if (err.message == 'try max num') {\n        throw err\n      } else {\n        return this.getList(bangid, page, retryNum)\n      }\n    }\n    if (resp.statusCode !== 200 || resp.body.code !== 200) return this.getList(bangid, page, retryNum)\n    // console.log(resp.body)\n    let musicDetail\n    try {\n      musicDetail = await musicDetailApi.getList(resp.body.playlist.trackIds.map(trackId => trackId.id))\n    } catch (err) {\n      console.log(err)\n      if (err.message == 'try max num') {\n        throw err\n      } else {\n        return this.getList(bangid, page, retryNum)\n      }\n    }\n    // console.log(musicDetail)\n    return {\n      total: musicDetail.list.length,\n      list: musicDetail.list,\n      limit: this.limit,\n      page,\n      source: 'wy',\n    }\n  },\n\n  getDetailPageUrl(id) {\n    if (typeof id == 'string') id = id.replace('wy__', '')\n    return `https://music.163.com/#/discover/toplist?id=${id}`\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { eapi } from './utils/crypto'\n// import { decodeName } from '../..'\n\n// const parseLyric = (str, lrc) => {\n//   if (!str) return ''\n\n//   str = str.replace(/\\r/g, '')\n\n//   let lxlyric = str.replace(/\\[((\\d+),\\d+)\\].*/g, str => {\n//     let result = str.match(/\\[((\\d+),\\d+)\\].*/)\n//     let time = parseInt(result[2])\n//     let ms = time % 1000\n//     time /= 1000\n//     let m = parseInt(time / 60).toString().padStart(2, '0')\n//     time %= 60\n//     let s = parseInt(time).toString().padStart(2, '0')\n//     time = `${m}:${s}.${ms}`\n//     str = str.replace(result[1], time)\n\n//     let startTime = 0\n//     str = str.replace(/\\(0,1\\) /g, ' ').replace(/\\(\\d+,\\d+\\)/g, time => {\n//       const [start, end] = time.replace(/^\\((\\d+,\\d+)\\)$/, '$1').split(',')\n\n//       time = `<${parseInt(startTime + parseInt(start))},${end}>`\n//       startTime = parseInt(startTime + parseInt(end))\n//       return time\n//     })\n\n//     return str\n//   })\n\n//   lxlyric = decodeName(lxlyric)\n//   return lxlyric.trim()\n// }\n\nconst eapiRequest = (url, data) => {\n  return httpFetch('https://interface3.music.163.com/eapi/song/lyric/v1', {\n    method: 'post',\n    headers: {\n      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n      origin: 'https://music.163.com',\n      // cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',\n    },\n    form: eapi(url, data),\n  })\n  // requestObj.promise = requestObj.promise.then(({ body }) => {\n  //   // console.log(raw)\n  //   console.log(body)\n  //   // console.log(eapiDecrypt(raw))\n  //   // return eapiDecrypt(raw)\n  //   return body\n  // })\n  // return requestObj\n}\n\nconst parseTools = {\n  rxps: {\n    info: /^{\"/,\n    lineTime: /^\\[(\\d+),\\d+\\]/,\n    wordTime: /\\(\\d+,\\d+,\\d+\\)/,\n    wordTimeAll: /(\\(\\d+,\\d+,\\d+\\))/g,\n  },\n  msFormat(timeMs) {\n    if (Number.isNaN(timeMs)) return ''\n    let ms = timeMs % 1000\n    timeMs /= 1000\n    let m = parseInt(timeMs / 60).toString().padStart(2, '0')\n    timeMs %= 60\n    let s = parseInt(timeMs).toString().padStart(2, '0')\n    return `[${m}:${s}.${ms}]`\n  },\n  parseLyric(lines) {\n    const lxlrcLines = []\n    const lrcLines = []\n\n    for (let line of lines) {\n      line = line.trim()\n      let result = this.rxps.lineTime.exec(line)\n      if (!result) {\n        if (line.startsWith('[offset')) {\n          lxlrcLines.push(line)\n          lrcLines.push(line)\n        }\n        continue\n      }\n\n      const startMsTime = parseInt(result[1])\n      const startTimeStr = this.msFormat(startMsTime)\n      if (!startTimeStr) continue\n\n      let words = line.replace(this.rxps.lineTime, '')\n\n      lrcLines.push(`${startTimeStr}${words.replace(this.rxps.wordTimeAll, '')}`)\n\n      let times = words.match(this.rxps.wordTimeAll)\n      if (!times) continue\n      times = times.map(time => {\n        const result = /\\((\\d+),(\\d+),\\d+\\)/.exec(time)\n        return `<${Math.max(parseInt(result[1]) - startMsTime, 0)},${result[2]}>`\n      })\n      const wordArr = words.split(this.rxps.wordTime)\n      wordArr.shift()\n      const newWords = times.map((time, index) => `${time}${wordArr[index]}`).join('')\n      lxlrcLines.push(`${startTimeStr}${newWords}`)\n    }\n    return {\n      lyric: lrcLines.join('\\n'),\n      lxlyric: lxlrcLines.join('\\n'),\n    }\n  },\n  parseHeaderInfo(str) {\n    str = str.trim()\n    str = str.replace(/\\r/g, '')\n    if (!str) return null\n    const lines = str.split('\\n')\n    return lines.map(line => {\n      if (!this.rxps.info.test(line)) return line\n      try {\n        const info = JSON.parse(line)\n        const timeTag = this.msFormat(info.t)\n        return timeTag ? `${timeTag}${info.c.map(t => t.tx).join('')}` : ''\n      } catch {\n        return ''\n      }\n    })\n  },\n  getIntv(interval) {\n    if (!interval) return 0\n    if (!interval.includes('.')) interval += '.0'\n    let arr = interval.split(/:|\\./)\n    while (arr.length < 3) arr.unshift('0')\n    const [m, s, ms] = arr\n    return parseInt(m) * 3600000 + parseInt(s) * 1000 + parseInt(ms)\n  },\n  fixTimeTag(lrc, targetlrc) {\n    let lrcLines = lrc.split('\\n')\n    const targetlrcLines = targetlrc.split('\\n')\n    const timeRxp = /^\\[([\\d:.]+)\\]/\n    let temp = []\n    let newLrc = []\n    targetlrcLines.forEach((line) => {\n      const result = timeRxp.exec(line)\n      if (!result) return\n      const words = line.replace(timeRxp, '')\n      if (!words.trim()) return\n      const t1 = this.getIntv(result[1])\n\n      while (lrcLines.length) {\n        const lrcLine = lrcLines.shift()\n        const lrcLineResult = timeRxp.exec(lrcLine)\n        if (!lrcLineResult) continue\n        const t2 = this.getIntv(lrcLineResult[1])\n        if (Math.abs(t1 - t2) < 100) {\n          const lrc = line.replace(timeRxp, lrcLineResult[0]).trim()\n          if (!lrc) continue\n          newLrc.push(lrc)\n          break\n        }\n        temp.push(lrcLine)\n      }\n      lrcLines = [...temp, ...lrcLines]\n      temp = []\n    })\n    return newLrc.join('\\n')\n  },\n  parse(ylrc, ytlrc, yrlrc, lrc, tlrc, rlrc) {\n    const info = {\n      lyric: '',\n      tlyric: '',\n      rlyric: '',\n      lxlyric: '',\n    }\n    if (ylrc) {\n      let lines = this.parseHeaderInfo(ylrc)\n      if (lines) {\n        const result = this.parseLyric(lines)\n        if (ytlrc) {\n          const lines = this.parseHeaderInfo(ytlrc)\n          if (lines) {\n            // if (lines.length == result.lyricLines.length) {\n            info.tlyric = this.fixTimeTag(result.lyric, lines.join('\\n'))\n            // } else info.tlyric = lines.join('\\n')\n          }\n        }\n        if (yrlrc) {\n          const lines = this.parseHeaderInfo(yrlrc)\n          if (lines) {\n            // if (lines.length == result.lyricLines.length) {\n            info.rlyric = this.fixTimeTag(result.lyric, lines.join('\\n'))\n            // } else info.rlyric = lines.join('\\n')\n          }\n        }\n\n        const timeRxp = /^\\[[\\d:.]+\\]/\n        const headers = lines.filter(l => timeRxp.test(l)).join('\\n')\n        info.lyric = `${headers}\\n${result.lyric}`\n        info.lxlyric = result.lxlyric\n        return info\n      }\n    }\n    if (lrc) {\n      const lines = this.parseHeaderInfo(lrc)\n      if (lines) info.lyric = lines.join('\\n')\n    }\n    if (tlrc) {\n      const lines = this.parseHeaderInfo(tlrc)\n      if (lines) info.tlyric = lines.join('\\n')\n    }\n    if (rlrc) {\n      const lines = this.parseHeaderInfo(rlrc)\n      if (lines) info.rlyric = lines.join('\\n')\n    }\n\n    return info\n  },\n}\n\n\n// https://github.com/Binaryify/NeteaseCloudMusicApi/pull/1523/files\n// export default songmid => {\n//   const requestObj = httpFetch('https://music.163.com/api/linux/forward', {\n//     method: 'post',\n//     'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n//     form: linuxapi({\n//       method: 'POST',\n//       url: 'https://music.163.com/api/song/lyric?_nmclfl=1',\n//       params: {\n//         id: songmid,\n//         tv: -1,\n//         lv: -1,\n//         rv: -1,\n//         kv: -1,\n//       },\n//     }),\n//   })\n//   requestObj.promise = requestObj.promise.then(({ body }) => {\n//     if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))\n//     // console.log(body)\n//     return {\n//       lyric: body.lrc.lyric,\n//       tlyric: body.tlyric?.lyric ?? '',\n//       rlyric: body.romalrc?.lyric ?? '',\n//       // lxlyric: parseLyric(body.klyric.lyric),\n//     }\n//   })\n//   return requestObj\n// }\n\n// https://github.com/lyswhut/lx-music-mobile/issues/370\nconst fixTimeLabel = (lrc, tlrc, romalrc) => {\n  if (lrc) {\n    let newLrc = lrc.replace(/\\[(\\d{2}:\\d{2}):(\\d{2})]/g, '[$1.$2]')\n    let newTlrc = tlrc?.replace(/\\[(\\d{2}:\\d{2}):(\\d{2})]/g, '[$1.$2]') ?? tlrc\n    if (newLrc != lrc || newTlrc != tlrc) {\n      lrc = newLrc\n      tlrc = newTlrc\n      if (romalrc) romalrc = romalrc.replace(/\\[(\\d{2}:\\d{2}):(\\d{2,3})]/g, '[$1.$2]').replace(/\\[(\\d{2}:\\d{2}\\.\\d{2})0]/g, '[$1]')\n    }\n  }\n\n  return { lrc, tlrc, romalrc }\n}\n\n// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/lyric_new.js\nexport default songmid => {\n  const requestObj = eapiRequest('/api/song/lyric/v1', {\n    id: songmid,\n    cp: false,\n    tv: 0,\n    lv: 0,\n    rv: 0,\n    kv: 0,\n    yv: 0,\n    ytv: 0,\n    yrv: 0,\n  })\n  requestObj.promise = requestObj.promise.then(({ body }) => {\n    // console.log(body)\n    if (body.code !== 200 || !body?.lrc?.lyric) return Promise.reject(new Error('Get lyric failed'))\n    const fixTimeLabelLrc = fixTimeLabel(body.lrc.lyric, body.tlyric?.lyric, body.romalrc?.lyric)\n    const info = parseTools.parse(body.yrc?.lyric, body.ytlrc?.lyric, body.yromalrc?.lyric, fixTimeLabelLrc.lrc, fixTimeLabelLrc.tlrc, fixTimeLabelLrc.romalrc)\n    // console.log(info)\n    if (!info.lyric) return Promise.reject(new Error('Get lyric failed'))\n    return info\n  })\n  return requestObj\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/musicDetail.js",
    "content": "import { httpFetch } from '../../request'\nimport { weapi } from './utils/crypto'\nimport { formatPlayTime, sizeFormate } from '../../index'\n// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js\n\nexport default {\n  getSinger(singers) {\n    let arr = []\n    singers?.forEach(singer => {\n      arr.push(singer.name)\n    })\n    return arr.join('、')\n  },\n  filterList({ songs, privileges }) {\n    // console.log(songs, privileges)\n    const list = []\n    songs.forEach((item, index) => {\n      const types = []\n      const _types = {}\n      let size\n      let privilege = privileges[index]\n      if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)\n      if (!privilege) return\n\n      if (privilege.maxBrLevel == 'hires') {\n        size = item.hr ? sizeFormate(item.hr.size) : null\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      switch (privilege.maxbr) {\n        case 999000:\n          size = item.sq ? sizeFormate(item.sq.size) : null\n          types.push({ type: 'flac', size })\n          _types.flac = {\n            size,\n          }\n        case 320000:\n          size = item.h ? sizeFormate(item.h.size) : null\n          types.push({ type: '320k', size })\n          _types['320k'] = {\n            size,\n          }\n        case 192000:\n        case 128000:\n          size = item.l ? sizeFormate(item.l.size) : null\n          types.push({ type: '128k', size })\n          _types['128k'] = {\n            size,\n          }\n      }\n\n      types.reverse()\n\n      if (item.pc) {\n        list.push({\n          singer: item.pc.ar ?? '',\n          name: item.pc.sn ?? '',\n          albumName: item.pc.alb ?? '',\n          albumId: item.al?.id,\n          source: 'wy',\n          interval: formatPlayTime(item.dt / 1000),\n          songmid: item.id,\n          img: item.al?.picUrl ?? '',\n          lrc: null,\n          otherSource: null,\n          types,\n          _types,\n          typeUrl: {},\n        })\n      } else {\n        list.push({\n          singer: this.getSinger(item.ar),\n          name: item.name ?? '',\n          albumName: item.al?.name,\n          albumId: item.al?.id,\n          source: 'wy',\n          interval: formatPlayTime(item.dt / 1000),\n          songmid: item.id,\n          img: item.al?.picUrl,\n          lrc: null,\n          otherSource: null,\n          types,\n          _types,\n          typeUrl: {},\n        })\n      }\n    })\n    // console.log(list)\n    return list\n  },\n  async getList(ids = [], retryNum = 0) {\n    if (retryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {\n      method: 'post',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n        origin: 'https://music.163.com',\n      },\n      form: weapi({\n        c: '[' + ids.map(id => ('{\"id\":' + id + '}')).join(',') + ']',\n        ids: '[' + ids.join(',') + ']',\n      }),\n    })\n    const { body, statusCode } = await requestObj.promise\n    if (statusCode != 200 || body.code !== 200) throw new Error('获取歌曲详情失败')\n    // console.log(body)\n    return { source: 'wy', list: this.filterList(body) }\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/musicInfo.js",
    "content": "// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js\nimport { httpFetch } from '../../request'\nimport { weapi } from './utils/crypto'\n\nexport default songmid => {\n  const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', {\n    method: 'post',\n    headers: {\n      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n      Referer: 'https://music.163.com/song?id=' + songmid,\n      origin: 'https://music.163.com',\n    },\n    form: weapi({\n      c: `[{\"id\":${songmid}}]`,\n      ids: `[${songmid}]`,\n    }),\n  })\n  requestObj.promise = requestObj.promise.then(({ body }) => {\n    // console.log(body)\n    if (body.code !== 200 || !body.songs.length) return Promise.reject(new Error('获取歌曲信息失败'))\n    return body.songs[0]\n  })\n  return requestObj\n}\n\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/musicSearch.js",
    "content": "// import { httpFetch } from '../../request'\n// import { weapi } from './utils/crypto'\nimport { sizeFormate, formatPlayTime } from '../../index'\n// import musicDetailApi from './musicDetail'\nimport { eapiRequest } from './utils/index'\n\nexport default {\n  limit: 30,\n  total: 0,\n  page: 0,\n  allPage: 1,\n  musicSearch(str, page, limit) {\n    // const searchRequest = eapiRequest('/api/cloudsearch/pc', {\n    //   s: str,\n    //   type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频\n    //   limit,\n    //   total: page == 1,\n    //   offset: limit * (page - 1),\n    // })\n    const searchRequest = eapiRequest('/api/search/song/list/page', {\n      keyword: str,\n      needCorrect: '1',\n      channel: 'typing',\n      offset: limit * (page - 1),\n      scene: 'normal',\n      total: page == 1,\n      limit,\n    })\n    return searchRequest.promise.then(({ body }) => body)\n  },\n  getSinger(singers) {\n    let arr = []\n    singers.forEach(singer => {\n      arr.push(singer.name)\n    })\n    return arr.join('、')\n  },\n  handleResult(rawList) {\n    // console.log(rawList)\n    if (!rawList) return []\n    return rawList.map(item => {\n      item = item.baseInfo.simpleSongData\n      const types = []\n      const _types = {}\n      let size\n\n      if (item.privilege.maxBrLevel == 'hires') {\n        size = item.hr ? sizeFormate(item.hr.size) : null\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      switch (item.privilege.maxbr) {\n        case 999000:\n          size = item.sq ? sizeFormate(item.sq.size) : null\n          types.push({ type: 'flac', size })\n          _types.flac = {\n            size,\n          }\n        case 320000:\n          size = item.h ? sizeFormate(item.h.size) : null\n          types.push({ type: '320k', size })\n          _types['320k'] = {\n            size,\n          }\n        case 192000:\n        case 128000:\n          size = item.l ? sizeFormate(item.l.size) : null\n          types.push({ type: '128k', size })\n          _types['128k'] = {\n            size,\n          }\n      }\n\n      types.reverse()\n\n      return {\n        singer: this.getSinger(item.ar),\n        name: item.name,\n        albumName: item.al.name,\n        albumId: item.al.id,\n        source: 'wy',\n        interval: formatPlayTime(item.dt / 1000),\n        songmid: item.id,\n        img: item.al.picUrl,\n        lrc: null,\n        types,\n        _types,\n        typeUrl: {},\n      }\n    })\n  },\n  search(str, page = 1, limit, retryNum = 0) {\n    if (++retryNum > 3) return Promise.reject(new Error('try max num'))\n    if (limit == null) limit = this.limit\n    return this.musicSearch(str, page, limit).then(result => {\n      // console.log(result)\n      if (!result || result.code !== 200) return this.search(str, page, limit, retryNum)\n      let list = this.handleResult(result.data.resources || [])\n      // console.log(list)\n\n      if (list == null) return this.search(str, page, limit, retryNum)\n\n      this.total = result.data.totalCount || 0\n      this.page = page\n      this.allPage = Math.ceil(this.total / this.limit)\n\n      return {\n        list,\n        allPage: this.allPage,\n        limit: this.limit,\n        total: this.total,\n        source: 'wy',\n      }\n      // return result.data\n    })\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/singer.js",
    "content": "import { eapiRequest } from './utils/index'\nimport { formatPlayTime, sizeFormate } from '../../index'\nimport { formatSingerName } from '../utils'\n\nexport default {\n  /**\n   * 获取歌手信息\n   * @param {*} id\n   */\n  getInfo(id) {\n    return eapiRequest('/api/artist/head/info/get', { id }).then(({ body }) => {\n      if (!body || body.code != 200) throw new Error('get singer info faild.')\n      return {\n        source: 'wy',\n        id: body.artist.id,\n        info: {\n          name: body.artist.name,\n          desc: body.artist.briefDesc,\n          avatar: body.user.avatarUrl,\n          gender: body.user.gender === 1 ? 'man' : 'woman',\n        },\n        count: {\n          music: body.artist.musicSize,\n          album: body.artist.albumSize,\n        },\n      }\n    })\n  },\n  /**\n   * 获取歌手歌曲列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  getSongList(id, page = 1, limit = 100) {\n    if (page === 1) page = 0\n    return eapiRequest('/api/v2/artist/songs', {\n      id,\n      limit,\n      offset: limit * page,\n    }).then(({ body }) => {\n      if (!body.songs || body.code != 200) throw new Error('get singer song list faild.')\n\n      const list = this.filterSongList(body.songs)\n      return {\n        list,\n        limit,\n        page,\n        total: body.total,\n        source: 'wy',\n      }\n    })\n  },\n  /**\n   * 获取歌手专辑列表\n   * @param {*} id\n   * @param {*} page\n   * @param {*} limit\n   */\n  getAlbumList(id, page = 1, limit = 10) {\n    if (page === 1) page = 0\n    return eapiRequest(`/api/artist/albums/${id}`, {\n      limit,\n      offset: limit * page,\n    }).then(({ body }) => {\n      if (!body.hotAlbums || body.code != 200) throw new Error('get singer album list faild.')\n\n      const list = this.filterAlbumList(body.hotAlbums)\n      return {\n        source: 'wy',\n        list,\n        limit,\n        page,\n        total: body.artist.albumSize,\n      }\n    })\n  },\n  filterAlbumList(raw) {\n    const list = []\n    raw.forEach(item => {\n      if (!item.id) return\n      list.push({\n        id: item.id,\n        count: item.size,\n        info: {\n          name: item.name,\n          author: formatSingerName(item.artists),\n          img: item.picUrl,\n          desc: null,\n        },\n      })\n    })\n    return list\n  },\n  filterSongList(raw) {\n    const list = []\n    raw.forEach(item => {\n      if (!item.id) return\n\n      const types = []\n      const _types = {}\n      let size\n      item.privilege.chargeInfoList.forEach(i => {\n        switch (i.rate) {\n          case 128000:\n            size = item.lMusic ? sizeFormate(item.lMusic.size) : null\n            types.push({ type: '128k', size })\n            _types['128k'] = {\n              size,\n            }\n          case 320000:\n            size = item.hMusic ? sizeFormate(item.hMusic.size) : null\n            types.push({ type: '320k', size })\n            _types['320k'] = {\n              size,\n            }\n          case 999000:\n            size = item.sqMusic ? sizeFormate(item.sqMusic.size) : null\n            types.push({ type: 'flac', size })\n            _types.flac = {\n              size,\n            }\n          case 1999000:\n            size = item.hrMusic ? sizeFormate(item.hrMusic.size) : null\n            types.push({ type: 'flac24bit', size })\n            _types.flac24bit = {\n              size,\n            }\n        }\n      })\n\n      list.push({\n        singer: formatSingerName(item.artists),\n        name: item.name,\n        albumName: item.album.name,\n        albumId: item.album.id,\n        songmid: item.id,\n        source: 'wy',\n        interval: formatPlayTime(item.duration),\n        img: null,\n        lrc: null,\n        otherSource: null,\n        types,\n        _types,\n        typeUrl: {},\n      })\n    })\n    return list\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/songList.js",
    "content": "// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_catlist.js\n// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_hot.js\n// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/top_playlist.js\n// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/playlist_detail.js\n\nimport { weapi, linuxapi } from './utils/crypto'\nimport { httpFetch } from '../../request'\nimport { formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index'\nimport musicDetailApi from './musicDetail'\nimport { eapiRequest } from './utils/index'\nimport { formatSingerName } from '../utils'\n\nexport default {\n  _requestObj_tags: null,\n  _requestObj_hotTags: null,\n  _requestObj_list: null,\n  limit_list: 30,\n  limit_song: 100000,\n  successCode: 200,\n  cookie: 'MUSIC_U=',\n  sortList: [\n    {\n      name: '最热',\n      id: 'hot',\n    },\n    // {\n    //   name: '最新',\n    //   id: 'new',\n    // },\n  ],\n  regExps: {\n    listDetailLink: /^.+(?:\\?|&)id=(\\d+)(?:&.*$|#.*$|$)/,\n    listDetailLink2: /^.+\\/playlist\\/(\\d+)\\/\\d+\\/.+$/,\n  },\n\n  async handleParseId(link, retryNum = 0) {\n    if (retryNum > 2) throw new Error('link try max num')\n\n    const requestObj_listDetailLink = httpFetch(link)\n    const { headers: { location }, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(headers)\n    if (statusCode > 400) return this.handleParseId(link, ++retryNum)\n    const url = location == null ? link : location\n    return this.regExps.listDetailLink.test(url)\n      ? url.replace(this.regExps.listDetailLink, '$1')\n      : url.replace(this.regExps.listDetailLink2, '$1')\n  },\n\n  async getListId(id) {\n    let cookie\n    if (/###/.test(id)) {\n      const [url, token] = id.split('###')\n      id = url\n      cookie = `MUSIC_U=${token}`\n    }\n    if ((/[?&:/]/.test(id))) {\n      if (this.regExps.listDetailLink.test(id)) {\n        id = id.replace(this.regExps.listDetailLink, '$1')\n      } else if (this.regExps.listDetailLink2.test(id)) {\n        id = id.replace(this.regExps.listDetailLink2, '$1')\n      } else {\n        id = await this.handleParseId(id)\n      }\n      // console.log(id)\n    }\n    return { id, cookie }\n  },\n  async getListDetail(rawId, page, tryNum = 0) { // 获取歌曲列表内的音乐\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n\n    const { id, cookie } = await this.getListId(rawId)\n    if (cookie) this.cookie = cookie\n\n    const requestObj_listDetail = httpFetch('https://music.163.com/api/linux/forward', {\n      method: 'post',\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n        Cookie: this.cookie,\n      },\n      form: linuxapi({\n        method: 'POST',\n        url: 'https://music.163.com/api/v3/playlist/detail',\n        params: {\n          id,\n          n: this.limit_song,\n          s: 8,\n        },\n      }),\n    })\n    const { statusCode, body } = await requestObj_listDetail.promise\n    if (statusCode !== 200 || body.code !== this.successCode) return this.getListDetail(id, page, ++tryNum)\n    let limit = 1000\n    let rangeStart = (page - 1) * limit\n    // console.log(body)\n    let list\n    if (body.playlist.trackIds.length == body.privileges.length) {\n      list = this.filterListDetail(body)\n    } else {\n      try {\n        list = (await musicDetailApi.getList(body.playlist.trackIds.slice(rangeStart, limit * page).map(trackId => trackId.id))).list\n      } catch (err) {\n        console.log(err)\n        if (err.message == 'try max num') {\n          throw err\n        } else {\n          return this.getListDetail(id, page, ++tryNum)\n        }\n      }\n    }\n    // console.log(list)\n    return {\n      list,\n      page,\n      limit,\n      total: body.playlist.trackIds.length,\n      source: 'wy',\n      info: {\n        play_count: formatPlayCount(body.playlist.playCount),\n        name: body.playlist.name,\n        img: body.playlist.coverImgUrl,\n        desc: body.playlist.description,\n        author: body.playlist.creator.nickname,\n      },\n    }\n  },\n  filterListDetail({ playlist: { tracks }, privileges }) {\n    // console.log(tracks, privileges)\n    const list = []\n    tracks.forEach((item, index) => {\n      const types = []\n      const _types = {}\n      let size\n      let privilege = privileges[index]\n      if (privilege.id !== item.id) privilege = privileges.find(p => p.id === item.id)\n      if (!privilege) return\n\n      if (privilege.maxBrLevel == 'hires') {\n        size = item.hr ? sizeFormate(item.hr.size) : null\n        types.push({ type: 'flac24bit', size })\n        _types.flac24bit = {\n          size,\n        }\n      }\n      switch (privilege.maxbr) {\n        case 999000:\n          size = null\n          types.push({ type: 'flac', size })\n          _types.flac = {\n            size,\n          }\n        case 320000:\n          size = item.h ? sizeFormate(item.h.size) : null\n          types.push({ type: '320k', size })\n          _types['320k'] = {\n            size,\n          }\n        case 192000:\n        case 128000:\n          size = item.l ? sizeFormate(item.l.size) : null\n          types.push({ type: '128k', size })\n          _types['128k'] = {\n            size,\n          }\n      }\n\n      types.reverse()\n\n      if (item.pc) {\n        list.push({\n          singer: item.pc.ar ?? '',\n          name: item.pc.sn ?? '',\n          albumName: item.pc.alb ?? '',\n          albumId: item.al?.id,\n          source: 'wy',\n          interval: formatPlayTime(item.dt / 1000),\n          songmid: item.id,\n          img: item.al?.picUrl ?? '',\n          lrc: null,\n          otherSource: null,\n          types,\n          _types,\n          typeUrl: {},\n        })\n      } else {\n        list.push({\n          singer: formatSingerName(item.ar, 'name'),\n          name: item.name ?? '',\n          albumName: item.al?.name,\n          albumId: item.al?.id,\n          source: 'wy',\n          interval: formatPlayTime(item.dt / 1000),\n          songmid: item.id,\n          img: item.al?.picUrl,\n          lrc: null,\n          otherSource: null,\n          types,\n          _types,\n          typeUrl: {},\n        })\n      }\n    })\n    return list\n  },\n\n  // 获取列表数据\n  getList(sortId, tagId, page, tryNum = 0) {\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    if (this._requestObj_list) this._requestObj_list.cancelHttp()\n    this._requestObj_list = httpFetch('https://music.163.com/weapi/playlist/list', {\n      method: 'post',\n      form: weapi({\n        cat: tagId || '全部', // 全部,华语,欧美,日语,韩语,粤语,小语种,流行,摇滚,民谣,电子,舞曲,说唱,轻音乐,爵士,乡村,R&B/Soul,古典,民族,英伦,金属,朋克,蓝调,雷鬼,世界音乐,拉丁,另类/独立,New Age,古风,后摇,Bossa Nova,清晨,夜晚,学习,工作,午休,下午茶,地铁,驾车,运动,旅行,散步,酒吧,怀旧,清新,浪漫,性感,伤感,治愈,放松,孤独,感动,兴奋,快乐,安静,思念,影视原声,ACG,儿童,校园,游戏,70后,80后,90后,网络歌曲,KTV,经典,翻唱,吉他,钢琴,器乐,榜单,00后\n        order: sortId, // hot,new\n        limit: this.limit_list,\n        offset: this.limit_list * (page - 1),\n        total: true,\n      }),\n    })\n    return this._requestObj_list.promise.then(({ body }) => {\n      // console.log(body)\n      if (body.code !== this.successCode) return this.getList(sortId, tagId, page, ++tryNum)\n      return {\n        list: this.filterList(body.playlists),\n        total: parseInt(body.total),\n        page,\n        limit: this.limit_list,\n        source: 'wy',\n      }\n    })\n  },\n  filterList(rawData) {\n    // console.log(rawData)\n    return rawData.map(item => ({\n      play_count: formatPlayCount(item.playCount),\n      id: String(item.id),\n      author: item.creator.nickname,\n      name: item.name,\n      time: item.createTime ? dateFormat(item.createTime, 'Y-M-D') : '',\n      img: item.coverImgUrl,\n      grade: item.grade,\n      total: item.trackCount,\n      desc: item.description,\n      source: 'wy',\n    }))\n  },\n\n  // 获取标签\n  getTag(tryNum = 0) {\n    if (this._requestObj_tags) this._requestObj_tags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_tags = httpFetch('https://music.163.com/weapi/playlist/catalogue', {\n      method: 'post',\n      form: weapi({}),\n    })\n    return this._requestObj_tags.promise.then(({ body }) => {\n      // console.log(JSON.stringify(body))\n      if (body.code !== this.successCode) return this.getTag(++tryNum)\n      return this.filterTagInfo(body)\n    })\n  },\n  filterTagInfo({ sub, categories }) {\n    const subList = {}\n    for (const item of sub) {\n      if (!subList[item.category]) subList[item.category] = []\n      subList[item.category].push({\n        parent_id: categories[item.category],\n        parent_name: categories[item.category],\n        id: item.name,\n        name: item.name,\n        source: 'wy',\n      })\n    }\n\n    const list = []\n    for (const key of Object.keys(categories)) {\n      list.push({\n        name: categories[key],\n        list: subList[key],\n        source: 'wy',\n      })\n    }\n    return list\n  },\n\n  // 获取热门标签\n  getHotTag(tryNum = 0) {\n    if (this._requestObj_hotTags) this._requestObj_hotTags.cancelHttp()\n    if (tryNum > 2) return Promise.reject(new Error('try max num'))\n    this._requestObj_hotTags = httpFetch('https://music.163.com/weapi/playlist/hottags', {\n      method: 'post',\n      form: weapi({}),\n    })\n    return this._requestObj_hotTags.promise.then(({ body }) => {\n      // console.log(JSON.stringify(body))\n      if (body.code !== this.successCode) return this.getTag(++tryNum)\n      return this.filterHotTagInfo(body.tags)\n    })\n  },\n  filterHotTagInfo(rawList) {\n    return rawList.map(item => ({\n      id: item.playlistTag.name,\n      name: item.playlistTag.name,\n      source: 'wy',\n    }))\n  },\n\n  getTags() {\n    return Promise.all([this.getTag(), this.getHotTag()]).then(([tags, hotTag]) => ({ tags, hotTag, source: 'wy' }))\n  },\n\n  async getDetailPageUrl(rawId) {\n    const { id } = await this.getListId(rawId)\n    return `https://music.163.com/#/playlist?id=${id}`\n  },\n\n  search(text, page, limit = 20) {\n    return eapiRequest('/api/cloudsearch/pc', {\n      s: text,\n      type: 1000, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频\n      limit,\n      total: page == 1,\n      offset: limit * (page - 1),\n    })\n      .promise.then(({ body }) => {\n        if (body.code != this.successCode) throw new Error('filed')\n        // console.log(body)\n        return {\n          list: this.filterList(body.result.playlists),\n          limit,\n          total: body.result.playlistCount,\n          source: 'wy',\n        }\n      })\n  },\n}\n\n// getList\n// getTags\n// getListDetail\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/tipSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { weapi } from './utils/crypto'\nimport { formatSingerName } from '../utils'\n\nexport default {\n  requestObj: null,\n  cancelTipSearch() {\n    if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp()\n  },\n  tipSearchBySong(str) {\n    this.cancelTipSearch()\n    this.requestObj = httpFetch('https://music.163.com/weapi/search/suggest/web', {\n      method: 'POST',\n      headers: {\n        referer: 'https://music.163.com/',\n        origin: 'https://music.163.com/',\n      },\n      form: weapi({\n        s: str,\n      }),\n    })\n    return this.requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode != 200 || body.code != 200) return Promise.reject(new Error('请求失败'))\n      return body.result.songs\n    })\n  },\n  handleResult(rawData) {\n    return rawData.map(info => `${info.name} - ${formatSingerName(info.artists, 'name')}`)\n  },\n  async search(str) {\n    return this.tipSearchBySong(str).then(result => this.handleResult(result))\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/utils/crypto.js",
    "content": "// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js\nimport { createCipheriv, createDecipheriv, publicEncrypt, randomBytes, createHash, constants } from 'crypto'\nconst iv = Buffer.from('0102030405060708')\nconst presetKey = Buffer.from('0CoJUm6Qyw8W8jud')\nconst linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q')\nconst base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\nconst publicKey = '-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\\n-----END PUBLIC KEY-----'\nconst eapiKey = 'e82ckenh8dichen8'\n\nconst aesEncrypt = (buffer, mode, key, iv) => {\n  const cipher = createCipheriv(mode, key, iv)\n  return Buffer.concat([cipher.update(buffer), cipher.final()])\n}\n\nconst aesDecrypt = function(cipherBuffer, mode, key, iv) {\n  let decipher = createDecipheriv(mode, key, iv)\n  return Buffer.concat([decipher.update(cipherBuffer), decipher.final()])\n}\n\nconst rsaEncrypt = (buffer, key) => {\n  buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])\n  return publicEncrypt({ key, padding: constants.RSA_NO_PADDING }, buffer)\n}\n\nexport const weapi = object => {\n  const text = JSON.stringify(object)\n  const secretKey = randomBytes(16).map(n => (base62.charAt(n % 62).charCodeAt()))\n  return {\n    params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'aes-128-cbc', presetKey, iv).toString('base64')), 'aes-128-cbc', secretKey, iv).toString('base64'),\n    encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'),\n  }\n}\n\nexport const linuxapi = object => {\n  const text = JSON.stringify(object)\n  return {\n    eparams: aesEncrypt(Buffer.from(text), 'aes-128-ecb', linuxapiKey, '').toString('hex').toUpperCase(),\n  }\n}\n\n\nexport const eapi = (url, object) => {\n  const text = typeof object === 'object' ? JSON.stringify(object) : object\n  const message = `nobody${url}use${text}md5forencrypt`\n  const digest = createHash('md5').update(message).digest('hex')\n  const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`\n  return {\n    params: aesEncrypt(Buffer.from(data), 'aes-128-ecb', eapiKey, '').toString('hex').toUpperCase(),\n  }\n}\n\nexport const eapiDecrypt = cipherBuffer => {\n  return aesDecrypt(cipherBuffer, 'aes-128-ecb', eapiKey, '').toString()\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/wy/utils/index.js",
    "content": "import { httpFetch } from '../../../request'\nimport { eapi } from './crypto'\n\nexport const eapiRequest = (url, data) => {\n  return httpFetch('http://interface.music.163.com/eapi/batch', {\n    method: 'post',\n    headers: {\n      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',\n      origin: 'https://music.163.com',\n      // cookie: 'os=pc; deviceId=A9C064BB4584D038B1565B58CB05F95290998EE8B025AA2D07AE; osver=Microsoft-Windows-10-Home-China-build-19043-64bit; appver=2.5.2.197409; channel=netease; MUSIC_A=37a11f2eb9de9930cad479b2ad495b0e4c982367fb6f909d9a3f18f876c6b49faddb3081250c4980dd7e19d4bd9bf384e004602712cf2b2b8efaafaab164268a00b47359f85f22705cc95cb6180f3aee40f5be1ebf3148d888aa2d90636647d0c3061cd18d77b7a0; __csrf=05b50d54082694f945d7de75c210ef94; mode=Z7M-KP5(7)GZ; NMTID=00OZLp2VVgq9QdwokUgq3XNfOddQyIAAAF_6i8eJg; ntes_kaola_ad=1',\n    },\n    form: eapi(url, data),\n  })\n  // requestObj.promise = requestObj.promise.then(({ body }) => {\n  //   // console.log(raw)\n  //   console.log(body)\n  //   // console.log(eapiDecrypt(raw))\n  //   // return eapiDecrypt(raw)\n  //   return body\n  // })\n  // return requestObj\n}\n"
  },
  {
    "path": "src/renderer/utils/musicSdk/xm.js",
    "content": "// import { apis } from '../api-source'\n// import leaderboard from './leaderboard'\n// import songList from './songList'\n// import musicSearch from './musicSearch'\n// import pic from './pic'\n// import lyric from './lyric'\n// import hotSearch from './hotSearch'\n// import comment from './comment'\n// import musicInfo from './musicInfo'\n// import { closeVerifyModal } from './util'\n\nconst xm = {\n  // songList,\n  // musicSearch,\n  // leaderboard,\n  // hotSearch,\n  // closeVerifyModal,\n  comment: {\n    getComment() {\n      return Promise.reject(new Error('fail'))\n    },\n    getHotComment() {\n      return Promise.reject(new Error('fail'))\n    },\n  },\n  getMusicUrl(songInfo, type) {\n    return {\n      promise: Promise.reject(new Error('fail')),\n    }\n    // return apis('xm').getMusicUrl(songInfo, type)\n  },\n  getLyric(songInfo) {\n    return {\n      promise: Promise.reject(new Error('fail')),\n    }\n    // return lyric.getLyric(songInfo)\n  },\n  getPic(songInfo) {\n    return Promise.reject(new Error('fail'))\n    // return pic.getPic(songInfo)\n  },\n  // getMusicDetailPageUrl(songInfo) {\n  //   if (songInfo.songStringId) return `https://www.xiami.com/song/${songInfo.songStringId}`\n\n  //   musicInfo.getMusicInfo(songInfo).then(({ data }) => {\n  //     songInfo.songStringId = data.songStringId\n  //   })\n  //   return `https://www.xiami.com/song/${songInfo.songmid}`\n  // },\n  // init() {\n  //   getToken()\n  // },\n}\n\nexport default xm\n"
  },
  {
    "path": "src/renderer/utils/pickrTools.ts",
    "content": "import { throttle } from '@common/utils/common'\nimport Pickr from '@simonwep/pickr'\nimport '@simonwep/pickr/dist/themes/classic.min.css'\n\nexport interface PickrTools {\n  pickr: Pickr | null\n  create: (dom: HTMLElement, color: string, swatches: string[] | null, change: (color: string) => void, reset?: () => void) => PickrTools\n  destroy: () => void\n  setColor: (color: string) => void\n}\n\nexport const pickrTools: PickrTools = {\n  pickr: null,\n  create(dom, color, swatches, change, reset) {\n    const pickrTools: PickrTools = Object.create(this)\n\n    pickrTools.pickr = Pickr.create({\n      el: dom,\n      default: color,\n      theme: 'classic', // or 'monolith', or 'nano'\n      defaultRepresentation: 'RGBA',\n      autoReposition: false,\n      closeWithKey: '',\n      appClass: 'color-picker',\n      comparison: false,\n      useAsButton: true,\n\n      swatches,\n\n      components: {\n\n        // Main components\n        preview: true,\n        opacity: true,\n        hue: true,\n\n        // Input / output Options\n        interaction: {\n          hex: true,\n          rgba: true,\n          input: true,\n          cancel: true,\n          // save: true,\n        },\n      },\n\n      i18n: {\n        // Strings visible in the UI\n        'ui:dialog': ' ',\n        'btn:toggle': window.i18n.t('theme_edit_modal__pick_color'),\n        'btn:swatch': ' ',\n        'btn:last-color': window.i18n.t('theme_edit_modal__pick_last_color'),\n        'btn:save': window.i18n.t('theme_edit_modal__pick_save'),\n        'btn:cancel': window.i18n.t('theme_edit_modal__pick_cancel'),\n\n        // Strings used for aria-labels\n        'aria:btn:save': ' ',\n        'aria:btn:cancel': ' ',\n        'aria:input': ' ',\n        'aria:palette': ' ',\n        'aria:hue': '',\n        'aria:opacity': ' ',\n      },\n    })\n\n    let swatchselectColor: any\n\n    const throttleChange = throttle((color: any, source: string) => {\n      if (source == 'swatch' && swatchselectColor !== color) return\n      change(color.toRGBA().toString())\n    })\n    pickrTools.pickr.on('swatchselect', (color: any) => {\n      swatchselectColor = color\n    }).on('change', throttleChange).on('cancel', () => {\n      console.log('cancel')\n      change(color)\n      reset?.()\n    })\n\n    return pickrTools\n  },\n  destroy() {\n    if (!this.pickr) return\n    this.pickr.destroyAndRemove()\n    this.pickr = null\n  },\n  setColor(color) {\n    this.pickr?.setColor(color)\n  },\n}\n"
  },
  {
    "path": "src/renderer/utils/request.js",
    "content": "import needle from 'needle'\n// import progress from 'request-progress'\nimport { debugRequest } from './env'\nimport { requestMsg } from './message'\nimport { bHh } from './musicSdk/options'\nimport { deflateRaw } from 'zlib'\nimport { proxy } from '@renderer/store'\nimport { httpOverHttp, httpsOverHttp } from 'tunnel'\n// import fs from 'fs'\n\nconst httpsRxp = /^https:/\nconst getRequestAgent = url => {\n  let options\n  if (proxy.enable && proxy.host) {\n    options = {\n      proxy: {\n        host: proxy.host,\n        port: proxy.port,\n      },\n    }\n  } else if (proxy.envProxy) {\n    options = {\n      proxy: {\n        host: proxy.envProxy.host,\n        port: proxy.envProxy.port,\n      },\n    }\n  }\n  return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined\n}\n\n\nconst request = (url, options, callback) => {\n  let data\n  if (options.body) {\n    data = options.body\n  } else if (options.form) {\n    data = options.form\n    // data.content_type = 'application/x-www-form-urlencoded'\n    options.json = false\n  } else if (options.formData) {\n    data = options.formData\n    // data.content_type = 'multipart/form-data'\n    options.json = false\n  }\n  options.response_timeout = options.timeout\n\n  return needle.request(options.method || 'get', url, data, options, (err, resp, body) => {\n    if (!err) {\n      body = resp.body = resp.raw.toString()\n      try {\n        resp.body = JSON.parse(resp.body)\n      } catch (_) {}\n      body = resp.body\n    }\n    callback(err, resp, body)\n  }).request\n}\n\n\nconst defaultHeaders = {\n  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',\n}\n// var proxyUrl = \"http://\" + user + \":\" + password + \"@\" + host + \":\" + port;\n// var proxiedRequest = request.defaults({'proxy': proxyUrl});\n\n/**\n * promise 形式的请求方法\n * @param {*} url\n * @param {*} options\n */\nconst buildHttpPromose = (url, options) => {\n  let obj = {\n    isCancelled: false,\n    cancelHttp: () => {\n      if (!obj.requestObj) return obj.isCancelled = true\n      cancelHttp(obj.requestObj)\n      obj.requestObj = null\n      obj.promise = obj.cancelHttp = null\n      obj.cancelFn(new Error(requestMsg.cancelRequest))\n      obj.cancelFn = null\n    },\n  }\n  obj.promise = new Promise((resolve, reject) => {\n    obj.cancelFn = reject\n    debugRequest && console.log(`\\n---send request------${url}------------`)\n    fetchData(url, options.method, options, (err, resp, body) => {\n      // options.isShowProgress && window.api.hideProgress()\n      debugRequest && console.log(`\\n---response------${url}------------`)\n      debugRequest && console.log(body)\n      obj.requestObj = null\n      obj.cancelFn = null\n      if (err) return reject(err)\n      resolve(resp)\n    }).then(ro => {\n      obj.requestObj = ro\n      if (obj.isCancelled) obj.cancelHttp()\n    })\n  })\n  return obj\n}\n\n/**\n * 请求超时自动重试\n * @param {*} url\n * @param {*} options\n */\nexport const httpFetch = (url, options = { method: 'get' }) => {\n  const requestObj = buildHttpPromose(url, options)\n  requestObj.promise = requestObj.promise.catch(err => {\n    // console.log('出错', err)\n    if (err.message === 'socket hang up') {\n      // window.globalObj.apiSource = 'temp'\n      return Promise.reject(new Error(requestMsg.unachievable))\n    }\n    switch (err.code) {\n      case 'ETIMEDOUT':\n      case 'ESOCKETTIMEDOUT':\n        return Promise.reject(new Error(requestMsg.timeout))\n      case 'ENOTFOUND':\n        return Promise.reject(new Error(requestMsg.notConnectNetwork))\n      default:\n        return Promise.reject(err)\n    }\n  })\n  return requestObj\n}\n\n/**\n * 取消请求\n * @param {*} index\n */\nexport const cancelHttp = requestObj => {\n  // console.log(requestObj)\n  if (!requestObj) return\n  // console.log('cancel:', requestObj)\n  if (!requestObj.abort) return\n  requestObj.abort()\n}\n\n\n/**\n * http 请求\n * @param {*} url 地址\n * @param {*} options 选项\n * @param {*} cb 回调\n * @return {Number} index 用于取消请求\n */\nexport const http = (url, options, cb) => {\n  if (typeof options === 'function') {\n    cb = options\n    options = {}\n  }\n\n  // 默认选项\n  if (options.method == null) options.method = 'get'\n\n  debugRequest && console.log(`\\n---send request------${url}------------`)\n  return fetchData(url, options.method, options, (err, resp, body) => {\n    // options.isShowProgress && window.api.hideProgress()\n    debugRequest && console.log(`\\n---response------${url}------------`)\n    debugRequest && console.log(body)\n    if (err) {\n      debugRequest && console.log(JSON.stringify(err))\n    }\n    cb(err, resp, body)\n  })\n}\n\n/**\n * http get 请求\n * @param {*} url 地址\n * @param {*} options 选项\n * @param {*} callback 回调\n * @return {Number} index 用于取消请求\n */\nexport const httpGet = (url, options, callback) => {\n  if (typeof options === 'function') {\n    callback = options\n    options = {}\n  }\n  // options.isShowProgress && window.api.showProgress({\n  //   title: options.progressMsg || '请求中',\n  //   modal: true,\n  // })\n\n  debugRequest && console.log(`\\n---send request-------${url}------------`)\n  return fetchData(url, 'get', options, function(err, resp, body) {\n    // options.isShowProgress && window.api.hideProgress()\n    debugRequest && console.log(`\\n---response------${url}------------`)\n    debugRequest && console.log(body)\n    if (err) {\n      debugRequest && console.log(JSON.stringify(err))\n    }\n    callback(err, resp, body)\n  })\n}\n\n/**\n * http post 请求\n * @param {*} url 请求地址\n * @param {*} data 提交的数据\n * @param {*} options 选项\n * @param {*} callback 回调\n * @return {Number} index 用于取消请求\n */\nexport const httpPost = (url, data, options, callback) => {\n  if (typeof options === 'function') {\n    callback = options\n    options = {}\n  }\n  // options.isShowProgress && window.api.showProgress({\n  //   title: options.progressMsg || '请求中',\n  //   modal: true,\n  // })\n  options.data = data\n\n  debugRequest && console.log(`\\n---send request-------${url}------------`)\n  return fetchData(url, 'post', options, function(err, resp, body) {\n    // options.isShowProgress && window.api.hideProgress()\n    debugRequest && console.log(`\\n---response------${url}------------`)\n    debugRequest && console.log(body)\n    if (err) {\n      debugRequest && console.log(JSON.stringify(err))\n    }\n    callback(err, resp, body)\n  })\n}\n\n/**\n * http jsonp 请求\n * @param {*} url 请求地址\n * @param {*} options 选项\n *             options.jsonpCallback 回调\n * @param {*} callback 回调\n * @return {Number} index 用于取消请求\n */\nexport const http_jsonp = (url, options, callback) => {\n  if (typeof options === 'function') {\n    callback = options\n    options = {}\n  }\n\n  let jsonpCallback = 'jsonpCallback'\n  if (url.indexOf('?') < 0) url += '?'\n  url += `&${options.jsonpCallback}=${jsonpCallback}`\n\n  options.format = 'script'\n\n  // options.isShowProgress && window.api.showProgress({\n  //   title: options.progressMsg || '请求中',\n  //   modal: true,\n  // })\n\n  debugRequest && console.log(`\\n---send request-------${url}------------`)\n  return fetchData(url, 'get', options, function(err, resp, body) {\n    // options.isShowProgress && window.api.hideProgress()\n    debugRequest && console.log(`\\n---response------${url}------------`)\n    debugRequest && console.log(body)\n    if (err) {\n      debugRequest && console.log(JSON.stringify(err))\n    } else {\n      body = JSON.parse(body.replace(new RegExp(`^${jsonpCallback}\\\\(({.*})\\\\)$`), '$1'))\n    }\n\n    callback(err, resp, body)\n  })\n}\n\nconst handleDeflateRaw = data => new Promise((resolve, reject) => {\n  deflateRaw(data, (err, buf) => {\n    if (err) return reject(err)\n    resolve(buf)\n  })\n})\n\nconst regx = /(?:\\d\\w)+/g\n\nconst fetchData = async(url, method, {\n  headers = {},\n  format = 'json',\n  timeout = 15000,\n  ...options\n}, callback) => {\n  // console.log(url, options)\n  console.log('---start---', url)\n  headers = Object.assign({}, headers)\n  if (headers[bHh]) {\n    const path = url.replace(/^https?:\\/\\/[\\w.:]+\\//, '/')\n    let s = Buffer.from(bHh, 'hex').toString()\n    s = s.replace(s.substr(-1), '')\n    s = Buffer.from(s, 'base64').toString()\n    let v = process.versions.app.split('-')[0].split('.').map(n => n.length < 3 ? n.padStart(3, '0') : n).join('')\n    let v2 = process.versions.app.split('-')[1] || ''\n    headers[s] = !s || `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${path}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`\n    delete headers[bHh]\n  }\n  return request(url, {\n    ...options,\n    method,\n    headers: Object.assign({}, defaultHeaders, headers),\n    timeout,\n    agent: getRequestAgent(url),\n    json: format === 'json',\n  }, (err, resp, body) => {\n    if (err) return callback(err, null)\n    callback(null, resp, body)\n  })\n}\n\nexport const checkUrl = (url, options = {}) => {\n  return new Promise((resolve, reject) => {\n    fetchData(url, 'head', options, (err, resp) => {\n      if (err) return reject(err)\n      if (resp.statusCode === 200) {\n        resolve()\n      } else {\n        reject(new Error(resp.statusCode))\n      }\n    })\n  })\n}\n"
  },
  {
    "path": "src/renderer/utils/simplify-chinese-main/LICENSE.md",
    "content": "\nThe MIT License (MIT)\n\nCopyright (c) 2021 Shigma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "src/renderer/utils/simplify-chinese-main/README.md",
    "content": "# simplify-chinese\n\nConvert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。\n\n```js\nconst { simplify, tranditionalize } = require('simplify-chinese')\n\nconsole.log(simplify('窩窩頭'))         // 窝窝头\nconsole.log(tranditionalize('窝窝头'))  // 窩窩頭\n```\n"
  },
  {
    "path": "src/renderer/utils/simplify-chinese-main/chinese.js",
    "content": "export const simplified = '万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤呼啧啬啭啮啰啴啸喷喽喾嗫嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾鼋鼌鼍鼗鼹齄齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟志制咨系范尝准闲拼'\nexport const traditional = '萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚唿嘖嗇囀齧囉嘽嘯噴嘍嚳囁噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽黿鼂鼉鞀鼴齇齊齏齒齔齕齗齟齡齙齠齜齦齬齪齲齷龍龔龕龜誌製谘係範嘗準閒拚'\n"
  },
  {
    "path": "src/renderer/utils/simplify-chinese-main/index.d.ts",
    "content": "export function simplify(source: string): string\nexport function tranditionalize(source: string): string\n"
  },
  {
    "path": "src/renderer/utils/simplify-chinese-main/index.js",
    "content": "import { simplified, traditional } from './chinese'\n\nconst stMap = new Map()\nconst tsMap = new Map()\n\nsimplified.split('').forEach((char, index) => {\n  stMap.set(char, traditional[index])\n  tsMap.set(traditional[index], char)\n})\n\nfunction simplify(source) {\n  let result = []\n  for (const char of source) {\n    result.push(tsMap.get(char) || char)\n  }\n  return result.join('')\n}\n\nfunction tranditionalize(source) {\n  let result = []\n  for (const char of source) {\n    result.push(stMap.get(char) || char)\n  }\n  return result.join('')\n}\n\nexport {\n  simplify,\n  tranditionalize,\n}\n"
  },
  {
    "path": "src/renderer/utils/update.js",
    "content": "import { httpGet } from './request'\nimport pkg from '../../../package.json'\n\n// TODO add Notice\n\nconst author = pkg.author.name\nconst name = pkg.name\n\nconst address = [\n  [`https://raw.githubusercontent.com/${author}/${name}/master/publish/version.json`, 'direct'],\n  ['https://registry.npmjs.org/lx-music-desktop-version-info/latest', 'npm'],\n  [`https://cdn.jsdelivr.net/gh/${author}/${name}/publish/version.json`, 'direct'],\n  [`https://fastly.jsdelivr.net/gh/${author}/${name}/publish/version.json`, 'direct'],\n  [`https://gcore.jsdelivr.net/gh/${author}/${name}/publish/version.json`, 'direct'],\n  ['https://registry.npmmirror.com/lx-music-desktop-version-info/latest', 'npm'],\n  ['https://gitee.com/lyswhut/lx-music-desktop-versions/raw/master/version.json', 'direct'],\n  ['http://cdn.stsky.cn/lx-music/desktop/version.json', 'direct'],\n]\n\nconst request = async(url, retryNum = 0) => {\n  return new Promise((resolve, reject) => {\n    httpGet(url, {\n      timeout: 10000,\n    }, (err, resp, body) => {\n      if (err || resp.statusCode != 200) {\n        ++retryNum >= 3\n          ? reject(err || new Error(resp.statusMessage || resp.statusCode))\n          : request(url, retryNum).then(resolve).catch(reject)\n      } else resolve(body)\n    })\n  })\n}\n\nconst getDirectInfo = async(url) => {\n  return request(url).then(info => {\n    if (info.version == null) throw new Error('failed')\n    return info\n  })\n}\n\nconst getNpmPkgInfo = async(url) => {\n  return request(url).then(json => {\n    if (!json.versionInfo) throw new Error('failed')\n    const info = JSON.parse(json.versionInfo)\n    if (info.version == null) throw new Error('failed')\n    return info\n  })\n}\n\nexport const getVersionInfo = async(index = 0) => {\n  const [url, source] = address[index]\n  let promise\n  switch (source) {\n    case 'direct':\n      promise = getDirectInfo(url)\n      break\n    case 'npm':\n      promise = getNpmPkgInfo(url)\n      break\n  }\n\n  return promise.catch(async(err) => {\n    index++\n    if (index >= address.length) throw err\n    return getVersionInfo(index)\n  })\n}\n\n// getVersionInfo().then(info => {\n//   console.log(info)\n// })\n"
  },
  {
    "path": "src/renderer/views/Download/index.vue",
    "content": "<template>\n  <div :class=\"$style.download\">\n    <div :class=\"$style.header\">\n      <base-tab v-model=\"activeTab\" :class=\"$style.tab\" :list=\"tabs\" />\n    </div>\n    <div :class=\"$style.content\">\n      <div class=\"thead\" :class=\"$style.thead\">\n        <table>\n          <thead>\n            <tr>\n              <th class=\"num\" style=\"width: 5%;\">#</th>\n              <th class=\"nobreak\">{{ $t('music_name') }}</th>\n              <th class=\"nobreak\" style=\"width: 20%;\">{{ $t('download__progress') }}</th>\n              <th class=\"nobreak\" style=\"width: 22%;\">{{ $t('download__status') }}</th>\n              <th class=\"nobreak\" style=\"width: 10%;\">{{ $t('download__quality') }}</th>\n              <th class=\"nobreak\" style=\"width: 13%;\">{{ $t('action') }}</th>\n            </tr>\n          </thead>\n        </table>\n      </div>\n      <div v-if=\"list.length\" ref=\"dom_listContent\" :class=\"$style.content\">\n        <base-virtualized-list\n          ref=\"listRef\" v-slot=\"{ item, index }\" :list=\"list\" key-name=\"id\" :item-height=\"listItemHeight\"\n          container-class=\"scroll\" content-class=\"list\"\n        >\n          <div\n            class=\"list-item\"\n            :class=\"[{[$style.active]: playTaskId == item.id }, { selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]\"\n            @click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n          >\n            <div class=\"list-item-cell no-select\" :class=\"$style.num\" style=\"flex: 0 0 5%;\">\n              <transition name=\"play-active\">\n                <div v-if=\"playTaskId == item.id\" :class=\"$style.playIcon\">\n                  <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n                    <use xlink:href=\"#icon-play-outline\" />\n                  </svg>\n                </div>\n                <div v-else class=\"num\">{{ index + 1 }}</div>\n              </transition>\n            </div>\n            <div class=\"list-item-cell auto name\">\n              <span class=\"select name\" :aria-label=\"getName(item)\">{{ getName(item) }}</span>\n            </div>\n            <div class=\"list-item-cell\" style=\"flex: 0 0 20%;\">{{ item.progress }}%<span v-if=\"item.status == downloadStatus.RUN && item.speed\"> - {{ item.speed }}/s</span></div>\n            <div class=\"list-item-cell\" style=\"flex: 0 0 22%;\" :aria-label=\"item.statusText\">{{ item.statusText }}</div>\n            <div class=\"list-item-cell\" style=\"flex: 0 0 10%;\">{{ getTypeName(item.metadata.quality) }}</div>\n            <div class=\"list-item-cell\" style=\"flex: 0 0 13%; padding-left: 0; padding-right: 0;\">\n              <material-list-buttons\n                :index=\"index\" :download-btn=\"false\" :file-btn=\"item.status != downloadStatus.ERROR\" remove-btn=\"remove-btn\"\n                :start-btn=\"!item.isComplate && item.status != downloadStatus.WAITING && (item.status != downloadStatus.RUN)\"\n                :pause-btn=\"!item.isComplate && (item.status == downloadStatus.RUN || item.status == downloadStatus.WAITING)\"\n                :list-add-btn=\"false\" :play-btn=\"item.status == downloadStatus.COMPLETED\"\n                :search-btn=\"item.status == downloadStatus.ERROR\" @btn-click=\"handleListBtnClick\"\n              />\n            </div>\n          </div>\n        </base-virtualized-list>\n      </div>\n      <div v-else :class=\"$style.noItem\">\n        <p v-text=\"$t('no_item')\" />\n      </div>\n      <base-menu v-model=\"isShowItemMenu\" :menus=\"menus\" :xy=\"menuLocation\" item-name=\"name\" @menu-click=\"handleMenuClick\" />\n      <!-- <base-menu :menus=\"listItemMenu\" :location=\"listMenu.menuLocation\" item-name=\"name\" :is-show=\"listMenu.isShowItemMenu\" @menu-click=\"handleListItemMenuClick\" /> -->\n    </div>\n    <common-list-add-modal v-model:show=\"isShowListAdd\" :music-info=\"selectedAddMusicInfo\" teleport=\"#view\" />\n    <common-list-add-multiple-modal v-model:show=\"isShowListAddMultiple\" :music-list=\"selectedList\" teleport=\"#view\" @confirm=\"removeAllSelect\" />\n  </div>\n</template>\n\n<script>\n// import { checkPath, openDirInExplorer, openUrl } from '@common/utils/electron'\n\nimport { ref } from '@common/utils/vueTools'\nimport useListInfo from './useListInfo'\nimport useList from './useList'\nimport useTab from './useTab'\nimport useMenu from './useMenu'\nimport usePlay from './usePlay'\nimport useTaskActions from './useTaskActions'\nimport useMusicAdd from './useMusicAdd'\nimport { downloadStatus } from '@renderer/store/download/state'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'Download',\n  setup() {\n    const listRef = ref()\n    const { tabs, activeTab } = useTab()\n\n    const {\n      rightClickSelectedIndex,\n      dom_listContent,\n      listAll,\n      list,\n      playTaskId,\n    } = useListInfo(activeTab)\n\n    const {\n      selectedList,\n      listItemHeight,\n      removeAllSelect,\n      handleSelectData,\n    } = useList({ listRef, list, listAll })\n\n    const {\n      handlePlayMusic,\n      handlePlayMusicLater,\n    } = usePlay({ selectedList, list, listAll, removeAllSelect })\n\n    const {\n      handleSearch,\n      handleOpenMusicDetail,\n      handleStartTask,\n      handlePauseTask,\n      handleRemoveTask,\n      handleOpenFile,\n    } = useTaskActions({ list, removeAllSelect, selectedList })\n\n    const {\n      isShowListAdd,\n      isShowListAddMultiple,\n      selectedAddMusicInfo,\n      handleShowMusicAddModal,\n    } = useMusicAdd({ selectedList, list })\n\n    const {\n      menus,\n      menuLocation,\n      isShowItemMenu,\n      showMenu,\n      menuClick,\n    } = useMenu({\n      handleStartTask,\n      handlePauseTask,\n      handleRemoveTask,\n      handleOpenFile,\n      handlePlayMusic,\n      handlePlayMusicLater,\n      handleShowMusicAddModal,\n      handleSearch,\n      handleOpenMusicDetail,\n    })\n\n    let clickTime = 0\n    let clickIndex = -1\n    const doubleClickPlay = index => {\n      if (\n        window.performance.now() - clickTime > 400 ||\n      clickIndex !== index\n      ) {\n        clickTime = window.performance.now()\n        clickIndex = index\n        return\n      }\n      const task = list.value[index]\n      if (task.isComplate) {\n        handlePlayMusic(list.value.indexOf(task), true)\n      } else if (task.status === downloadStatus.RUN || task.status === downloadStatus.WAITING) {\n        void handlePauseTask(index, true)\n      } else {\n        void handleStartTask(index, true)\n      }\n      clickTime = 0\n      clickIndex = -1\n    }\n\n    const handleListItemClick = (event, index) => {\n      if (rightClickSelectedIndex.value > -1) return\n      handleSelectData(index)\n      doubleClickPlay(index)\n    }\n    const handleListItemRightClick = (event, index) => {\n      rightClickSelectedIndex.value = index\n      showMenu(event, list.value[index], index)\n    }\n    const handleMenuClick = (action) => {\n      let index = rightClickSelectedIndex.value\n      rightClickSelectedIndex.value = -1\n      menuClick(action, index)\n    }\n\n    const handleListBtnClick = ({ action, index }) => {\n      switch (action) {\n        case 'play':\n          handlePlayMusic(index, true)\n          break\n        case 'start':\n          void handleStartTask(index, true)\n          break\n        case 'pause':\n          void handlePauseTask(index, true)\n          break\n        case 'remove':\n          void handleRemoveTask(index, true)\n          break\n        case 'file':\n          void handleOpenFile(index)\n          break\n        case 'search':\n          handleSearch(index)\n          break\n      }\n    }\n\n    const getName = (downloadInfo) => {\n      return appSetting['download.fileName'].replace('歌名', downloadInfo.metadata.musicInfo.name).replace('歌手', downloadInfo.metadata.musicInfo.singer)\n    }\n    const getTypeName = (quality) => {\n      return quality == 'flac24bit' ? 'FLAC Hires' : quality?.toUpperCase()\n    }\n    return {\n      listRef,\n      list,\n      downloadStatus,\n      rightClickSelectedIndex,\n      dom_listContent,\n      tabs,\n      activeTab,\n      selectedList,\n      listItemHeight,\n      playTaskId,\n\n      isShowListAdd,\n      isShowListAddMultiple,\n      selectedAddMusicInfo,\n\n      removeAllSelect,\n\n      menus,\n      menuLocation,\n      isShowItemMenu,\n\n      handleListItemClick,\n      handleListItemRightClick,\n      handleMenuClick,\n      handleListBtnClick,\n\n      getName,\n      getTypeName,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.download {\n  position: relative;\n  overflow: hidden;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n\n  :global(.list-item) {\n    &.active {\n      color: var(--color-button-font);\n    }\n  }\n}\n.num {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n}\n.playIcon {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  color: var(--color-button-font);\n  opacity: .7;\n}\n\n.content {\n  min-height: 0;\n  font-size: 14px;\n  display: flex;\n  flex-flow: column nowrap;\n  flex: auto;\n}\n\n.noItem {\n  position: relative;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/views/Download/useList.js",
    "content": "import { computed, watch, ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport { getFontSizeWithScreen } from '@renderer/utils'\nimport { appSetting } from '@renderer/store/setting'\n\nconst useKeyEvent = ({ listRef, handleSelectAllData }) => {\n  const keyEvent = {\n    isShiftDown: false,\n    isModDown: false,\n  }\n\n  const handle_key_shift_down = () => {\n    keyEvent.isShiftDown ||= true\n  }\n  const handle_key_shift_up = () => {\n    keyEvent.isShiftDown &&= false\n  }\n  const handle_key_mod_down = () => {\n    keyEvent.isModDown ||= true\n  }\n  const handle_key_mod_up = () => {\n    keyEvent.isModDown &&= false\n  }\n  const handle_key_mod_a_down = ({ event }) => {\n    if (event.target.tagName == 'INPUT' || document.activeElement != listRef.value?.$el) return\n    event.preventDefault()\n    if (event.repeat) return\n    keyEvent.isModDown = false\n    handleSelectAllData()\n  }\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_shift_down', handle_key_shift_down)\n    window.key_event.off('key_shift_up', handle_key_shift_up)\n    window.key_event.off('key_mod_down', handle_key_mod_down)\n    window.key_event.off('key_mod_up', handle_key_mod_up)\n    window.key_event.off('key_mod+a_down', handle_key_mod_a_down)\n  })\n  window.key_event.on('key_shift_down', handle_key_shift_down)\n  window.key_event.on('key_shift_up', handle_key_shift_up)\n  window.key_event.on('key_mod_down', handle_key_mod_down)\n  window.key_event.on('key_mod_up', handle_key_mod_up)\n  window.key_event.on('key_mod+a_down', handle_key_mod_a_down)\n\n  return keyEvent\n}\n\nexport default ({ listRef, list, listAll }) => {\n  const selectedList = ref([])\n\n  let lastSelectIndex = -1\n  const listItemHeight = computed(() => {\n    return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)\n  })\n\n  const removeAllSelect = () => {\n    selectedList.value = []\n  }\n  const handleSelectAllData = () => {\n    removeAllSelect()\n    selectedList.value = [...list.value]\n  }\n  const keyEvent = useKeyEvent({ handleSelectAllData, listRef })\n\n  const handleSelectData = clickIndex => {\n    if (keyEvent.isShiftDown) {\n      if (selectedList.value.length) {\n        removeAllSelect()\n        if (lastSelectIndex != clickIndex) {\n          let isNeedReverse = false\n          let _lastSelectIndex = lastSelectIndex\n          if (clickIndex < _lastSelectIndex) {\n            let temp = _lastSelectIndex\n            _lastSelectIndex = clickIndex\n            clickIndex = temp\n            isNeedReverse = true\n          }\n          selectedList.value = list.value.slice(_lastSelectIndex, clickIndex + 1)\n          if (isNeedReverse) selectedList.value.reverse()\n        }\n      } else {\n        selectedList.value.push(list.value[clickIndex])\n        lastSelectIndex = clickIndex\n      }\n    } else if (keyEvent.isModDown) {\n      lastSelectIndex = clickIndex\n      let item = list.value[clickIndex]\n      let index = selectedList.value.indexOf(item)\n      if (index < 0) {\n        selectedList.value.push(item)\n      } else {\n        selectedList.value.splice(index, 1)\n      }\n    } else if (selectedList.value.length) {\n      removeAllSelect()\n    }\n  }\n\n  watch(listAll, removeAllSelect)\n\n  return {\n    selectedList,\n    listItemHeight,\n    removeAllSelect,\n    handleSelectData,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/useListInfo.js",
    "content": "import { ref, computed } from '@common/utils/vueTools'\nimport { playMusicInfo, playInfo } from '@renderer/store/player/state'\nimport { downloadStatus } from '@renderer/store/download/state'\nimport { getDownloadList } from '@renderer/store/download/action'\nimport { LIST_IDS } from '@common/constants'\n\n\nexport default (activeTab) => {\n  const rightClickSelectedIndex = ref(-1)\n  const dom_listContent = ref(null)\n\n  const listAll = ref([])\n  getDownloadList().then(l => {\n    listAll.value = l\n  })\n\n  const list = computed(() => {\n    switch (activeTab.value) {\n      case 'runing':\n        return listAll.value.filter(i => i.status == downloadStatus.RUN || i.status == downloadStatus.WAITING)\n      case 'paused':\n        return listAll.value.filter(i => i.status == downloadStatus.PAUSE)\n      case 'error':\n        return listAll.value.filter(i => i.status == downloadStatus.ERROR)\n      case 'finished':\n        return listAll.value.filter(i => i.status == downloadStatus.COMPLETED)\n      default:\n        return [...listAll.value]\n    }\n  })\n\n  const playTaskId = computed(() => playMusicInfo.listId == LIST_IDS.DOWNLOAD ? listAll.value[playInfo.playIndex]?.id : '')\n\n\n  return {\n    rightClickSelectedIndex,\n    dom_listContent,\n    listAll,\n    list,\n    playTaskId,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/useMenu.js",
    "content": "import { computed, ref, shallowReactive, reactive, nextTick } from '@common/utils/vueTools'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { DOWNLOAD_STATUS } from '@common/constants'\n\nexport default ({\n  handleStartTask,\n  handlePauseTask,\n  handleRemoveTask,\n  handleOpenFile,\n  handlePlayMusic,\n  handlePlayMusicLater,\n  handleShowMusicAddModal,\n  handleSearch,\n  handleOpenMusicDetail,\n}) => {\n  const itemMenuControl = reactive({\n    play: true,\n    start: true,\n    pause: true,\n    playLater: true,\n    file: true,\n    sourceDetail: true,\n    search: true,\n    remove: true,\n    addTo: true,\n  })\n  const t = useI18n()\n  const menuLocation = shallowReactive({ x: 0, y: 0 })\n  const isShowItemMenu = ref(false)\n\n  const menus = computed(() => {\n    return [\n      {\n        name: t('list__play'),\n        action: 'play',\n        hide: !itemMenuControl.play,\n      },\n      {\n        name: t('list__start'),\n        action: 'start',\n        hide: !itemMenuControl.start,\n      },\n      {\n        name: t('list__pause'),\n        action: 'pause',\n        hide: !itemMenuControl.pause,\n      },\n      {\n        name: t('list__play_later'),\n        action: 'playLater',\n        hide: !itemMenuControl.playLater,\n      },\n      {\n        name: t('list__file'),\n        action: 'file',\n        hide: !itemMenuControl.file,\n      },\n      {\n        name: t('list__add_to'),\n        action: 'addTo',\n        disabled: !itemMenuControl.addTo,\n      },\n      {\n        name: t('list__source_detail'),\n        action: 'sourceDetail',\n        disabled: !itemMenuControl.sourceDetail,\n      },\n      {\n        name: t('list__search'),\n        action: 'search',\n        hide: !itemMenuControl.search,\n      },\n      {\n        name: t('list__remove'),\n        action: 'remove',\n        hide: !itemMenuControl.remove,\n      },\n    ]\n  })\n\n  const showMenu = (event, taskInfo) => {\n    itemMenuControl.sourceDetail = !!musicSdk[taskInfo.metadata.musicInfo.source]?.getMusicDetailPageUrl\n\n    if (taskInfo.isComplate) {\n      itemMenuControl.play =\n        itemMenuControl.playLater =\n        itemMenuControl.file = true\n      itemMenuControl.start =\n        itemMenuControl.pause = false\n    } else if (taskInfo.status === DOWNLOAD_STATUS.ERROR || taskInfo.status === DOWNLOAD_STATUS.PAUSE) {\n      itemMenuControl.play =\n        itemMenuControl.playLater =\n        itemMenuControl.pause =\n        itemMenuControl.file = false\n      itemMenuControl.start = true\n    } else {\n      itemMenuControl.play =\n        itemMenuControl.playLater =\n        itemMenuControl.start =\n        itemMenuControl.file = false\n      itemMenuControl.pause = true\n    }\n\n    menuLocation.x = event.pageX\n    menuLocation.y = event.pageY\n\n    if (isShowItemMenu.value) return\n\n    nextTick(() => {\n      isShowItemMenu.value = true\n    })\n  }\n\n  const hideMenu = () => {\n    isShowItemMenu.value = false\n  }\n\n  const menuClick = (action, index) => {\n    // console.log(action)\n    hideMenu()\n    if (!action) return\n    switch (action.action) {\n      case 'start':\n        handleStartTask(index)\n        break\n      case 'pause':\n        handlePauseTask(index)\n        break\n      case 'file':\n        handleOpenFile(index)\n        break\n      case 'play':\n        handlePlayMusic(index)\n        break\n      case 'playLater':\n        handlePlayMusicLater(index)\n        break\n      case 'addTo':\n        handleShowMusicAddModal(index)\n        break\n      case 'search':\n        handleSearch(index)\n        break\n      case 'remove':\n        handleRemoveTask(index)\n        break\n      case 'sourceDetail':\n        handleOpenMusicDetail(index)\n    }\n  }\n\n  return {\n    menus,\n    menuLocation,\n    isShowItemMenu,\n    showMenu,\n    menuClick,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/useMusicAdd.js",
    "content": "import { ref, shallowRef, nextTick, markRaw } from '@common/utils/vueTools'\n\nexport default ({ selectedList, list }) => {\n  const isShowListAdd = ref(false)\n  const isShowListAddMultiple = ref(false)\n  const selectedAddMusicInfo = shallowRef(null)\n\n  const handleShowMusicAddModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isShowListAddMultiple.value = true\n    } else {\n      selectedAddMusicInfo.value = markRaw(list.value[index].metadata.musicInfo)\n      nextTick(() => {\n        isShowListAdd.value = true\n      })\n    }\n  }\n\n  return {\n    isShowListAdd,\n    isShowListAddMultiple,\n    selectedAddMusicInfo,\n    handleShowMusicAddModal,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/usePlay.js",
    "content": "import { addTempPlayList } from '@renderer/store/player/action'\nimport { playList } from '@renderer/core/player'\nimport { LIST_IDS } from '@common/constants'\n\nexport default ({ selectedList, list, listAll, removeAllSelect }) => {\n  const handlePlayMusic = (index) => {\n    playList(LIST_IDS.DOWNLOAD, listAll.value.indexOf(list.value[index]))\n  }\n\n  const handlePlayMusicLater = (index, single) => {\n    if (selectedList.value.length && !single) {\n      addTempPlayList(selectedList.value.map(s => ({ listId: LIST_IDS.DOWNLOAD, musicInfo: s })))\n      removeAllSelect()\n    } else {\n      addTempPlayList([{ listId: LIST_IDS.DOWNLOAD, musicInfo: list.value[index] }])\n    }\n  }\n\n  return {\n    handlePlayMusic,\n    handlePlayMusicLater,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/useTab.js",
    "content": "import { computed, ref } from '@common/utils/vueTools'\n\nexport default () => {\n  const tabs = computed(() => {\n    return [\n      {\n        label: window.i18n.t('download__all'),\n        id: 'all',\n      },\n      {\n        label: window.i18n.t('download__running'),\n        id: 'runing',\n      },\n      {\n        label: window.i18n.t('download__paused'),\n        id: 'paused',\n      },\n      {\n        label: window.i18n.t('download__error'),\n        id: 'error',\n      },\n      {\n        label: window.i18n.t('download__finished'),\n        id: 'finished',\n      },\n    ]\n  })\n  const activeTab = ref('all')\n\n  // const setActiveTab = (tab) => {\n  //   activeTab.value = tab\n  // }\n\n  return {\n    tabs,\n    activeTab,\n    // setActiveTab,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Download/useTaskActions.js",
    "content": "import { useRouter } from '@common/utils/vueRouter'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { openUrl } from '@common/utils/electron'\nimport { checkPath } from '@common/utils/nodejs'\n// import { dialog } from '@renderer/plugins/Dialog'\n// import { useI18n } from '@renderer/plugins/i18n'\n// import { appSetting } from '@renderer/store/setting'\nimport { toOldMusicInfo } from '@renderer/utils/index'\nimport { startDownloadTasks, pauseDownloadTasks, removeDownloadTasks } from '@renderer/store/download/action'\nimport { openDirInExplorer } from '@renderer/utils/ipc'\n\nexport default ({ list, selectedList, removeAllSelect }) => {\n  const router = useRouter()\n  // const t = useI18n()\n\n  const handleSearch = index => {\n    const info = list.value[index].metadata.musicInfo\n    router.push({\n      path: '/search',\n      query: {\n        text: `${info.name} ${info.singer}`,\n      },\n    })\n  }\n\n  const handleOpenMusicDetail = index => {\n    const task = list.value[index]\n    const mInfo = toOldMusicInfo(task.metadata.musicInfo)\n    const url = musicSdk[mInfo.source]?.getMusicDetailPageUrl?.(mInfo)\n    if (!url) return\n    openUrl(url)\n  }\n\n  const handleStartTask = async(index, single) => {\n    if (selectedList.value.length && !single) {\n      startDownloadTasks([...selectedList.value])\n      removeAllSelect()\n    } else {\n      startDownloadTasks([list.value[index]])\n    }\n  }\n\n  const handlePauseTask = async(index, single) => {\n    if (selectedList.value.length && !single) {\n      pauseDownloadTasks([...selectedList.value])\n      removeAllSelect()\n    } else {\n      pauseDownloadTasks([list.value[index]])\n    }\n  }\n\n  const handleRemoveTask = async(index, single) => {\n    if (selectedList.value.length && !single) {\n      // const confirm = await (selectedList.value.length > 1\n      //   ? dialog.confirm({\n      //     message: t('lists__remove_music_tip', { len: selectedList.value.length }),\n      //     confirmButtonText: t('lists__remove_tip_button'),\n      //   })\n      //   : Promise.resolve(true)\n      // )\n      // if (!confirm) return\n      removeDownloadTasks(selectedList.value.map(m => m.id))\n      removeAllSelect()\n    } else {\n      removeDownloadTasks([list.value[index].id])\n    }\n  }\n\n  const handleOpenFile = async(index) => {\n    const task = list.value[index]\n    if (!checkPath(task.metadata.filePath)) return\n    openDirInExplorer(task.metadata.filePath)\n  }\n\n  return {\n    handleSearch,\n    handleOpenMusicDetail,\n    handleStartTask,\n    handlePauseTask,\n    handleRemoveTask,\n    handleOpenFile,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/BoardList/index.vue",
    "content": "<template>\n  <ul ref=\"dom_lists_list\" class=\"scroll\" :class=\"$style.listsContent\">\n    <li\n      v-for=\"(item, index) in list\"\n      :key=\"item.id\" :class=\"[$style.listsItem, { [$style.active]: item.id == boardId }, { [$style.clicked]: rightClickItemIndex == index }]\"\n      :aria-label=\"item.name\" @click=\"handleToggleList(item.id)\" @contextmenu=\"handleRigthClick($event, index)\"\n    >\n      <span :class=\"$style.listsLabel\">\n        <transition name=\"list-active\">\n          <svg-icon v-if=\"item.id == boardId\" name=\"angle-right-solid\" :class=\"$style.activeIcon\" />\n        </transition>\n        {{ item.name }}\n      </span>\n    </li>\n  </ul>\n  <base-menu\n    v-model=\"isShowMenu\"\n    :menus=\"menus\"\n    :xy=\"menuLocation\"\n    item-name=\"name\"\n    @menu-click=\"handleMenuClick\"\n  />\n</template>\n\n<script setup>\nimport { watch, shallowReactive, ref } from '@common/utils/vueTools'\nimport { getBoardsList, setBoard } from '@renderer/store/leaderboard/action'\nimport { boards } from '@renderer/store/leaderboard/state'\nimport useMenu from './useMenu'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\n\nconst props = defineProps({\n  source: {\n    type: String,\n    required: true,\n  },\n  boardId: {\n    type: [String, undefined],\n    default: undefined,\n  },\n})\n\nconst emit = defineEmits(['show-menu'])\n\nconst router = useRouter()\nconst route = useRoute()\n\nconst list = shallowReactive([])\nconst rightClickItemIndex = ref(-1)\n\nconst handleToggleList = (id) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      source: props.source,\n      boardId: id,\n    },\n  })\n}\n\nconst {\n  menus,\n  menuLocation,\n  isShowMenu,\n  showMenu,\n  menuClick,\n} = useMenu({ emit, list })\n\nconst handleRigthClick = (event, index) => {\n  rightClickItemIndex.value = index\n  showMenu(event, index)\n}\nconst handleMenuClick = (action) => {\n  if (rightClickItemIndex.value < 0) return\n  let index = rightClickItemIndex.value\n  rightClickItemIndex.value = -1\n  menuClick(action, index, props.source)\n}\n\n\nwatch(() => props.source, async(source) => {\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  let boardList = boards[source]\n  if (boardList == null) setBoard(boardList = await getBoardsList(source), source)\n  list.splice(0, list.length, ...boardList.list)\n  if (!props.boardId && boardList.list.length) handleToggleList(boardList.list[0].id)\n}, {\n  immediate: true,\n})\n\ndefineExpose({ hideMenu: handleMenuClick })\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.listsContent {\n  flex: auto;\n  min-width: 0;\n  overflow-y: scroll;\n  // overflow-y: scroll !important;\n  // border-right: 1px solid rgba(0, 0, 0, 0.12);\n}\n.listsItem {\n  position: relative;\n  transition: .3s ease;\n  transition-property: color, background-color;\n  background-color: transparent;\n  &:hover:not(.active) {\n    background-color: var(--color-primary-background-hover);\n    cursor: pointer;\n  }\n  &.active {\n    // background-color:\n    color: var(--color-primary);\n  }\n  &.selected {\n    background-color: var(--color-primary-font-active);\n  }\n  &.clicked {\n    background-color: var(--color-primary-background-hover);\n  }\n  &.editing {\n    padding: 0 10px;\n    background-color: var(--color-primary-background-hover);\n    .listsLabel {\n      display: none;\n    }\n    .listsInput {\n      display: block;\n    }\n  }\n}\n.activeIcon {\n  height: .9em;\n  width: .9em;\n  margin-left: -0.45em;\n  vertical-align: -0.05em;\n}\n.listsLabel {\n  display: block;\n  height: 100%;\n  padding: 0 10px;\n  font-size: 13px;\n  line-height: 36px;\n  .mixin-ellipsis-1();\n}\n\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/BoardList/useMenu.js",
    "content": "import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { addSongListDetail, playSongListDetail } from '../action'\n\nexport default ({\n  emit,\n  list,\n}) => {\n  // const menuControl = reactive({\n  //   play: true,\n  //   collect: true,\n  // })\n  const t = useI18n()\n  const menuLocation = reactive({ x: 0, y: 0 })\n  const isShowMenu = ref(false)\n\n  const menus = computed(() => {\n    return [\n      {\n        name: t('list__play'),\n        action: 'play',\n        disabled: false,\n      },\n      {\n        name: t('list__collect'),\n        action: 'collect',\n        disabled: false,\n      },\n    ]\n  })\n\n\n  const showMenu = (event, index) => {\n    menuLocation.x = event.pageX\n    menuLocation.y = event.pageY\n\n    if (isShowMenu.value) return\n    emit('show-menu')\n    nextTick(() => {\n      isShowMenu.value = true\n    })\n  }\n\n  const hideMenu = () => {\n    isShowMenu.value = false\n  }\n\n\n  const menuClick = (action, index, source) => {\n    // console.log(action)\n    hideMenu()\n    if (!action) return\n    // const id = `board__${this.source}__${board.id}`\n    const board = list[index]\n    switch (action.action) {\n      case 'play':\n        playSongListDetail(board.id)\n        break\n      case 'collect':\n        addSongListDetail(board.id, board.name, source)\n        break\n    }\n  }\n\n  return {\n    menus,\n    menuLocation,\n    isShowMenu,\n    showMenu,\n    menuClick,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/MusicList/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <material-online-list\n      ref=\"listRef\"\n      :page=\"listDetailInfo.page\"\n      :limit=\"listDetailInfo.limit\"\n      :total=\"listDetailInfo.total\"\n      :list=\"listDetailInfo.list\"\n      :no-item=\"listDetailInfo.noItemLabel\"\n      @show-menu=\"hideListsMenu\"\n      @play-list=\"handlePlayList\"\n      @toggle-page=\"togglePage\"\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { watch } from '@common/utils/vueTools'\nimport useList from './useList'\n\n\nconst props = defineProps<{\n  source: LX.OnlineSource\n  boardId?: string\n}>()\n\nconst emit = defineEmits(['show-menu'])\n\nconst {\n  listRef,\n  listDetailInfo,\n  getList,\n  handlePlayList,\n} = useList()\n\nwatch(() => props.boardId, (boardId) => {\n  if (!boardId) return\n  getList(boardId, 1)\n}, {\n  immediate: true,\n})\n\n\nconst hideListsMenu = () => {\n  emit('show-menu')\n}\n\nconst togglePage = (page: number) => {\n  getList(listDetailInfo.id, page)\n}\n\nconst hideMenu = () => {\n  listRef.value.handleMenuClick()\n}\n\ndefineExpose({ hideMenu })\n\n\n</script>\n\n\n<style lang=\"less\" module>\n.container {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n}\n\n.list {\n  overflow: hidden;\n  height: 100%;\n  flex: auto;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/MusicList/useList.ts",
    "content": "import { ref } from '@common/utils/vueTools'\n// import { useI18n } from '@renderer/plugins/i18n'\n// import { } from '@renderer/store/search/state'\nimport { getAndSetListDetail } from '@renderer/store/leaderboard/action'\nimport { listDetailInfo } from '@renderer/store/leaderboard/state'\nimport { playSongListDetail } from '../action'\n\nexport default () => {\n  const listRef = ref<any>(null)\n\n  const handlePlayList = (index: number) => {\n    void playSongListDetail(listDetailInfo.id, listDetailInfo.list, index)\n  }\n\n  const getList = (id: string, page: number) => {\n    void getAndSetListDetail(id, page).then(() => {\n      setTimeout(() => {\n        if (listRef.value) listRef.value.scrollToTop()\n      })\n    })\n  }\n\n  return {\n    listRef,\n    listDetailInfo,\n    getList,\n    handlePlayList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/action.ts",
    "content": "import { tempListMeta, userLists } from '@renderer/store/list/state'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport syncSourceList from '@renderer/store/list/syncSourceList'\nimport { getListDetail, getListDetailAll } from '@renderer/store/leaderboard/action'\nimport { createUserList, setTempList } from '@renderer/store/list/action'\nimport { playList } from '@renderer/core/player/action'\nimport { LIST_IDS } from '@common/constants'\nimport { toMD5 } from '@renderer/utils'\n\nconst getListId = (id: string) => `board__${id}`\n\nexport const addSongListDetail = async(id: string, name: string, source: LX.OnlineSource) => {\n  // console.log(this.listDetail.info)\n  // if (!this.listDetail.info.name) return\n  const listId = getListId(id)\n  const targetList = userLists.find(l => l.sourceListId == listId)\n  if (targetList) {\n    const confirm = await dialog.confirm({\n      message: window.i18n.t('duplicate_list_tip', { name: targetList.name }),\n      cancelButtonText: window.i18n.t('lists__import_part_button_cancel'),\n      confirmButtonText: window.i18n.t('confirm_button_text'),\n    })\n    if (!confirm) return\n    void syncSourceList(targetList)\n    return\n  }\n\n  const list = await getListDetailAll(id)\n  await createUserList({\n    name,\n    id: `${source}_${toMD5(listId)}`,\n    list,\n    source,\n    sourceListId: listId,\n  })\n}\n\nexport const playSongListDetail = async(id: string, list?: LX.Music.MusicInfoOnline[], index: number = 0) => {\n  let isPlayingList = false\n  // console.log(list)\n  const listId = getListId(id)\n  if (!list?.length) list = (await getListDetail(id, 1)).list\n  if (list?.length) {\n    await setTempList(listId, [...list])\n    playList(LIST_IDS.TEMP, index)\n    isPlayingList = true\n  }\n  const fullList = await getListDetailAll(id)\n  if (!fullList.length) return\n  if (isPlayingList) {\n    if (tempListMeta.id == listId) {\n      await setTempList(listId, [...fullList])\n    }\n  } else {\n    await setTempList(listId, [...fullList])\n    playList(LIST_IDS.TEMP, index)\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Leaderboard/index.vue",
    "content": "<template>\n  <div :class=\"$style.leaderboard\">\n    <div :class=\"$style.lists\">\n      <div :class=\"$style.listsSelect\">\n        <base-selection :model-value=\"source\" :class=\"$style.select\" :list=\"sourceList\" item-key=\"id\" item-name=\"name\" @update:model-value=\"handleToggleSource\" />\n      </div>\n      <BoardList ref=\"boardListRef\" :board-id=\"boardId\" :source=\"source\" @show-menu=\"$refs.musicListRef?.hideMenu()\" />\n    </div>\n    <div :class=\"$style.list\">\n      <MusicList ref=\"musicListRef\" :source=\"source\" :board-id=\"boardId\" @show-menu=\"$refs.boardListRef?.hideMenu()\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { computed, ref } from '@common/utils/vueTools'\nimport { getLeaderboardSetting, setLeaderboardSetting } from '@renderer/utils/data'\nimport BoardList from './BoardList/index.vue'\nimport MusicList from './MusicList/index.vue'\nimport { sources } from '@renderer/store/leaderboard/state'\nimport { sourceNames } from '@renderer/store'\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\n\n\nconst source = ref('')\nconst boardId = ref(null)\n\nconst verifyQueryParams = async function(to, from, next) {\n  let _source = to.query.source\n  let _boardId = to.query.boardId\n\n  if (_source == null) {\n    const setting = await getLeaderboardSetting()\n    if (_source == null) {\n      _source = setting.source\n      _boardId = setting.boardId\n    }\n    next({\n      path: to.path,\n      query: { ...to.query, source: _source, boardId: _boardId },\n    })\n    return\n  }\n  next()\n  source.value = _source\n  boardId.value = _boardId\n  void setLeaderboardSetting({ source: _source, boardId: _boardId })\n}\n\n\nexport default {\n  components: {\n    BoardList,\n    MusicList,\n  },\n  beforeRouteEnter: verifyQueryParams,\n  beforeRouteUpdate: verifyQueryParams,\n  setup() {\n    const musicListRef = ref(null)\n    const boardListRef = ref(null)\n    const sourceList = computed(() => {\n      return sources.map(s => ({ id: s, name: sourceNames.value[s] }))\n    })\n    const router = useRouter()\n    const route = useRoute()\n    const handleToggleSource = (id) => {\n      void router.replace({\n        path: route.path,\n        query: {\n          source: id,\n        },\n      })\n    }\n\n    return {\n      source,\n      boardId,\n      sourceList,\n      handleToggleSource,\n      musicListRef,\n      boardListRef,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.leaderboard {\n  height: 100%;\n  display: flex;\n  position: relative;\n}\n.header {\n  flex: none;\n  width: 100%;\n  display: flex;\n  flex-flow: row nowrap;\n\n}\n.tab {\n  flex: auto;\n}\n.select {\n  flex: none;\n  width: 80px;\n}\n.content {\n  flex: auto;\n  display: flex;\n  overflow: hidden;\n  flex-flow: column nowrap;\n}\n\n.lists {\n  flex: none;\n  width: 14.8%;\n  display: flex;\n  flex-flow: column nowrap;\n}\n.listsHeader {\n  position: relative;\n}\n\n.listsSelect {\n  font-size: 12px;\n\n  &:hover {\n    :global(.icon) {\n      opacity: 1;\n    }\n  }\n\n  >:global(.content) {\n    display: block;\n    width: 100%;\n  }\n  :global(.label-content) {\n    background-color: transparent !important;\n    line-height: 38px;\n    height: 38px;\n    border-radius: 0;\n    &:hover {\n      background: none !important;\n    }\n  }\n  :global(.label) {\n    color: var(--color-font) !important;\n  }\n  :global(.icon) {\n    opacity: .6;\n    transition: opacity .3s ease;\n  }\n\n  :global(.selection-list) {\n    max-height: 500px;\n    box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);\n    li {\n      // background-color: var(--color-main-background);\n      line-height: 38px;\n      font-size: 13px;\n      &:hover {\n        background-color: var(--color-button-background-hover);\n      }\n      &:active {\n        background-color: var(--color-button-background-active);\n      }\n    }\n  }\n  // line-height: 38px;\n  // padding: 0 10px;\n  border-bottom: var(--color-list-header-border-bottom);\n  flex: none;\n}\n\n.list {\n  position: relative;\n  overflow: hidden;\n  height: 100%;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  // .noItem {\n\n  // }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/components/MusicSortModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" teleport=\"#view\" @close=\"handleClose\" @after-enter=\"$refs.input.focus()\">\n    <main :class=\"$style.main\">\n      <h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>\n      <base-input\n        ref=\"input\"\n        v-model=\"sortNum\"\n        :class=\"$style.input\"\n        type=\"number\"\n        :placeholder=\"$t('music_sort__input_tip')\"\n        @submit=\"handleSubmit\" @blur=\"verify\"\n      />\n      <div :class=\"$style.footer\">\n        <base-btn :class=\"$style.btn\" @click=\"handleSubmit\">{{ $t('btn_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicInfo: {\n      type: Object,\n      default() {\n        return {}\n      },\n    },\n    selectedNum: {\n      type: Number,\n      default: 0,\n    },\n  },\n  emits: ['update:show', 'confirm'],\n  data() {\n    return {\n      sortNum: '',\n    }\n  },\n  watch: {\n    show(n) {\n      if (n) {\n        this.sortNum = ''\n      }\n    },\n  },\n  methods: {\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    verify() {\n      let num = /^[1-9]\\d*/.exec(this.sortNum)\n      num = num ? parseInt(num[0]) : ''\n      this.sortNum = num.toString()\n      return num\n    },\n    handleSubmit() {\n      let num = this.verify()\n      if (this.sortNum == '') return\n      this.handleClose()\n      this.$emit('confirm', num)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 0 15px;\n  max-width: 530px;\n  min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    word-break: break-all;\n    // text-align: center;\n    padding: 15px 0 8px;\n  }\n}\n\n.input {\n  // width: 100%;\n  // height: 26px;\n  padding: 8px 8px;\n}\n.footer {\n  margin: 20px 0 15px auto;\n}\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n\n  +.btn {\n    margin-left: 10px;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/components/MusicToggleModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" teleport=\"#view\" bg-close height=\"100%\" @close=\"handleClose\">\n    <main :class=\"$style.main\">\n      <base-tab v-model=\"source\" :class=\"$style.tab\" :list=\"tabs\" />\n      <div class=\"scroll\" :class=\"$style.list\">\n        <template v-if=\"list.length\">\n          <div v-for=\"item in list\" :key=\"item.id\" :class=\"$style.listItem\">\n            <!-- <div :class=\"$style.num\">{{ index + 1 }}</div> -->\n            <div :class=\"$style.textContent\">\n              <h3 :class=\"$style.text\" :aria-label=\"`${item.name} - ${item.singer}`\">{{ item.name }}</h3>\n              <h3 v-if=\"item.meta.albumName\" :class=\"[$style.text, $style.albumName]\" :aria-label=\"item.meta.albumName\">\n                {{ item.singer }}\n                <span v-if=\"item.meta.albumName\"> / {{ item.meta.albumName }}</span>\n              </h3>\n            </div>\n            <div :class=\"$style.label\">{{ item.interval }}</div>\n            <div :class=\"$style.btns\">\n              <button type=\"button\" :class=\"$style.btn\" @click=\"openDetail(item)\">\n                <svg-icon name=\"share\" />\n              </button>\n              <button type=\"button\" :class=\"$style.btn\" @click=\"handlePlay(item)\">\n                <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 287.386 287.386\" space=\"preserve\">\n                  <use xlink:href=\"#icon-testPlay\" />\n                </svg>\n              </button>\n            </div>\n          </div>\n        </template>\n        <div v-else :class=\"$style.noItem\">\n          <p v-text=\"noItemLabel\" />\n        </div>\n      </div>\n      <div :class=\"$style.footer\">\n        <div :class=\"$style.info\">\n          <h2>\n            <div :class=\"$style.nameLabel\">\n              <span :class=\"$style.name\">{{ musicInfo.name }}</span>\n              <span :class=\"$style.label\">{{ musicInfo.source }} {{ musicInfo.interval }}</span>\n            </div>\n            <div :class=\"$style.singer\">\n              {{ musicInfo.singer }}\n              <span v-if=\"musicInfo.meta.albumName\"> / {{ musicInfo.meta.albumName }}</span>\n            </div>\n          </h2>\n          <template v-if=\"toggleMusicInfo\">\n            <span style=\"flex: none;\">→</span>\n            <h2>\n              <div :class=\"$style.nameLabel\">\n                <span :class=\"$style.name\">{{ toggleMusicInfo.name }}</span>\n                <span :class=\"$style.label\">{{ toggleMusicInfo.source }} {{ musicInfo.interval }}</span>\n              </div>\n              <div :class=\"$style.singer\">\n                {{ toggleMusicInfo.singer }}\n                <span v-if=\"toggleMusicInfo.meta.albumName\"> / {{ toggleMusicInfo.meta.albumName }}</span>\n              </div>\n            </h2>\n          </template>\n        </div>\n        <base-btn :disabled=\"!toggleMusicInfo || musicInfo.id == toggleMusicInfo.id\" :class=\"$style.btn\" @click=\"handleConfirm\">{{ $t('music_toggle_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { LIST_IDS } from '@common/constants'\nimport { openUrl } from '@common/utils/electron'\nimport { playNext } from '@renderer/core/player'\nimport { getSourceI18nPrefix } from '@renderer/store'\nimport { addTempPlayList } from '@renderer/store/player/action'\nimport { playMusicInfo } from '@renderer/store/player/state'\nimport { toNewMusicInfo, toOldMusicInfo } from '@renderer/utils'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { markRaw } from 'vue'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicInfo: {\n      type: Object,\n      default() {\n        return {}\n      },\n    },\n  },\n  emits: ['update:show', 'toggle'],\n  data() {\n    return {\n      tabs: [],\n      lists: {},\n      source: '',\n      isError: false,\n      loading: false,\n      searchKey: 0,\n      toggleMusicInfo: null,\n    }\n  },\n  computed: {\n    list() {\n      return this.lists[this.source] ?? []\n    },\n    noItemLabel() {\n      return this.loading ? this.$t('list__loading') : this.isError ? this.$t('list__load_failed') : this.$t('no_item')\n    },\n  },\n  watch: {\n    show(n) {\n      if (n) {\n        this.isError = false\n        this.toggleMusicInfo = null\n        const musicInfo = this.musicInfo\n        this.tabs = []\n        this.lists = {}\n        this.loading = true\n        const searchKey = this.searchKey = Math.random()\n        void musicSdk.searchMusic({\n          name: musicInfo.name,\n          singer: musicInfo.singer,\n          source: '',\n          albumName: musicInfo.meta.albumName,\n          interval: musicInfo.interval ?? '',\n        }).then((lists) => {\n          if (this.searchKey != searchKey) return\n          const prefix = getSourceI18nPrefix()\n          this.tabs = lists.map(item => {\n            return {\n              id: item.source,\n              label: window.i18n.t(prefix + item.source),\n            }\n          })\n          if (lists.length) this.source = lists[0].source\n          for (const s of lists) this.lists[s.source] = s.list.map(s => markRaw(toNewMusicInfo(s)))\n        }).catch(() => {\n          if (this.searchKey != searchKey) return\n          this.isError = true\n        }).finally(() => {\n          if (this.searchKey != searchKey) return\n          this.loading = false\n        })\n      }\n    },\n  },\n  methods: {\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    handleConfirm() {\n      this.$emit('toggle', this.toggleMusicInfo)\n    },\n    openDetail(minfo) {\n      const url = musicSdk[minfo.source]?.getMusicDetailPageUrl(toOldMusicInfo(minfo))\n      if (!url) return\n      void openUrl(url)\n    },\n    handlePlay(musicInfo) {\n      this.toggleMusicInfo = musicInfo\n      const isPlaying = !!playMusicInfo.musicInfo\n      addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n      if (isPlaying) void playNext()\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 10px 7px 0;\n  width: 560px;\n  max-width: 100%;\n  box-sizing: border-box;\n  // min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  height: 100%;\n}\n.tab {\n  flex: none;\n}\n\n.list {\n  flex: auto;\n  min-height: 100px;\n  min-width: 460px;\n  // background-color: @color-search-form-background;\n  font-size: 13px;\n  transition-property: height;\n  margin-top: 10px;\n  padding: 0 7px;\n  // position: relative;\n  .listItem {\n    position: relative;\n    padding: 10px 5px;\n    transition: background-color .2s ease;\n    line-height: 1.4;\n    // height: 100%;\n    // overflow: hidden;\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n    border-radius: 4px;\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n    }\n    // &:last-child {\n    //   border-bottom-left-radius: 4px;\n    //   border-bottom-right-radius: 4px;\n    // }\n  }\n\n  .num {\n    flex: none;\n    font-size: 12px;\n    width: 20px;\n    text-align: center;\n    color: var(--color-font-label);\n  }\n  .textContent {\n    flex: auto;\n    min-width: 0;\n    display: flex;\n    flex-flow: column nowrap;\n    align-items: flex-start;\n    overflow: hidden;\n  }\n  .text {\n    max-width: 100%;\n    .mixin-ellipsis-1();\n  }\n  .albumName {\n    font-size: 12px;\n    opacity: 0.6;\n    // .mixin-ellipsis-1();\n  }\n  .label {\n    flex: none;\n    font-size: 12px;\n    opacity: 0.5;\n    padding: 0 5px;\n    display: flex;\n    align-items: center;\n    // transform: rotate(45deg);\n    // background-color:\n  }\n  .btns {\n    flex: none;\n    font-size: 12px;\n    padding: 0 5px;\n    display: flex;\n    align-items: center;\n  }\n  .btn {\n    background-color: transparent;\n    border: none;\n    border-radius: @form-radius;\n    margin-right: 5px;\n    cursor: pointer;\n    padding: 4px 7px;\n    color: var(--color-button-font);\n    outline: none;\n    transition: background-color 0.2s ease;\n    line-height: 0;\n    &:last-child {\n      margin-right: 0;\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n    }\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n    }\n    &:active {\n      background-color: var(--color-primary-font-active);\n    }\n  }\n\n  .noItem {\n    position: relative;\n    display: flex;\n    flex-flow: column nowrap;\n    justify-content: center;\n    align-items: center;\n    height: 100%;\n\n    p {\n      font-size: 16px;\n      color: var(--color-font-label);\n    }\n  }\n}\n\n.footer {\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n  align-items: center;\n  padding: 10px 7px;\n  .info {\n    min-width: 0;\n    display: flex;\n    flex-flow: row nowrap;\n    padding-right: 10px;\n    gap: 10px;\n    font-size: 12px;\n    align-items: center;\n\n    h2 {\n      min-width: 0;\n      color: var(--color-font);\n      line-height: 1.5;\n      word-break: break-all;\n    }\n    .nameLabel {\n      display: flex;\n      flex-flow: row nowrap;\n    }\n    .name {\n      .mixin-ellipsis();\n    }\n    .label {\n      flex: none;\n      font-size: 12px;\n      opacity: 0.8;\n      padding: 0 5px;\n      color: var(--color-primary);\n      // display: flex;\n      // align-items: center;\n      // transform: rotate(45deg);\n      // background-color:\n    }\n    .singer {\n      // font-size: 0.9em;\n      color: var(--color-font-label);\n      .mixin-ellipsis();\n    }\n  }\n\n  .btn {\n    flex: none;\n    // box-sizing: border-box;\n    // margin-left: 15px;\n    // margin-bottom: 15px;\n    // height: 36px;\n    // line-height: 36px;\n    // padding: 0 10px !important;\n    min-width: 70px;\n    // .mixin-ellipsis-1();\n\n    +.btn {\n      margin-left: 10px;\n    }\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/components/SearchList.vue",
    "content": "<template>\n  <teleport to=\"#view\">\n    <div v-show=\"isShow\" ref=\"dom_container\" :class=\"$style.container\">\n      <transition enter-active-class=\"animated-fast zoomIn\" leave-active-class=\"animated zoomOut\" @after-leave=\"handleAnimated\">\n        <div v-show=\"visible\" :class=\"$style.search\">\n          <div :class=\"$style.form\">\n            <input\n              ref=\"dom_input\" v-model.trim=\"text\" class=\"ignore-esc\" :placeholder=\"placeholder\" @input=\"handleDelaySearch\"\n              @keydown.arrow-down.arrow-up.prevent @keyup.arrow-down.prevent.exact=\"handleKeyDown\" @keyup.arrow-up.prevent.exact=\"handleKeyUp\"\n              @keyup.enter=\"handleTemplistClick(selectIndex)\"\n              @keyup.escape.prevent.exact=\"handleKeyEsc\" @keydown.control.prevent=\"handle_key_mod_down\" @keydown.meta.prevent=\"handle_key_mod_down\"\n              @keyup.control.prevent=\"handle_key_mod_up\" @keyup.meta.prevent=\"handle_key_mod_up\" @contextmenu=\"handleContextMenu\"\n            >\n            <button type=\"button\" @click=\"handleHide\">\n              <slot>\n                <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"70%\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\">\n                  <use xlink:href=\"#icon-delete\" />\n                </svg>\n              </slot>\n            </button>\n          </div>\n          <div v-if=\"resultList\" ref=\"dom_scrollContainer\" class=\"scroll\" :class=\"$style.list\" :style=\"listStyle\">\n            <ul ref=\"dom_list\">\n              <li v-for=\"(item, index) in resultList\" :key=\"item.songmid\" :class=\"selectIndex === index ? $style.select : null\" @mouseenter=\"selectIndex = index\" @click=\"handleTemplistClick(index)\">\n                <div :class=\"$style.img\" />\n                <div :class=\"$style.text\">\n                  <h3 :class=\"$style.text\">{{ item.name }} - {{ item.singer }}</h3>\n                  <h3 v-if=\"item.meta.albumName\" :class=\"[$style.text, $style.albumName]\">{{ item.meta.albumName }}</h3>\n                </div>\n                <div :class=\"$style.source\">{{ item.source }}</div>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </transition>\n    </div>\n  </teleport>\n</template>\n\n<script>\nimport { debounce } from '@common/utils'\nimport { clipboardReadText } from '@common/utils/electron'\nimport { toRaw } from '@common/utils/vueTools'\n\nexport default {\n  props: {\n    placeholder: {\n      type: String,\n      default: 'Search for something...',\n    },\n    list: {\n      type: Array,\n      default() {\n        return []\n      },\n    },\n    visible: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['action'],\n  data() {\n    return {\n      text: '',\n      selectIndex: -1,\n      listStyle: {\n        height: 0,\n        maxHeight: 0,\n        overflow: 'hidden',\n      },\n      maxHeight: 0,\n      resultList: [],\n      isModDown: false,\n      isShow: false,\n    }\n  },\n  watch: {\n    resultList(n) {\n      if (this.selectIndex > -1) this.selectIndex = -1\n      this.$nextTick(() => {\n        const height = this.$refs.dom_list.scrollHeight\n        if (height > this.maxHeight) {\n          this.listStyle.height = this.maxHeight + 'px'\n          this.listStyle.overflow = 'auto'\n        } else {\n          this.listStyle.height = height + 'px'\n          this.listStyle.overflow = 'hidden'\n        }\n      })\n    },\n    list(n) {\n      if (!this.visible) return\n      this.handleDelaySearch()\n    },\n    visible(n) {\n      if (!n) return\n      this.isShow = true\n      this.init()\n    },\n  },\n  created() {\n    this.handleDelaySearch = debounce(() => {\n      this.handleSearch()\n    })\n    if (this.visible) this.isShow = true\n  },\n  mounted() {\n    this.init()\n    // window.key_event.on('key_mod_down', this.handle_key_mod_down)\n    // window.key_event.on('key_mod_up', this.handle_key_mod_up)\n    window.key_event.on('key_mod+f_down', this.handle_key_mod_f_down)\n  },\n  beforeUnmount() {\n    // window.key_event.off('key_mod_down', this.handle_key_mod_down)\n    // window.key_event.off('key_mod_up', this.handle_key_mod_up)\n    window.key_event.off('key_mod+f_down', this.handle_key_mod_f_down)\n  },\n  methods: {\n    init() {\n      if (!this.visible) return\n      this.handleSearch()\n      this.$nextTick(() => {\n        if (!this.listStyle.maxHeight) {\n          this.maxHeight = this.$refs.dom_container.offsetParent.clientHeight - this.$refs.dom_list.offsetTop - 70\n          this.listStyle.maxHeight = this.maxHeight + 'px'\n        }\n        this.$refs.dom_input.focus()\n      })\n    },\n    handleKeyEsc() {\n      if (this.text.length > 0) {\n        this.text = ''\n        this.resultList = []\n      } else {\n        this.handleHide()\n      }\n    },\n    handle_key_mod_down() {\n      console.log('handle_key_mod_down')\n      this.isModDown ||= true\n    },\n    handle_key_mod_up() {\n      this.isModDown &&= false\n    },\n    handle_key_mod_f_down() {\n      if (this.visible) this.$refs.dom_input.focus()\n    },\n    handleAnimated() {\n      if (this.visible) return\n      this.isShow = false\n    },\n    handleTemplistClick(index) {\n      if (index < 0) return\n      const id = this.resultList[index].id\n      this.sendEvent('listClick', {\n        index: this.list.findIndex(m => m.id == id),\n        isPlay: this.isModDown,\n      })\n    },\n    handleHide() {\n      this.sendEvent('hide')\n    },\n    sendEvent(action, data) {\n      this.$emit('action', {\n        action,\n        data,\n      })\n    },\n    handleKeyDown() {\n      if (this.resultList.length) {\n        this.selectIndex = this.selectIndex + 1 < this.resultList.length ? this.selectIndex + 1 : 0\n        this.handleScrollList()\n      } else if (this.selectIndex > -1) {\n        this.selectIndex = -1\n      }\n    },\n    handleKeyUp() {\n      if (this.resultList.length) {\n        this.selectIndex = this.selectIndex - 1 < -1 ? this.resultList.length - 1 : this.selectIndex - 1\n        this.handleScrollList()\n      } else if (this.selectIndex > -1) {\n        this.selectIndex = -1\n      }\n    },\n    handleScrollList() {\n      if (this.selectIndex < 0) return\n      let dom = this.$refs.dom_list.children[this.selectIndex]\n      let offsetTop = dom.offsetTop\n      let scrollTop = this.$refs.dom_scrollContainer.scrollTop\n      let top\n      if (offsetTop < scrollTop) {\n        top = offsetTop\n      } else if (offsetTop + dom.clientHeight > this.$refs.dom_scrollContainer.clientHeight + scrollTop) {\n        top = offsetTop + dom.clientHeight - this.$refs.dom_scrollContainer.clientHeight\n      } else return\n      this.$refs.dom_scrollContainer.scrollTo(0, top)\n    },\n    handleContextMenu() {\n      let str = clipboardReadText()\n      str = str.trim()\n      str = str.replace(/\\t|\\r\\n|\\n|\\r/g, ' ')\n      str = str.replace(/\\s+/g, ' ')\n      let dom_input = this.$refs.dom_input\n      const text = dom_input.value\n      // if (dom_input.selectionStart == dom_input.selectionEnd) {\n      const value = text.substring(0, dom_input.selectionStart) + str + text.substring(dom_input.selectionEnd, text.length)\n      // event.target.value = value\n      this.text = value\n      // } else {\n      //   clipboardWriteText(text.substring(dom_input.selectionStart, dom_input.selectionEnd))\n      // }\n    },\n    async handleSearch() {\n      if (!this.text.length) return this.resultList = []\n      this.resultList = await window.lx.worker.main.searchListMusic(toRaw(this.list), this.text)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  position: absolute;\n  left: 50%;\n  transform: translateX(-50%);\n  top: 20px;\n  width: 45%;\n  height: @height-toolbar * 0.52;\n  z-index: 99;\n}\n\n.search {\n  position: absolute;\n  width: 100%;\n  border-radius: 4px;\n  transition: box-shadow .4s ease, background-color @transition-normal;\n  display: flex;\n  flex-flow: column nowrap;\n  background-color: var(--color-primary-light-600-alpha-100);\n  box-shadow: 0 1px 2px rgba(0,0,0,0.07),\n                0 2px 4px rgba(0,0,0,0.07),\n                0 4px 8px rgba(0,0,0,0.07),\n                0 8px 16px rgba(0,0,0,0.07),\n                0 16px 32px rgba(0,0,0,0.07),\n                0 32px 64px rgba(0,0,0,0.07);\n\n  &.active {\n    .form {\n      input {\n        border-bottom-left-radius: 0;\n\n      }\n      button {\n        border-bottom-right-radius: 0;\n      }\n    }\n  }\n  .form {\n    display: flex;\n    height: @height-toolbar * 0.52;\n    position: relative;\n    input {\n      flex: auto;\n      // border: 1px solid;\n      border-top-left-radius: 4px;\n      border-bottom-left-radius: 4px;\n      background-color: transparent;\n      // border-bottom: 2px solid var(--color-primary);\n      // border-color: var(--color-primary);\n      border: none;\n\n      outline: none;\n      // height: @height-toolbar * .7;\n      padding: 0 5px;\n      overflow: hidden;\n      font-size: 13.5px;\n      line-height: @height-toolbar * 0.52 + 5px;\n      &::placeholder {\n        color: var(--color-button-font);\n        font-size: .98em;\n      }\n    }\n    button {\n      flex: none;\n      border: none;\n      // background-color: @color-search-form-background;\n      background-color: transparent;\n      outline: none;\n      border-top-right-radius: 4px;\n      border-bottom-right-radius: 4px;\n      cursor: pointer;\n      height: 100%;\n      padding: 6px 9px;\n      color: var(--color-button-font);\n      transition: background-color .2s ease;\n      opacity: 0.8;\n\n      &:hover {\n        background-color: var(--color-button-background-hover);\n      }\n      &:active {\n        background-color: var(--color-button-background-active);\n      }\n    }\n  }\n  .list {\n    // background-color: @color-search-form-background;\n    font-size: 13px;\n    transition: .3s ease;\n    height: 0;\n    transition-property: height;\n    position: relative;\n    scroll-behavior: smooth;\n\n    li {\n      position: relative;\n      cursor: pointer;\n      padding: 8px 5px;\n      transition: background-color .2s ease;\n      line-height: 1.3;\n      // overflow: hidden;\n      display: flex;\n      flex-flow: row nowrap;\n\n      &.select {\n        background-color: var(--color-primary-dark-100-alpha-700);\n      }\n      border-radius: 4px;\n      // &:last-child {\n      //   border-bottom-left-radius: 4px;\n      //   border-bottom-right-radius: 4px;\n      // }\n    }\n  }\n}\n\n.img {\n  flex: none;\n}\n.text {\n  flex: auto;\n  .mixin-ellipsis-1();\n}\n.albumName {\n  font-size: 12px;\n  opacity: 0.6;\n  .mixin-ellipsis-1();\n}\n.source {\n  flex: none;\n  font-size: 12px;\n  opacity: 0.5;\n  padding: 0 5px;\n  display: flex;\n  align-items: center;\n  // transform: rotate(45deg);\n  // background-color:\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/index.vue",
    "content": "<template>\n  <div :class=\"$style.list\">\n    <div class=\"thead\">\n      <table>\n        <thead>\n          <tr v-if=\"actionButtonsVisible\">\n            <th class=\"num\" style=\"width: 5%;\">#</th>\n            <th class=\"nobreak\">{{ $t('music_name') }}</th>\n            <th class=\"nobreak\" style=\"width: 22%;\">{{ $t('music_singer') }}</th>\n            <th class=\"nobreak\" style=\"width: 22%;\">{{ $t('music_album') }}</th>\n            <th class=\"nobreak\" style=\"width: 9%;\">{{ $t('music_time') }}</th>\n            <th class=\"nobreak\" style=\"width: 16%;\">{{ $t('action') }}</th>\n          </tr>\n          <tr v-else>\n            <th class=\"num\" style=\"width: 5%;\">#</th>\n            <th class=\"nobreak\">{{ $t('music_name') }}</th>\n            <th class=\"nobreak\" style=\"width: 25%;\">{{ $t('music_singer') }}</th>\n            <th class=\"nobreak\" style=\"width: 28%;\">{{ $t('music_album') }}</th>\n            <th class=\"nobreak\" style=\"width: 10%;\">{{ $t('music_time') }}</th>\n          </tr>\n        </thead>\n      </table>\n    </div>\n    <div v-show=\"list.length\" ref=\"dom_listContent\" :class=\"$style.content\">\n      <base-virtualized-list\n        v-if=\"actionButtonsVisible\" ref=\"listRef\" v-slot=\"{ item, index }\" :list=\"list\" key-name=\"id\"\n        :item-height=\"listItemHeight\" container-class=\"scroll\" content-class=\"list\"\n        @scroll=\"saveListPosition\" @contextmenu.capture=\"handleListRightClick\"\n      >\n        <div\n          class=\"list-item\" :class=\"[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]\"\n          @click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n        >\n          <div class=\"list-item-cell no-select\" :class=\"$style.num\" style=\"flex: 0 0 5%;\">\n            <transition name=\"play-active\">\n              <div v-if=\"playerInfo.isPlayList && playerInfo.playIndex === index\" :class=\"$style.playIcon\">\n                <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n                  <use xlink:href=\"#icon-play-outline\" />\n                </svg>\n              </div>\n              <div v-else class=\"num\">{{ index + 1 }}</div>\n            </transition>\n          </div>\n          <div class=\"list-item-cell auto name\" :aria-label=\"item.name\">\n            <span class=\"select name\">{{ item.name }}</span>\n            <span v-if=\"isShowSource\" class=\"no-select label-source\">{{ item.source }}</span>\n          </div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 22%;\"><span class=\"select\" :aria-label=\"item.singer\">{{ item.singer }}</span></div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 22%;\"><span class=\"select\" :aria-label=\"item.meta.albumName\">{{ item.meta.albumName }}</span></div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 9%;\"><span class=\"no-select\">{{ item.interval || '--/--' }}</span></div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 16%; padding-left: 0; padding-right: 0;\">\n            <material-list-buttons :index=\"index\" :download-btn=\"assertApiSupport(item.source) && item.source != 'local'\" @btn-click=\"handleListBtnClick\" />\n          </div>\n        </div>\n      </base-virtualized-list>\n      <base-virtualized-list\n        v-else ref=\"listRef\" v-slot=\"{ item, index }\" :list=\"list\" key-name=\"id\"\n        :item-height=\"listItemHeight\" container-class=\"scroll\" content-class=\"list\"\n        @scroll=\"saveListPosition\" @contextmenu.capture=\"handleListRightClick\"\n      >\n        <div\n          class=\"list-item\"\n          :class=\"[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]\"\n          @click=\"handleListItemClick($event, index)\" @contextmenu=\"handleListItemRightClick($event, index)\"\n        >\n          <div class=\"list-item-cell no-select\" :class=\"$style.num\" style=\"flex: 0 0 5%;\">\n            <transition name=\"play-active\">\n              <div v-if=\"playerInfo.isPlayList && playerInfo.playIndex === index\" :class=\"$style.playIcon\">\n                <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n                  <use xlink:href=\"#icon-play-outline\" />\n                </svg>\n              </div>\n              <div v-else class=\"num\">{{ index + 1 }}</div>\n            </transition>\n          </div>\n          <div class=\"list-item-cell auto name\">\n            <span class=\"select name\" :aria-label=\"item.name\">{{ item.name }}</span>\n            <span v-if=\"isShowSource\" class=\"no-select label-source\">{{ item.source }}</span>\n          </div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 25%;\"><span class=\"select\" :aria-label=\"item.singer\">{{ item.singer }}</span></div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 28%;\"><span class=\"select\" :aria-label=\"item.meta.albumName\">{{ item.meta.albumName }}</span></div>\n          <div class=\"list-item-cell\" style=\"flex: 0 0 10%;\"><span class=\"no-select\">{{ item.interval || '--/--' }}</span></div>\n        </div>\n      </base-virtualized-list>\n    </div>\n    <div v-show=\"!list.length\" :class=\"$style.noItem\">\n      <p v-text=\"$t('no_item')\" />\n    </div>\n    <common-list-add-modal\n      v-model:show=\"isShowListAdd\" :is-move=\"isMove\" :from-list-id=\"listId\"\n      :music-info=\"selectedAddMusicInfo\" :exclude-list-id=\"excludeListIds\" teleport=\"#view\"\n    />\n    <common-list-add-multiple-modal\n      v-model:show=\"isShowListAddMultiple\" :from-list-id=\"listId\"\n      :is-move=\"isMoveMultiple\" :music-list=\"selectedList\" :exclude-list-id=\"excludeListIds\" teleport=\"#view\" @confirm=\"removeAllSelect\"\n    />\n    <common-download-modal v-model:show=\"isShowDownload\" :music-info=\"selectedDownloadMusicInfo\" teleport=\"#view\" :list-id=\"listId\" />\n    <common-download-multiple-modal v-model:show=\"isShowDownloadMultiple\" :list=\"selectedList\" teleport=\"#view\" :list-id=\"listId\" @confirm=\"removeAllSelect\" />\n    <search-list :list=\"list\" :visible=\"isShowSearchBar\" @action=\"handleMusicSearchAction\" />\n    <music-sort-modal v-model:show=\"isShowMusicSortModal\" :music-info=\"selectedSortMusicInfo\" :selected-num=\"selectedNum\" @confirm=\"sortMusic\" />\n    <music-toggle-modal v-model:show=\"isShowMusicToggleModal\" :music-info=\"selectedToggleMusicInfo\" @toggle=\"toggleSource\" />\n    <base-menu v-model=\"isShowItemMenu\" :menus=\"menus\" :xy=\"menuLocation\" item-name=\"name\" @menu-click=\"handleMenuClick\" />\n  </div>\n</template>\n\n<script>\nimport { clipboardWriteText } from '@common/utils/electron'\nimport { assertApiSupport } from '@renderer/store/utils'\nimport SearchList from './components/SearchList.vue'\nimport MusicSortModal from './components/MusicSortModal.vue'\nimport MusicToggleModal from './components/MusicToggleModal.vue'\nimport useListInfo from './useListInfo'\nimport useList from './useList'\nimport useMenu from './useMenu'\nimport usePlay from './usePlay'\nimport useMusicDownload from './useMusicDownload'\nimport useMusicAdd from './useMusicAdd'\nimport useSort from './useSort'\nimport useMusicActions from './useMusicActions'\nimport useSearch from './useSearch'\nimport useListScroll from './useListScroll'\nimport useMusicToggle from './useMusicToggle'\nimport { appSetting } from '@renderer/store/setting'\nexport default {\n  name: 'MusicList',\n  components: {\n    SearchList,\n    MusicSortModal,\n    MusicToggleModal,\n  },\n  props: {\n    listId: {\n      type: String,\n      required: true,\n    },\n  },\n  emits: ['show-menu'],\n  setup(props, { emit }) {\n    const actionButtonsVisible = appSetting['list.actionButtonsVisible']\n\n    let scrollIndex = null\n    let isAnimation = false\n    const handleRestoreScroll = (_scrollIndex, _isAnimation) => {\n      scrollIndex = _scrollIndex\n      isAnimation = _isAnimation\n      if (isAnimation) void restoreScroll(scrollIndex, isAnimation)\n      // console.log('handleRestoreScroll', scrollIndex, isAnimation)\n    }\n    const onLoadedList = () => {\n      // console.log('restoreScroll', scrollIndex, isAnimation)\n      void restoreScroll(scrollIndex, isAnimation)\n    }\n\n    const {\n      rightClickSelectedIndex,\n      selectedIndex,\n      dom_listContent,\n      listRef,\n      list,\n      playerInfo,\n      setSelectedIndex,\n      isShowSource,\n      excludeListIds,\n    } = useListInfo({ props, onLoadedList })\n\n    const {\n      selectedList,\n      listItemHeight,\n      handleSelectData,\n      removeAllSelect,\n    } = useList({ listRef, list })\n\n    const {\n      handlePlayMusic,\n      handlePlayMusicLater,\n      doubleClickPlay,\n    } = usePlay({ props, selectedList, list, removeAllSelect })\n\n    const {\n      isShowListAdd,\n      isMove,\n      isShowListAddMultiple,\n      isMoveMultiple,\n      selectedAddMusicInfo,\n      handleShowMusicAddModal,\n      handleShowMusicMoveModal,\n    } = useMusicAdd({ selectedList, list })\n\n    const {\n      isShowDownload,\n      isShowDownloadMultiple,\n      selectedDownloadMusicInfo,\n      handleShowDownloadModal,\n    } = useMusicDownload({ selectedList, list })\n\n    const {\n      isShowMusicSortModal,\n      selectedNum,\n      selectedSortMusicInfo,\n      handleShowSortModal,\n      sortMusic,\n    } = useSort({ props, list, selectedList, removeAllSelect })\n\n    const {\n      handleShowMusicToggleModal,\n      isShowMusicToggleModal,\n      selectedToggleMusicInfo,\n      toggleSource,\n    } = useMusicToggle(props, list)\n\n    const {\n      handleSearch,\n      handleOpenMusicDetail,\n      handleCopyName,\n      handleDislikeMusic,\n      handleRemoveMusic,\n    } = useMusicActions({ props, list, removeAllSelect, selectedList })\n\n    const {\n      menus,\n      menuLocation,\n      isShowItemMenu,\n      showMenu,\n      menuClick,\n    } = useMenu({\n      assertApiSupport,\n      emit,\n\n      handleShowDownloadModal,\n      handlePlayMusic,\n      handlePlayMusicLater,\n      handleShowMusicToggleModal,\n      handleSearch,\n      handleShowMusicAddModal,\n      handleShowMusicMoveModal,\n      handleShowSortModal,\n      handleOpenMusicDetail,\n      handleCopyName,\n      handleDislikeMusic,\n      handleRemoveMusic,\n    })\n\n    const {\n      isShowSearchBar,\n      searchList,\n      handleMusicSearchAction,\n    } = useSearch({\n      setSelectedIndex,\n      handlePlayMusic,\n      listRef,\n    })\n\n    const { saveListPosition, restoreScroll } = useListScroll({ props, listRef, list, handleRestoreScroll })\n\n\n    const handleListItemClick = (event, index) => {\n      if (rightClickSelectedIndex.value > -1) return\n      handleSelectData(index)\n      doubleClickPlay(index)\n    }\n    const handleListItemRightClick = (event, index) => {\n      rightClickSelectedIndex.value = index\n      showMenu(event, list.value[index], index)\n    }\n    const handleMenuClick = (action) => {\n      let index = rightClickSelectedIndex.value\n      rightClickSelectedIndex.value = -1\n      menuClick(action, index)\n    }\n    const handleListRightClick = (event) => {\n      if (!event.target.classList.contains('select')) return\n      event.stopImmediatePropagation()\n      let classList = dom_listContent.value.classList\n      classList.add('copying')\n      window.requestAnimationFrame(() => {\n        let str = window.getSelection().toString()\n        classList.remove('copying')\n        str = str.split(/\\n\\n/).map(s => s.replace(/\\n/g, '  ')).join('\\n').trim()\n        if (!str.length) return\n        clipboardWriteText(str)\n      })\n    }\n    const handleListBtnClick = ({ action, index }) => {\n      switch (action) {\n        case 'download':\n          handleShowDownloadModal(index, true)\n          break\n        case 'play':\n          handlePlayMusic(index, true)\n          break\n        case 'search':\n          handleSearch(index)\n          break\n        case 'listAdd':\n          handleShowMusicAddModal(index, true)\n          break\n      }\n    }\n    const scrollToTop = () => {\n      listRef.value.scrollTo(0, true)\n    }\n\n    return {\n      listItemHeight,\n      handleListItemClick,\n      selectedList,\n      handleListItemRightClick,\n      removeAllSelect,\n      handleListBtnClick,\n      rightClickSelectedIndex,\n      selectedIndex,\n      dom_listContent,\n      listRef,\n      excludeListIds,\n\n      menus,\n      isShowItemMenu,\n      menuLocation,\n      handleMenuClick,\n\n      handleListRightClick,\n      assertApiSupport,\n\n      isShowListAdd,\n      isMove,\n      isShowListAddMultiple,\n      isMoveMultiple,\n      selectedAddMusicInfo,\n\n      isShowMusicSortModal,\n      selectedNum,\n      selectedSortMusicInfo,\n      sortMusic,\n\n      isShowDownload,\n      isShowDownloadMultiple,\n      selectedDownloadMusicInfo,\n\n      scrollToTop,\n\n      isShowSearchBar,\n      searchList,\n      handleMusicSearchAction,\n\n      list,\n      playerInfo,\n\n      saveListPosition,\n      isShowSource,\n      handleRestoreScroll,\n\n      actionButtonsVisible,\n\n      isShowMusicToggleModal,\n      selectedToggleMusicInfo,\n      toggleSource,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.list {\n  overflow: hidden;\n  height: 100%;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n\n  :global(.list-item) {\n    &.active {\n      color: var(--color-button-font);\n    }\n  }\n  :global {\n    .label-source {\n      color: var(--color-primary);\n      padding: 5px;\n      font-size: .8em;\n      line-height: 1.2;\n      opacity: .75;\n      display: inline-block;\n    }\n  }\n}\n.num {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n}\n.playIcon {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  color: var(--color-button-font);\n  opacity: .7;\n}\n.content {\n  min-height: 0;\n  font-size: 14px;\n  display: flex;\n  flex-flow: column nowrap;\n  flex: auto;\n}\n\n.noItem {\n  position: relative;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useList.js",
    "content": "import { computed, watch, ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { isFullscreen } from '@renderer/store'\nimport { getFontSizeWithScreen } from '@renderer/utils'\nimport { appSetting } from '@renderer/store/setting'\n\nconst useKeyEvent = ({ handleSelectAllData, listRef }) => {\n  const keyEvent = {\n    isShiftDown: false,\n    isModDown: false,\n  }\n\n  const handle_key_shift_down = () => {\n    keyEvent.isShiftDown ||= true\n  }\n  const handle_key_shift_up = () => {\n    keyEvent.isShiftDown &&= false\n  }\n  const handle_key_mod_down = () => {\n    keyEvent.isModDown ||= true\n  }\n  const handle_key_mod_up = () => {\n    keyEvent.isModDown &&= false\n  }\n  const handle_key_mod_a_down = ({ event }) => {\n    if (event.target.tagName == 'INPUT' || document.activeElement != listRef.value?.$el) return\n    event.preventDefault()\n    if (event.repeat) return\n    keyEvent.isModDown = false\n    handleSelectAllData()\n  }\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_shift_down', handle_key_shift_down)\n    window.key_event.off('key_shift_up', handle_key_shift_up)\n    window.key_event.off('key_mod_down', handle_key_mod_down)\n    window.key_event.off('key_mod_up', handle_key_mod_up)\n    window.key_event.off('key_mod+a_down', handle_key_mod_a_down)\n  })\n  window.key_event.on('key_shift_down', handle_key_shift_down)\n  window.key_event.on('key_shift_up', handle_key_shift_up)\n  window.key_event.on('key_mod_down', handle_key_mod_down)\n  window.key_event.on('key_mod_up', handle_key_mod_up)\n  window.key_event.on('key_mod+a_down', handle_key_mod_a_down)\n\n  return keyEvent\n}\n\nexport default ({ listRef, list }) => {\n  const selectedList = ref([])\n\n  let lastSelectIndex = -1\n  const listItemHeight = computed(() => {\n    return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)\n  })\n\n  const removeAllSelect = () => {\n    selectedList.value = []\n  }\n  const handleSelectAllData = () => {\n    removeAllSelect()\n    selectedList.value = [...list.value]\n  }\n  const keyEvent = useKeyEvent({ listRef, handleSelectAllData })\n\n  const handleSelectData = clickIndex => {\n    if (keyEvent.isShiftDown) {\n      if (selectedList.value.length) {\n        removeAllSelect()\n        if (lastSelectIndex != clickIndex) {\n          let _lastSelectIndex = lastSelectIndex\n          let isNeedReverse = false\n          if (clickIndex < _lastSelectIndex) {\n            let temp = _lastSelectIndex\n            _lastSelectIndex = clickIndex\n            clickIndex = temp\n            isNeedReverse = true\n          }\n          selectedList.value = list.value.slice(_lastSelectIndex, clickIndex + 1)\n          if (isNeedReverse) selectedList.value.reverse()\n        }\n      } else {\n        selectedList.value.push(list.value[clickIndex])\n        lastSelectIndex = clickIndex\n      }\n    } else if (keyEvent.isModDown) {\n      lastSelectIndex = clickIndex\n      let item = list.value[clickIndex]\n      let index = selectedList.value.indexOf(item)\n      if (index < 0) {\n        selectedList.value.push(item)\n      } else {\n        selectedList.value.splice(index, 1)\n      }\n    } else if (selectedList.value.length) {\n      removeAllSelect()\n    }\n  }\n\n  watch(list, removeAllSelect)\n\n  return {\n    selectedList,\n    listItemHeight,\n    removeAllSelect,\n    handleSelectData,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useListInfo.js",
    "content": "import { ref, watch, computed, onBeforeUnmount } from '@common/utils/vueTools'\nimport { playMusicInfo, playInfo } from '@renderer/store/player/state'\nimport { getListMusics } from '@renderer/store/list/action'\nimport { appSetting } from '@renderer/store/setting'\n\n\nexport default ({ props, onLoadedList }) => {\n  const rightClickSelectedIndex = ref(-1)\n  const selectedIndex = ref(-1)\n  const dom_listContent = ref(null)\n  const listRef = ref(null)\n\n  const excludeListIds = computed(() => ([props.listId]))\n\n\n  const list = ref([])\n  watch(() => props.listId, id => {\n    getListMusics(id).then(l => {\n      list.value = [...l]\n      if (id != props.listId) return\n      onLoadedList()\n    })\n  }, {\n    immediate: true,\n  })\n\n  const playerInfo = computed(() => ({\n    isPlayList: playMusicInfo.listId == props.listId,\n    playIndex: playInfo.playIndex,\n  }))\n\n  const setSelectedIndex = index => {\n    selectedIndex.value = index\n  }\n\n  const isShowSource = computed(() => appSetting['list.isShowSource'])\n\n  const handleMyListUpdate = (ids) => {\n    if (!ids.includes(props.listId)) return\n    getListMusics(props.listId).then(l => {\n      list.value = [...l]\n    })\n  }\n\n  window.app_event.on('myListUpdate', handleMyListUpdate)\n\n  onBeforeUnmount(() => {\n    window.app_event.off('myListUpdate', handleMyListUpdate)\n  })\n\n  return {\n    rightClickSelectedIndex,\n    selectedIndex,\n    dom_listContent,\n    listRef,\n    list,\n    playerInfo,\n    setSelectedIndex,\n    isShowSource,\n    excludeListIds,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useListScroll.js",
    "content": "import { onMounted, onBeforeUnmount } from '@common/utils/vueTools'\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\nimport { setListPosition, getListPosition } from '@renderer/utils/data'\nimport { appSetting } from '@renderer/store/setting'\n\nexport default ({ props, listRef, list, handleRestoreScroll }) => {\n  const route = useRoute()\n  const router = useRouter()\n\n  const saveListPosition = () => {\n    setListPosition(props.listId, listRef.value?.getScrollTop() || 0)\n  }\n\n  const handleScrollList = (index, isAnimation, callback = () => {}) => {\n    listRef.value.scrollToIndex(index, -150, isAnimation, callback)\n  }\n\n  const restoreScroll = async(index, isAnimation) => {\n    // console.log(index, isAnimation)\n    if (!list.value.length) return\n    if (index == null) {\n      let location = await getListPosition(props.listId) || 0\n      if (appSetting['list.isSaveScrollLocation'] && location != null) {\n        listRef.value?.scrollTo(location)\n      }\n      return\n    }\n\n    handleScrollList(index, isAnimation)\n  }\n\n  onMounted(() => {\n    handleRestoreScroll(route.query.scrollIndex, false)\n    if (route.query.scrollIndex != null) {\n      router.replace({\n        path: '/list',\n        query: {\n          id: props.listId,\n          updated: true,\n        },\n      })\n    }\n  })\n  onBeforeUnmount(() => {\n    saveListPosition()\n  })\n\n  return {\n    saveListPosition,\n    restoreScroll,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useMenu.js",
    "content": "import { computed, ref, shallowReactive, reactive, nextTick } from '@common/utils/vueTools'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { hasDislike } from '@renderer/core/dislikeList'\n\nexport default ({\n  assertApiSupport,\n  emit,\n\n  handleShowDownloadModal,\n  handlePlayMusic,\n  handlePlayMusicLater,\n  handleSearch,\n  handleShowMusicToggleModal,\n  handleShowMusicAddModal,\n  handleShowMusicMoveModal,\n  handleShowSortModal,\n  handleOpenMusicDetail,\n  handleCopyName,\n  handleDislikeMusic,\n  handleRemoveMusic,\n}) => {\n  const itemMenuControl = reactive({\n    play: true,\n    playLater: true,\n    copyName: true,\n    addTo: true,\n    moveTo: true,\n    sort: true,\n    toggleSource: true,\n    download: true,\n    search: true,\n    dislike: true,\n    remove: true,\n    sourceDetail: true,\n  })\n  const t = useI18n()\n  const menuLocation = shallowReactive({ x: 0, y: 0 })\n  const isShowItemMenu = ref(false)\n\n  const menus = computed(() => {\n    return [\n      {\n        name: t('list__play'),\n        action: 'play',\n        disabled: !itemMenuControl.play,\n      },\n      {\n        name: t('list__download'),\n        action: 'download',\n        disabled: !itemMenuControl.download,\n      },\n      {\n        name: t('list__play_later'),\n        action: 'playLater',\n        disabled: !itemMenuControl.playLater,\n      },\n      {\n        name: t('list__add_to'),\n        action: 'addTo',\n        disabled: !itemMenuControl.addTo,\n      },\n      {\n        name: t('list__move_to'),\n        action: 'moveTo',\n        disabled: !itemMenuControl.moveTo,\n      },\n      {\n        name: t('list__sort'),\n        action: 'sort',\n        disabled: !itemMenuControl.sort,\n      },\n      {\n        name: t('list__toggle_source'),\n        action: 'toggleSource',\n        disabled: !itemMenuControl.toggleSource,\n      },\n      {\n        name: t('list__copy_name'),\n        action: 'copyName',\n        disabled: !itemMenuControl.copyName,\n      },\n      {\n        name: t('list__source_detail'),\n        action: 'sourceDetail',\n        disabled: !itemMenuControl.sourceDetail,\n      },\n      {\n        name: t('list__search'),\n        action: 'search',\n        disabled: !itemMenuControl.search,\n      },\n      {\n        name: t('list__dislike'),\n        action: 'dislike',\n        disabled: !itemMenuControl.dislike,\n      },\n      {\n        name: t('list__remove'),\n        action: 'remove',\n        disabled: !itemMenuControl.remove,\n      },\n    ]\n  })\n\n  const showMenu = (event, musicInfo) => {\n    itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source]?.getMusicDetailPageUrl\n    // itemMenuControl.play =\n    //   itemMenuControl.playLater =\n    itemMenuControl.download = assertApiSupport(musicInfo.source) && musicInfo.source != 'local'\n\n    itemMenuControl.dislike = !hasDislike(musicInfo)\n\n    menuLocation.x = event.pageX\n    menuLocation.y = event.pageY\n\n    if (isShowItemMenu.value) return\n\n    emit('show-menu')\n    nextTick(() => {\n      isShowItemMenu.value = true\n    })\n  }\n\n  const hideMenu = () => {\n    isShowItemMenu.value = false\n  }\n\n  const menuClick = (action, index) => {\n    // console.log(action)\n    hideMenu()\n    if (!action) return\n    switch (action.action) {\n      case 'play':\n        handlePlayMusic(index)\n        break\n      case 'playLater':\n        handlePlayMusicLater(index)\n        break\n      case 'copyName':\n        handleCopyName(index)\n        break\n      case 'addTo':\n        handleShowMusicAddModal(index)\n        break\n      case 'moveTo':\n        handleShowMusicMoveModal(index)\n        break\n      case 'sort':\n        handleShowSortModal(index)\n        break\n      case 'toggleSource':\n        handleShowMusicToggleModal(index)\n        break\n      case 'download':\n        handleShowDownloadModal(index)\n        break\n      case 'search':\n        handleSearch(index)\n        break\n      case 'dislike':\n        handleDislikeMusic(index)\n        break\n      case 'remove':\n        handleRemoveMusic(index)\n        break\n      case 'sourceDetail':\n        handleOpenMusicDetail(index)\n    }\n  }\n\n  return {\n    menus,\n    menuLocation,\n    isShowItemMenu,\n    showMenu,\n    menuClick,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useMusicActions.js",
    "content": "import { useRouter } from '@common/utils/vueRouter'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { openUrl, clipboardWriteText } from '@common/utils/electron'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { removeListMusics } from '@renderer/store/list/action'\nimport { appSetting } from '@renderer/store/setting'\nimport { toOldMusicInfo } from '@renderer/utils/index'\nimport { addDislikeInfo, hasDislike } from '@renderer/core/dislikeList'\nimport { playNext } from '@renderer/core/player'\nimport { playMusicInfo } from '@renderer/store/player/state'\n\n\nexport default ({ props, list, selectedList, removeAllSelect }) => {\n  const router = useRouter()\n  const t = useI18n()\n\n  const handleSearch = index => {\n    const info = list.value[index]\n    router.push({\n      path: '/search',\n      query: {\n        text: `${info.name} ${info.singer}`,\n      },\n    })\n  }\n\n  const handleOpenMusicDetail = index => {\n    const minfo = list.value[index]\n    const url = musicSdk[minfo.source]?.getMusicDetailPageUrl(toOldMusicInfo(minfo))\n    if (!url) return\n    openUrl(url)\n  }\n\n  const handleCopyName = index => {\n    const minfo = list.value[index]\n    clipboardWriteText(appSetting['download.fileName'].replace('歌名', minfo.name).replace('歌手', minfo.singer))\n  }\n\n  const handleDislikeMusic = async(index) => {\n    const minfo = list.value[index]\n    const confirm = await dialog.confirm({\n      message: minfo.singer ? t('lists__dislike_music_singer_tip', { name: minfo.name, singer: minfo.singer }) : t('lists__dislike_music_tip', { name: minfo.name }),\n      cancelButtonText: t('cancel_button_text_2'),\n      confirmButtonText: t('confirm_button_text'),\n    })\n    if (!confirm) return\n    await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }])\n    if (hasDislike(playMusicInfo.musicInfo)) {\n      playNext(true)\n    }\n  }\n\n  const handleRemoveMusic = async(index, single) => {\n    if (selectedList.value.length && !single) {\n      const confirm = await (selectedList.value.length > 1\n        ? dialog.confirm({\n          message: t('lists__remove_music_tip', { len: selectedList.value.length }),\n          confirmButtonText: t('lists__remove_tip_button'),\n        })\n        : Promise.resolve(true)\n      )\n      if (!confirm) return\n      removeListMusics({ listId: props.listId, ids: selectedList.value.map(m => m.id) })\n      removeAllSelect()\n    } else {\n      removeListMusics({ listId: props.listId, ids: [list.value[index].id] })\n    }\n  }\n\n  return {\n    handleSearch,\n    handleOpenMusicDetail,\n    handleCopyName,\n    handleDislikeMusic,\n    handleRemoveMusic,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useMusicAdd.js",
    "content": "import { ref, nextTick } from '@common/utils/vueTools'\n\nexport default ({ selectedList, list }) => {\n  const isShowListAdd = ref(false)\n  const isMove = ref(false)\n  const isMoveMultiple = ref(false)\n  const isShowListAddMultiple = ref(false)\n  const selectedAddMusicInfo = ref(null)\n\n  const handleShowMusicAddModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isMoveMultiple.value = false\n      isShowListAddMultiple.value = true\n    } else {\n      isMove.value = false\n      selectedAddMusicInfo.value = list.value[index]\n      nextTick(() => {\n        isShowListAdd.value = true\n      })\n    }\n  }\n\n  const handleShowMusicMoveModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isMoveMultiple.value = true\n      isShowListAddMultiple.value = true\n    } else {\n      isMove.value = true\n      selectedAddMusicInfo.value = list.value[index]\n      nextTick(() => {\n        isShowListAdd.value = true\n      })\n    }\n  }\n\n  return {\n    isShowListAdd,\n    isMove,\n    isMoveMultiple,\n    isShowListAddMultiple,\n    selectedAddMusicInfo,\n    handleShowMusicAddModal,\n    handleShowMusicMoveModal,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useMusicDownload.js",
    "content": "import { ref, nextTick } from '@common/utils/vueTools'\n\nexport default ({ selectedList, list }) => {\n  const isShowDownload = ref(false)\n  const isShowDownloadMultiple = ref(false)\n  const musicInfo = ref(null)\n\n  const handleShowDownloadModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      isShowDownloadMultiple.value = true\n    } else {\n      musicInfo.value = list.value[index]\n      nextTick(() => {\n        isShowDownload.value = true\n      })\n    }\n  }\n\n  return {\n    isShowDownload,\n    isShowDownloadMultiple,\n    selectedDownloadMusicInfo: musicInfo,\n    handleShowDownloadModal,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useMusicToggle.js",
    "content": "// import { updateListMusicsPosition } from '@renderer/store/list/action'\nimport { ref, nextTick } from '@common/utils/vueTools'\nimport { removeListMusics } from '@renderer/store/list/listManage'\nimport { playListById } from '@renderer/core/player'\nimport { addListMusics, updateListMusicsPosition } from '@renderer/store/list/action'\nimport { playMusicInfo } from '@renderer/store/player/state'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { useI18n } from '@renderer/plugins/i18n'\n\nexport default (props, list) => {\n  const isShowMusicToggleModal = ref(false)\n  const musicInfo = ref(null)\n  const t = useI18n()\n\n  const handleShowMusicToggleModal = (index) => {\n    musicInfo.value = list.value[index]\n    nextTick(() => {\n      isShowMusicToggleModal.value = true\n    })\n  }\n\n  const toggleSource = async(toggleMusicInfo) => {\n    const oldId = musicInfo.value.id\n    let oldIdx = list.value.findIndex(m => m.id == oldId)\n    if (oldIdx < 0) {\n      isShowMusicToggleModal.value = false\n      await addListMusics(props.listId, [toggleMusicInfo])\n      return\n    }\n    const id = toggleMusicInfo.id\n    const index = list.value.findIndex(m => m.id == id)\n    const removeIds = [oldId]\n    if (index > -1) {\n      if (!await dialog.confirm({\n        message: t('music_toggle_duplicate_tip'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('confirm_button_text'),\n      })) return\n      removeIds.push(id)\n    }\n    isShowMusicToggleModal.value = false\n    await removeListMusics({ listId: props.listId, ids: removeIds })\n    await addListMusics(props.listId, [toggleMusicInfo], 'bottom')\n    if (index != -1 && index < oldIdx) oldIdx--\n    await updateListMusicsPosition({ listId: props.listId, ids: [id], position: oldIdx })\n    if (playMusicInfo.listId == props.listId && playMusicInfo.musicInfo?.id == oldId) {\n      playListById(props.listId, toggleMusicInfo.id)\n    }\n  }\n\n  return {\n    isShowMusicToggleModal,\n    selectedToggleMusicInfo: musicInfo,\n    handleShowMusicToggleModal,\n    toggleSource,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/usePlay.js",
    "content": "import { addTempPlayList } from '@renderer/store/player/action'\nimport { playList } from '@renderer/core/player'\n\nexport default ({ props, selectedList, list, removeAllSelect }) => {\n  let clickTime = 0\n  let clickIndex = -1\n\n  const handlePlayMusic = (index) => {\n    playList(props.listId, index)\n  }\n\n  const handlePlayMusicLater = (index, single) => {\n    if (selectedList.value.length && !single) {\n      addTempPlayList(selectedList.value.map(s => ({ listId: props.listId, musicInfo: s })))\n      removeAllSelect()\n    } else {\n      addTempPlayList([{ listId: props.listId, musicInfo: list.value[index] }])\n    }\n  }\n\n  const doubleClickPlay = index => {\n    if (\n      window.performance.now() - clickTime > 400 ||\n      clickIndex !== index\n    ) {\n      clickTime = window.performance.now()\n      clickIndex = index\n      return\n    }\n    handlePlayMusic(index, true)\n    clickTime = 0\n    clickIndex = -1\n  }\n\n  return {\n    handlePlayMusic,\n    handlePlayMusicLater,\n    doubleClickPlay,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useSearch.js",
    "content": "import { ref, onBeforeUnmount } from '@common/utils/vueTools'\n\nexport default ({ setSelectedIndex, handlePlayMusic, listRef }) => {\n  const isShowSearchBar = ref(false)\n  const searchList = ref([])\n\n  const handleShowSearchBar = () => {\n    isShowSearchBar.value = true\n  }\n\n  const handleMusicSearchAction = ({ action, data: { index, isPlay } = {} }) => {\n    isShowSearchBar.value = false\n    switch (action) {\n      case 'listClick':\n        if (index < 0) return\n        listRef.value.scrollToIndex(index, -150, true, () => {\n          setSelectedIndex(index)\n          setTimeout(() => {\n            setSelectedIndex(-1)\n            if (isPlay) handlePlayMusic(index)\n          }, 600)\n        })\n        break\n    }\n  }\n\n  window.key_event.on('key_mod+f_down', handleShowSearchBar)\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_mod+f_down', handleShowSearchBar)\n  })\n\n  return {\n    isShowSearchBar,\n    searchList,\n    handleMusicSearchAction,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MusicList/useSort.js",
    "content": "import { updateListMusicsPosition } from '@renderer/store/list/action'\nimport { ref, nextTick } from '@common/utils/vueTools'\n\nexport default ({ props, list, selectedList, removeAllSelect }) => {\n  const isShowMusicSortModal = ref(false)\n  const selectedNum = ref(0)\n  const musicInfo = ref(null)\n\n  const handleShowSortModal = (index, single) => {\n    if (selectedList.value.length && !single) {\n      selectedNum.value = selectedList.value.length\n    } else {\n      selectedNum.value = 0\n      musicInfo.value = list.value[index]\n    }\n    nextTick(() => {\n      isShowMusicSortModal.value = true\n    })\n  }\n\n  const sortMusic = num => {\n    num = Math.min(num, list.value.length)\n    updateListMusicsPosition({\n      listId: props.listId,\n      position: num - 1,\n      ids: (selectedNum.value ? [...selectedList.value] : [musicInfo.value]).map(m => m.id),\n    })\n    removeAllSelect()\n    isShowMusicSortModal.value = false\n  }\n\n  return {\n    isShowMusicSortModal,\n    selectedNum,\n    selectedSortMusicInfo: musicInfo,\n    handleShowSortModal,\n    sortMusic,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/actions.ts",
    "content": "import { addListMusics, setFetchingListStatus } from '@renderer/store/list/action'\nimport { showSelectDialog } from '@renderer/utils/ipc'\n\n\nconst handleAddMusics = async(listId: string, filePaths: string[], index: number = -1) => {\n  // console.log(index + 1, index + 201)\n  const paths = filePaths.slice(index + 1, index + 201)\n  const musicInfos = await window.lx.worker.main.createLocalMusicInfos(paths)\n  if (musicInfos.length) await addListMusics(listId, musicInfos)\n  index += 200\n  if (filePaths.length - 1 > index) await handleAddMusics(listId, filePaths, index)\n}\nexport const addLocalFile = async(listInfo: LX.List.MyListInfo) => {\n  const { canceled, filePaths } = await showSelectDialog({\n    title: window.i18n.t('lists__add_local_file_desc'),\n    properties: ['openFile', 'multiSelections'],\n    filters: [\n      // https://support.google.com/chromebook/answer/183093\n      // 3gp, .avi, .mov, .m4v, .m4a, .mp3, .mkv, .ogm, .ogg, .oga, .webm, .wav\n      { name: 'Media File', extensions: ['mp3', 'flac', 'ogg', 'oga', 'wav', 'm4a'] },\n      // { name: 'All Files', extensions: ['*'] },\n    ],\n  })\n  if (canceled || !filePaths.length) return\n\n  console.log(filePaths)\n  setFetchingListStatus(listInfo.id, true)\n  await handleAddMusics(listInfo.id, filePaths)\n  setFetchingListStatus(listInfo.id, false)\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/components/DuplicateMusicModal.vue",
    "content": "<template>\n  <material-modal :show=\"visible\" bg-close teleport=\"#view\" width=\"60%\" max-width=\"900px\" @close=\"$emit('update:visible', false)\">\n    <div :class=\"$style.header\">\n      <h2>{{ listName }}</h2>\n    </div>\n    <base-virtualized-list\n      v-if=\"duplicateList.length\" v-slot=\"{ item, index }\" :list=\"duplicateList\" key-name=\"id\" :class=\"$style.list\" style=\"contain: none;\"\n      :item-height=\"listItemHeight\" container-class=\"scroll\" content-class=\"list\"\n    >\n      <div :class=\"$style.listItem\">\n        <div :class=\"$style.num\">{{ item.index + 1 }}</div>\n        <div :class=\"$style.textContent\">\n          <h3 :class=\"$style.text\" :aria-label=\"`${item.musicInfo.name} - ${item.musicInfo.singer}`\">{{ item.musicInfo.name }} - {{ item.musicInfo.singer }}</h3>\n          <h3 v-if=\"item.musicInfo.meta.albumName\" :class=\"[$style.text, $style.albumName]\" :aria-label=\"item.musicInfo.meta.albumName\">{{ item.musicInfo.meta.albumName }}</h3>\n        </div>\n        <div :class=\"$style.label\">{{ item.musicInfo.source }}</div>\n        <div :class=\"$style.label\">{{ item.musicInfo.interval }}</div>\n        <div :class=\"$style.btns\">\n          <button type=\"button\" :class=\"$style.btn\" @click=\"handlePlay(index)\">\n            <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 287.386 287.386\" space=\"preserve\">\n              <use xlink:href=\"#icon-testPlay\" />\n            </svg>\n          </button>\n          <button type=\"button\" :class=\"$style.btn\" @click=\"handleRemove(index)\">\n            <svg v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"50%\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\">\n              <use xlink:href=\"#icon-delete\" />\n            </svg>\n          </button>\n        </div>\n      </div>\n    </base-virtualized-list>\n    <div v-else :class=\"$style.noItem\">\n      <p v-text=\"$t('no_item')\" />\n    </div>\n  </material-modal>\n</template>\n\n<script>\nimport { ref, watch, computed, markRawList } from '@common/utils/vueTools'\nimport { playList } from '@renderer/core/player'\nimport { getListMusics, removeListMusics } from '@renderer/store/list/action'\nimport { isFullscreen } from '@renderer/store'\nimport { appSetting } from '@renderer/store/setting'\nimport { getFontSizeWithScreen } from '@renderer/utils'\nimport { LIST_IDS } from '@common/constants'\nimport { useI18n } from '@root/lang'\n\nexport default {\n  props: {\n    visible: {\n      type: Boolean,\n      default: false,\n    },\n    listInfo: { // { id: '', name: '' }\n      type: Object,\n      required: true,\n    },\n  },\n  emits: ['update:visible'],\n  setup(props) {\n    const t = useI18n()\n    const duplicateList = ref([])\n    const listItemHeight = computed(() => {\n      return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 3.2)\n    })\n\n    const handlePlay = (index) => {\n      const { index: musicInfoIndex } = duplicateList.value[index]\n      playList(props.listInfo.id, musicInfoIndex)\n    }\n    const handleFilterList = async() => {\n      // console.time('filter')\n      duplicateList.value = markRawList(await window.lx.worker.main.filterDuplicateMusic(await getListMusics(props.listInfo.id)))\n      // console.log(duplicateList.value)\n      // console.timeEnd('filter')\n    }\n    const handleRemove = async(index) => {\n      const { musicInfo: targetMusicInfo } = duplicateList.value.splice(index, 1)[0]\n      duplicateList.value = [...duplicateList.value]\n      await removeListMusics({ listId: props.listInfo.id, ids: [targetMusicInfo.id] })\n      await handleFilterList()\n    }\n\n    watch(() => props.visible, (visible) => {\n      if (visible) {\n        if (duplicateList.value.length) duplicateList.value = []\n        void handleFilterList()\n      }\n    })\n\n    const listName = computed(() => {\n      switch (props.listInfo.id) {\n        case LIST_IDS.DEFAULT:\n        case LIST_IDS.LOVE:\n          return t(props.listInfo.name)\n\n        default: return props.listInfo.name\n      }\n    })\n\n    return {\n      listItemHeight,\n      duplicateList,\n      handleFilterList,\n      handleRemove,\n      handlePlay,\n      listName,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.header {\n  flex: none;\n  padding: 15px;\n  text-align: center;\n  h2 {\n    word-break: break-all;\n  }\n}\n.main {\n  min-height: 175px;\n  min-width: 380px;\n  // display: flex;\n  // flex-flow: column nowrap;\n}\n\n.list {\n  min-height: 175px;\n  min-width: 380px;\n  // background-color: @color-search-form-background;\n  font-size: 13px;\n  transition-property: height;\n  // position: relative;\n  .listItem {\n    position: relative;\n    padding: 0 5px;\n    transition: background-color .2s ease;\n    line-height: 1.4;\n    height: 100%;\n    // overflow: hidden;\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n    }\n    // border-radius: 4px;\n    // &:last-child {\n    //   border-bottom-left-radius: 4px;\n    //   border-bottom-right-radius: 4px;\n    // }\n  }\n}\n\n.num {\n  flex: none;\n  font-size: 12px;\n  width: 30px;\n  text-align: center;\n  color: var(--color-font-label);\n}\n\n.textContent {\n  flex: auto;\n  padding-left: 5px;\n  min-width: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  align-items: flex-start;\n  overflow: hidden;\n}\n.text {\n  max-width: 100%;\n  .mixin-ellipsis-1();\n}\n.albumName {\n  font-size: 12px;\n  opacity: 0.6;\n  // .mixin-ellipsis-1();\n}\n.label {\n  flex: none;\n  font-size: 12px;\n  opacity: 0.5;\n  padding: 0 5px;\n  display: flex;\n  align-items: center;\n  // transform: rotate(45deg);\n  // background-color:\n}\n.btns {\n  flex: none;\n  font-size: 12px;\n  padding: 0 5px;\n  display: flex;\n  align-items: center;\n}\n.btn {\n  background-color: transparent;\n  border: none;\n  border-radius: @form-radius;\n  margin-right: 5px;\n  cursor: pointer;\n  padding: 4px 7px;\n  color: var(--color-button-font);\n  outline: none;\n  transition: background-color 0.2s ease;\n  line-height: 0;\n  &:last-child {\n    margin-right: 0;\n  }\n\n  svg {\n    height: 16px;\n  }\n\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-font-active);\n  }\n}\n\n.noItem {\n  position: relative;\n  height: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n\n  p {\n    font-size: 16px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MyList/components/ListSortModal.vue",
    "content": "<template>\n  <material-modal :show=\"visible\" teleport=\"#view\" bg-close @close=\"closeModal\" @after-leave=\"handleAfterLeave\">\n    <main class=\"scroll\" :class=\"$style.main\">\n      <div :class=\"$style.header\">\n        <h2>{{ listName }}</h2>\n      </div>\n      <section>\n        <h3 :class=\"$style.title\">{{ $t('list_sort_modal_by_field') }}</h3>\n        <ul :class=\"$style.list\">\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_field_name\" v-model=\"sortField\" name=\"list_sort_modal_field\" :aria-label=\"$t('list_sort_modal_by_name')\"\n              need=\"need\" value=\"name\" :disabled=\"disabledSortFislds\" :label=\"$t('list_sort_modal_by_name')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_field_singer\" v-model=\"sortField\" name=\"list_sort_modal_field\"\n              need=\"need\" value=\"singer\" :disabled=\"disabledSortFislds\" :label=\"$t('list_sort_modal_by_singer')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_field_album\" v-model=\"sortField\" name=\"list_sort_modal_field\"\n              need=\"need\" value=\"albumName\" :disabled=\"disabledSortFislds\" :label=\"$t('list_sort_modal_by_album')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_field_time\" v-model=\"sortField\" name=\"list_sort_modal_field\"\n              need=\"need\" value=\"interval\" :disabled=\"disabledSortFislds\" :label=\"$t('list_sort_modal_by_time')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_field_source\" v-model=\"sortField\" name=\"list_sort_modal_field\"\n              need=\"need\" value=\"source\" :disabled=\"disabledSortFislds\" :label=\"$t('list_sort_modal_by_source')\"\n            />\n          </li>\n        </ul>\n      </section>\n      <section>\n        <h3 :class=\"$style.title\">{{ $t('list_sort_modal_by_type') }}</h3>\n        <ul :class=\"$style.list\">\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_type_up\" v-model=\"sortType\" name=\"list_sort_modal_type\"\n              need=\"need\" value=\"up\" :label=\"$t('list_sort_modal_by_up')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_type_down\" v-model=\"sortType\" name=\"list_sort_modal_type\"\n              need=\"need\" value=\"down\" :label=\"$t('list_sort_modal_by_down')\"\n            />\n          </li>\n          <li :class=\"$style.listItem\">\n            <base-checkbox\n              id=\"list_sort_modal_type_random\" v-model=\"sortType\" name=\"list_sort_modal_type\"\n              need=\"need\" value=\"random\" :label=\"$t('list_sort_modal_by_random')\"\n            />\n          </li>\n        </ul>\n      </section>\n      <div :class=\"$style.footer\">\n        <base-btn :class=\"$style.btn\" @click=\"closeModal\">{{ $t('btn_cancel') }}</base-btn>\n        <base-btn :class=\"$style.btn\" @click=\"handleSort\">{{ $t('btn_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { ref, computed } from '@common/utils/vueTools'\n// import { dialog } from '@renderer/plugins/Dialog'\nimport { getListMusics, updateListMusicsPosition } from '@renderer/store/list/action'\nimport { useI18n } from '@root/lang'\nimport { LIST_IDS } from '@common/constants'\n\n\nexport default {\n  props: {\n    visible: {\n      type: Boolean,\n      default: false,\n    },\n    listInfo: { // { id: '', name: '' }\n      type: Object,\n      required: true,\n    },\n  },\n  emits: ['update:visible'],\n  setup(props, { emit }) {\n    const t = useI18n()\n    const sortField = ref('')\n    const sortType = ref('')\n    const closeModal = () => {\n      emit('update:visible', false)\n    }\n    const handleAfterLeave = () => {\n      sortField.value = ''\n      sortType.value = ''\n    }\n    const verify = () => {\n      return !!sortType.value && (!!sortField.value || sortType.value == 'random')\n    }\n    const handleSort = async() => {\n      if (!verify()) return\n      // if (!await dialog.confirm({\n      //   message: t('list_sort_modal_tip_confirm'),\n      //   cancelButtonText: t('cancel_button_text'),\n      //   confirmButtonText: t('confirm_button_text'),\n      // })) return\n\n      let list = [...(await getListMusics(props.listInfo.id))]\n      list = await window.lx.worker.main.sortListMusicInfo(list, sortType.value, sortField.value, window.i18n.locale)\n      console.log(sortType.value, sortField.value)\n\n      closeModal()\n      void updateListMusicsPosition({ listId: props.listInfo.id, position: 0, ids: list.map(m => m.id) })\n    }\n\n    const listName = computed(() => {\n      switch (props.listInfo.id) {\n        case LIST_IDS.DEFAULT:\n          return t(props.listInfo.name)\n        case LIST_IDS.LOVE:\n          return t(props.listInfo.name)\n        default:\n          return props.listInfo.name\n      }\n    })\n\n    const disabledSortFislds = computed(() => {\n      return sortType.value == 'random'\n    })\n\n    return {\n      sortField,\n      sortType,\n      disabledSortFislds,\n      closeModal,\n      handleSort,\n      handleAfterLeave,\n      listName,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.header {\n  flex: none;\n  padding: 15px;\n  text-align: center;\n  h2 {\n    color: var(--color-font);\n    word-break: break-all;\n  }\n}\n\n.main {\n  padding: 0 15px;\n  width: 360px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n}\n.title {\n  font-size: 14px;\n  color: var(--color-font-label);\n  padding: 10px 0 8px;\n}\n.list {\n  display: flex;\n  flex-flow: row wrap;\n  font-size: 14px;\n}\n.listItem {\n  width: (100% / 2);\n  padding-left: 10px;\n  margin-bottom: 8px;\n  box-sizing: border-box;\n}\n.footer {\n  margin: 20px 0 15px auto;\n}\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n\n  +.btn {\n    margin-left: 10px;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MyList/components/ListUpdateModal.vue",
    "content": "<template>\n  <material-modal :show=\"visible\" bg-close teleport=\"#view\" @close=\"$emit('update:visible', false)\">\n    <div :class=\"$style.header\">\n      <h2>{{ $t('list_update_modal__title') }}</h2>\n    </div>\n    <main class=\"scroll\" :class=\"$style.main\">\n      <ul v-if=\"lists.length\" ref=\"dom_list\" :class=\"$style.list\">\n        <li v-for=\"list in lists\" :key=\"list.id\" :class=\"[$style.listItem, {[$style.fetching]: fetchingListStatus[list.id]}]\">\n          <div :class=\"$style.listLeft\">\n            <h3 :class=\"$style.text\">{{ list.name }} <span :class=\"$style.label\">{{ list.source }}</span></h3>\n            <div>\n              <base-checkbox\n                :id=\"`list_auto_update_${list.id}`\" :model-value=\"updateInfo[list.id]?.isAutoUpdate == true\"\n                :class=\"$style.checkbox\" :label=\"$t('list_update_modal__auto_update')\" @change=\"handleChangeAutoUpdate(list, $event)\"\n              />\n              <span :class=\"$style.label\" style=\"vertical-align: text-top;\">{{ listUpdateTimes[list.id] }}</span>\n            </div>\n          </div>\n          <div :class=\"$style.btns\">\n            <button :class=\"$style.btn\" :disabled=\"fetchingListStatus[list.id]\" outline=\"outline\" :aria-label=\"$t('list_update_modal__update')\" @click.stop=\"handleUpdate(list)\">\n              <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" style=\"transform: rotate(45deg);\" viewBox=\"0 0 24 24\" space=\"preserve\">\n                <use xlink:href=\"#icon-refresh\" />\n              </svg>\n            </button>\n          </div>\n        </li>\n      </ul>\n      <div v-else :class=\"$style.noItem\">\n        <p v-text=\"$t('no_item')\" />\n      </div>\n    </main>\n    <div :class=\"$style.footer\">\n      <div :class=\"$style.tips\">{{ $t('list_update_modal__tips') }}</div>\n    </div>\n  </material-modal>\n</template>\n\n<script>\nimport { computed, ref } from '@common/utils/vueTools'\nimport { userLists, fetchingListStatus, listUpdateTimes } from '@renderer/store/list/state'\nimport handleSyncSourceList from '@renderer/store/list/syncSourceList'\nimport musicSdk from '@renderer/utils/musicSdk'\n// import { dateFormat } from '@common/utils/renderer'\nimport { getListUpdateInfo, setListAutoUpdate } from '@renderer/utils/data'\n\nexport default {\n  props: {\n    visible: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:visible'],\n  setup() {\n    const lists = computed(() => userLists.filter(l => !!l.source && !!musicSdk[l.source]?.songList))\n    const updateInfo = ref({})\n    // const updateTimes = ref({})\n\n    void getListUpdateInfo().then((listUpdateInfo) => {\n      updateInfo.value = listUpdateInfo\n      // if (listUpdateTimes._inited) {\n      //   for (const [id, value] of Object.entries(info)) {\n      //     autoUpdate[id] = value.isAutoUpdate == true\n      //   }\n      // } else {\n      //   for (const [id, value] of Object.entries(info)) {\n      //     autoUpdate[id] = value.isAutoUpdate == true\n      //     listUpdateTimes[id] = value.updateTime ? dateFormat(value.updateTime) : ''\n      //   }\n      // }\n      // listUpdateTimes._inited = true\n    })\n\n    const handleUpdate = (targetListInfo) => {\n      void handleSyncSourceList(targetListInfo)\n      // console.log(targetListInfo.list.length, list.length)\n    }\n\n    const handleChangeAutoUpdate = (list, enable) => {\n      void setListAutoUpdate(list.id, enable)\n    }\n\n    return {\n      lists,\n      updateInfo,\n      fetchingListStatus,\n      handleUpdate,\n      handleChangeAutoUpdate,\n      listUpdateTimes,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@width: 460px;\n\n.header {\n  flex: none;\n  padding: 15px;\n  text-align: center;\n  h2 {\n    word-break: break-all;\n  }\n}\n.main {\n  min-height: 115px;\n  width: @width;\n}\n\n.list {\n  // background-color: @color-search-form-background;\n  font-size: 13px;\n  transition-property: height;\n  position: relative;\n  .listItem {\n    position: relative;\n    padding: 15px 10px 15px 15px;\n    transition: .3s ease;\n    transition-property: background-color, opacity;\n    line-height: 1.3;\n    // overflow: hidden;\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n    }\n    // border-radius: 4px;\n    // &:last-child {\n    //   border-bottom-left-radius: 4px;\n    //   border-bottom-right-radius: 4px;\n    // }\n    &.fetching {\n      opacity: .5;\n    }\n  }\n}\n\n.listLeft {\n  flex: auto;\n  min-width: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n}\n\n.text {\n  flex: auto;\n  margin-bottom: 2px;\n  .mixin-ellipsis-1();\n}\n.checkbox {\n  margin-top: 3px;\n  font-size: 14px;\n  opacity: .86;\n}\n\n.label {\n  flex: none;\n  font-size: 12px;\n  opacity: 0.5;\n  padding: 0 10px;\n  // display: flex;\n  // align-items: center;\n  // transform: rotate(45deg);\n  // background-color:\n}\n.btns {\n  flex: none;\n  font-size: 12px;\n  padding: 0 5px;\n  display: flex;\n  align-items: center;\n}\n.btn {\n  background-color: transparent;\n  border: none;\n  border-radius: @form-radius;\n  margin-right: 5px;\n  cursor: pointer;\n  padding: 4px 7px;\n  color: var(--color-button-font);\n  outline: none;\n  transition: background-color 0.2s ease;\n  line-height: 0;\n  &:last-child {\n    margin-right: 0;\n  }\n\n  svg {\n    height: 22px;\n    width: 22px;\n  }\n\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-font-active);\n  }\n}\n\n.footer {\n  width: @width;\n}\n.tips {\n  padding: 8px 15px;\n  font-size: 13px;\n  line-height: 1.25;\n  color: var(--color-font);\n}\n\n.noItem {\n  position: relative;\n  height: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n\n  p {\n    font-size: 16px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MyList/components/MusicSortModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" teleport=\"#view\" @close=\"handleClose\" @after-enter=\"$refs.input.focus()\">\n    <main :class=\"$style.main\">\n      <h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>\n      <base-input\n        ref=\"input\"\n        v-model=\"sortNum\"\n        :class=\"$style.input\"\n        type=\"number\"\n        :placeholder=\"$t('music_sort__input_tip')\"\n        @submit=\"handleSubmit\" @blur=\"verify\"\n      />\n      <div :class=\"$style.footer\">\n        <base-btn :class=\"$style.btn\" @click=\"handleSubmit\">{{ $t('btn_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n    musicInfo: {\n      type: Object,\n      default() {\n        return {}\n      },\n    },\n    selectedNum: {\n      type: Number,\n      default: 0,\n    },\n  },\n  emits: ['update:show', 'confirm'],\n  data() {\n    return {\n      sortNum: '',\n    }\n  },\n  watch: {\n    show(n) {\n      if (n) {\n        this.sortNum = ''\n      }\n    },\n  },\n  methods: {\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    verify() {\n      let num = /^[1-9]\\d*/.exec(this.sortNum)\n      num = num ? parseInt(num[0]) : ''\n      this.sortNum = num.toString()\n      return num\n    },\n    handleSubmit() {\n      let num = this.verify()\n      if (this.sortNum == '') return\n      this.handleClose()\n      this.$emit('confirm', num)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 0 15px;\n  max-width: 530px;\n  min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    word-break: break-all;\n    // text-align: center;\n    padding: 15px 0 8px;\n  }\n}\n\n.input {\n  // width: 100%;\n  // height: 26px;\n  padding: 8px 8px;\n}\n.footer {\n  margin: 20px 0 15px auto;\n}\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n\n  +.btn {\n    margin-left: 10px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MyList/index.vue",
    "content": "<template>\n  <div ref=\"dom_lists\" :class=\"$style.lists\">\n    <div :class=\"$style.listHeader\">\n      <h2 :class=\"$style.listsTitle\">{{ $t('my_list') }}</h2>\n      <div :class=\"$style.headerBtns\">\n        <button :class=\"$style.listsAdd\" :aria-label=\"$t('lists__new_list_btn')\" @click=\"isShowNewList = true\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"70%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-list-add\" />\n          </svg>\n        </button>\n        <button :class=\"$style.listsAdd\" :aria-label=\"$t('list_update_modal__title')\" @click=\"isShowListUpdateModal = true\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" style=\"transform: rotate(45deg);\" height=\"70%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-refresh\" />\n          </svg>\n        </button>\n      </div>\n    </div>\n    <ul ref=\"dom_lists_list\" class=\"scroll\" :class=\"[$style.listsContent, { [$style.sortable]: isModDown }]\">\n      <li\n        class=\"default-list\" :class=\"[$style.listsItem, {[$style.active]: defaultList.id == listId}, {[$style.clicked]: rightClickItemIndex == -2}, {[$style.fetching]: fetchingListStatus[defaultList.id]}]\"\n        :aria-label=\"$t(defaultList.name)\" :aria-selected=\"defaultList.id == listId\"\n        @contextmenu=\"handleListsItemRigthClick($event, -2)\" @click=\"handleListToggle(defaultList.id)\"\n      >\n        <!-- <div v-if=\"defaultList.id == listId\" :class=\"$style.activeIcon\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"40%\" viewBox=\"0 0 451.846 451.847\" space=\"preserve\">\n            <use xlink:href=\"#icon-right\" />\n          </svg>\n        </div> -->\n        <span :class=\"$style.listsLabel\">\n          <transition name=\"list-active\">\n            <svg-icon v-if=\"defaultList.id == listId\" name=\"angle-right-solid\" :class=\"$style.activeIcon\" />\n          </transition>\n          {{ $t(defaultList.name) }}\n        </span>\n      </li>\n      <li\n        class=\"default-list\" :class=\"[$style.listsItem, {[$style.active]: loveList.id == listId}, {[$style.clicked]: rightClickItemIndex == -1}, {[$style.fetching]: fetchingListStatus[loveList.id]}]\"\n        :aria-label=\"$t(loveList.name)\" :aria-selected=\"loveList.id == listId\"\n        @contextmenu=\"handleListsItemRigthClick($event, -1)\" @click=\"handleListToggle(loveList.id)\"\n      >\n        <span :class=\"$style.listsLabel\">\n          <transition name=\"list-active\">\n            <svg-icon v-if=\"loveList.id == listId\" name=\"angle-right-solid\" :class=\"$style.activeIcon\" />\n          </transition>\n          {{ $t(loveList.name) }}\n        </span>\n      </li>\n      <li\n        v-for=\"(item, index) in userLists\"\n        :key=\"item.id\" class=\"user-list\"\n        :class=\"[$style.listsItem, {[$style.active]: item.id == listId}, {[$style.clicked]: rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]\"\n        :data-index=\"index\" :aria-label=\"item.name\" :aria-selected=\"defaultList.id == listId\" @contextmenu=\"handleListsItemRigthClick($event, index)\"\n      >\n        <span :class=\"$style.listsLabel\" @click=\"handleListToggle(item.id, index + 2)\">\n          <transition name=\"list-active\">\n            <svg-icon v-if=\"item.id == listId\" name=\"angle-right-solid\" :class=\"$style.activeIcon\" />\n          </transition>\n          {{ item.name }}\n        </span>\n        <base-input\n          :class=\"$style.listsInput\" type=\"text\" :value=\"item.name\"\n          :placeholder=\"item.name\" @keyup.enter=\"handleSaveListName(index, $event)\" @blur=\"handleSaveListName(index, $event)\"\n        />\n      </li>\n      <transition enter-active-class=\"animated-fast slideInLeft\" leave-active-class=\"animated-fast fadeOut\" @after-leave=\"isNewListLeave = false\" @after-enter=\"$refs.dom_listsNewInput.focus()\">\n        <li v-if=\"isShowNewList\" :class=\"[$style.listsItem, $style.listsNew, {[$style.newLeave]: isNewListLeave}]\">\n          <base-input\n            ref=\"dom_listsNewInput\" :class=\"$style.listsInput\" type=\"text\" :placeholder=\"$t('lists__new_list_input')\"\n            @keyup.enter=\"handleCreateList\" @blur=\"handleCreateList\"\n          />\n        </li>\n      </transition>\n    </ul>\n    <base-menu v-model=\"isShowMenu\" :menus=\"menus\" :xy=\"menuLocation\" item-name=\"name\" @menu-click=\"handleMenuClick\" />\n    <DuplicateMusicModal v-model:visible=\"isShowDuplicateMusicModal\" :list-info=\"duplicateListInfo\" />\n    <ListSortModal v-model:visible=\"isShowListSortModal\" :list-info=\"sortListInfo\" />\n    <ListUpdateModal v-model:visible=\"isShowListUpdateModal\" />\n  </div>\n</template>\n\n<script>\nimport { openUrl } from '@common/utils/electron'\n\nimport musicSdk from '@renderer/utils/musicSdk'\nimport DuplicateMusicModal from './components/DuplicateMusicModal.vue'\nimport ListSortModal from './components/ListSortModal.vue'\nimport ListUpdateModal from './components/ListUpdateModal.vue'\n\nimport { defaultList, loveList, userLists, fetchingListStatus } from '@renderer/store/list/state'\nimport { removeUserList } from '@renderer/store/list/action'\n\nimport { ref, watch } from '@common/utils/vueTools'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { LIST_IDS } from '@common/constants'\n\nimport { dialog } from '@renderer/plugins/Dialog'\n\nimport { saveListPrevSelectId } from '@renderer/utils/data'\n\nimport { useI18n } from '@renderer/plugins/i18n'\n\n\nimport useShare from './useShare'\nimport useMenu from './useMenu'\nimport useListUpdate from './useListUpdate'\nimport useSort from './useSort'\nimport useDarg from './useDarg'\nimport useEditList from './useEditList'\nimport useListScroll from './useListScroll'\nimport useDuplicate from './useDuplicate'\n\nexport default {\n  name: 'MyLists',\n  components: {\n    DuplicateMusicModal,\n    ListSortModal,\n    ListUpdateModal,\n  },\n  props: {\n    listId: {\n      type: String,\n      required: true,\n    },\n  },\n  emits: ['show-menu'],\n  setup(props, { emit }) {\n    const router = useRouter()\n    const t = useI18n()\n\n    const dom_lists_list = ref(null)\n    const rightClickItemIndex = ref(-10)\n\n    const { handleImportList, handleExportList } = useShare()\n    const { isShowListUpdateModal, handleUpdateSourceList } = useListUpdate()\n    const { isShowListSortModal, sortListInfo, handleSortList } = useSort()\n    const { isShowDuplicateMusicModal, duplicateListInfo, handleDuplicateList } = useDuplicate()\n    const { handleRename, handleSaveListName, isShowNewList, isNewListLeave, handleCreateList } = useEditList({ dom_lists_list })\n    useListScroll({ dom_lists_list })\n\n    const handleOpenSourceDetailPage = async(listInfo) => {\n      const { source, sourceListId } = listInfo\n      if (!sourceListId) return\n      let url\n      if (/board__/.test(sourceListId)) {\n        const id = sourceListId.replace(/board__/, '')\n        url = musicSdk[source].leaderboard.getDetailPageUrl(id)\n      } else if (musicSdk[source]?.songList?.getDetailPageUrl) {\n        url = await musicSdk[source].songList.getDetailPageUrl(sourceListId)\n      }\n      if (!url) return\n      void openUrl(url)\n    }\n\n    const handleRemove = (listInfo) => {\n      void dialog.confirm({\n        message: t('lists__remove_tip', { name: listInfo.name }),\n        confirmButtonText: t('lists__remove_tip_button'),\n      }).then(isRemove => {\n        if (!isRemove) return\n        void removeUserList([listInfo.id])\n        if (props.listId == listInfo.id) {\n          handleListToggle(LIST_IDS.DEFAULT)\n        }\n      })\n    }\n\n    const {\n      menus,\n      menuLocation,\n      isShowMenu,\n      showMenu,\n      menuClick,\n    } = useMenu({\n      emit,\n\n      handleImportList,\n      handleExportList,\n      handleUpdateSourceList,\n      handleOpenSourceDetailPage,\n      handleSortList,\n      handleDuplicateList,\n      handleRename,\n      handleRemove,\n    })\n\n    const handleListsItemRigthClick = (event, index) => {\n      rightClickItemIndex.value = index\n      showMenu(event, index)\n    }\n\n    const handleListToggle = (id) => {\n      if (id == props.listId) return\n      router.replace({\n        path: '/list',\n        query: { id },\n      }).catch(_ => _)\n    }\n\n    const handleMenuClick = (action) => {\n      if (rightClickItemIndex.value < -2) return\n      let index = rightClickItemIndex.value\n      rightClickItemIndex.value = -10\n      menuClick(action, index)\n    }\n\n    const { isModDown } = useDarg({ dom_lists_list, handleMenuClick, handleSaveListName })\n\n\n    watch(() => props.listId, (listId) => {\n      saveListPrevSelectId(listId)\n    })\n\n    watch(() => userLists, (lists) => {\n      if (lists.some(l => l.id == props.listId)) return\n      void router.replace({\n        path: '/list',\n        query: {\n          id: defaultList.id,\n        },\n      })\n    })\n\n    return {\n      rightClickItemIndex,\n      defaultList,\n      loveList,\n      userLists,\n      fetchingListStatus,\n      dom_lists_list,\n      isShowListUpdateModal,\n      isShowListSortModal,\n      sortListInfo,\n      isShowDuplicateMusicModal,\n      duplicateListInfo,\n      handleSaveListName,\n      isShowNewList,\n      isNewListLeave,\n      handleCreateList,\n      handleListsItemRigthClick,\n      isShowMenu,\n      handleMenuClick,\n      menus,\n      menuLocation,\n      handleListToggle,\n      isModDown,\n      hideMenu: handleMenuClick,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n@lists-item-height: 36px;\n.lists {\n  flex: none;\n  width: 16%;\n  display: flex;\n  flex-flow: column nowrap;\n}\n.listHeader {\n  position: relative;\n  display: flex;\n  flex-flow: row nowrap;\n  border-bottom: var(--color-list-header-border-bottom);\n  &:hover {\n    .listsAdd {\n      opacity: 1;\n    }\n  }\n}\n.listsTitle {\n  flex: auto;\n  font-size: 12px;\n  line-height: 38px;\n  padding: 0 10px;\n  .mixin-ellipsis-1();\n}\n.headerBtns {\n  flex: none;\n  display: flex;\n}\n.listsAdd {\n  // position: absolute;\n  // right: 0;\n  margin-top: 6px;\n  background: none;\n  height: 30px;\n  border: none;\n  outline: none;\n  border-radius: @radius-border;\n  cursor: pointer;\n  opacity: .1;\n  transition: opacity @transition-normal;\n  color: var(--color-button-font);\n  svg {\n    vertical-align: bottom;\n  }\n  &:active {\n    opacity: .7 !important;\n  }\n  &:hover {\n    opacity: .6 !important;\n  }\n}\n.listsContent {\n  flex: auto;\n  min-width: 0;\n  overflow-y: scroll !important;\n  // border-right: 1px solid rgba(0, 0, 0, 0.12);\n\n  &.sortable {\n    * {\n      -webkit-user-drag: element;\n    }\n\n    .listsItem {\n      &:hover, &.active, &.selected, &.clicked {\n        background-color: transparent !important;\n      }\n\n      &.dragingItem {\n        background-color: var(--color-primary-background-hover) !important;\n      }\n    }\n  }\n}\n.listsItem {\n  position: relative;\n  transition: .3s ease;\n  transition-property: color, background-color, opacity;\n  background-color: transparent;\n  &:not(.active) {\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n      cursor: pointer;\n    }\n  }\n  &.active {\n    // background-color:\n    color: var(--color-primary);\n  }\n  &.selected {\n    background-color: var(--color-primary-font-active);\n  }\n  &.clicked {\n    background-color: var(--color-primary-background-hover);\n  }\n  &.fetching {\n    opacity: .5;\n  }\n  &.editing {\n    padding: 0 10px;\n    background-color: var(--color-primary-background-hover);\n    .listsLabel {\n      display: none;\n    }\n    .listsInput {\n      display: block;\n    }\n  }\n}\n.activeIcon {\n  height: .9em;\n  width: .9em;\n  margin-left: -0.45em;\n  vertical-align: -0.05em;\n}\n.listsLabel {\n  display: block;\n  height: @lists-item-height;\n  padding: 0 10px;\n  font-size: 13px;\n  line-height: @lists-item-height;\n  .mixin-ellipsis-1();\n}\n.listsInput {\n  width: 100%;\n  height: @lists-item-height;\n  // border: none;\n  padding: 0;\n  // padding-bottom: 1px;\n  line-height: @lists-item-height;\n  background: none !important;\n  border-radius: 0;\n  // outline: none;\n  font-size: 13px;\n  display: none;\n  // font-family: inherit;\n}\n\n.listsNew {\n  padding: 0 10px;\n  background-color: var(--color-primary-background-hover) !important;\n  .listsInput {\n    display: block;\n  }\n}\n.newLeave {\n  margin-top: -@lists-item-height;\n  z-index: -1;\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useDarg.ts",
    "content": "import { onBeforeUnmount, ref, type Ref, useCssModule } from '@common/utils/vueTools'\nimport { updateUserListPosition } from '@renderer/store/list/action'\nimport { userLists } from '@renderer/store/list/state'\nimport useDarg from '@renderer/utils/compositions/useDrag'\n\n\nexport default ({ dom_lists_list, handleSaveListName, handleMenuClick }: {\n  dom_lists_list: Ref<HTMLElement | null>\n  handleSaveListName: () => Promise<void> | void\n  handleMenuClick: () => void\n}) => {\n  const isModDown = ref(false)\n  const styles = useCssModule()\n\n  const { setDisabled } = useDarg({\n    dom_list: dom_lists_list,\n    dragingItemClassName: styles.dragingItem,\n    filter: 'default-list',\n    onUpdate(newIndex: number, oldIndex: number) {\n      void updateUserListPosition({ ids: [userLists[oldIndex - 2].id], position: newIndex - 2 })\n    },\n  })\n\n  const handle_key_mod_down = ({ event }: LX.KeyDownEevent) => {\n    if (!isModDown.value) {\n      // console.log(event)\n      switch ((event!.target as HTMLElement).tagName) {\n        case 'INPUT':\n        case 'SELECT':\n        case 'TEXTAREA':\n          return\n        default: if ((event!.target as HTMLElement).isContentEditable) return\n      }\n\n      isModDown.value = true\n      setDisabled(false)\n      void handleSaveListName()\n    }\n    handleMenuClick()\n  }\n  const handle_key_mod_up = () => {\n    if (isModDown.value) {\n      isModDown.value = false\n      setDisabled(true)\n    }\n  }\n\n  window.key_event.on('key_mod_down', handle_key_mod_down)\n  window.key_event.on('key_mod_up', handle_key_mod_up)\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_mod_down', handle_key_mod_down)\n    window.key_event.off('key_mod_up', handle_key_mod_up)\n  })\n\n  return {\n    isModDown,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useDuplicate.ts",
    "content": "import { ref, reactive } from '@common/utils/vueTools'\n\n\nexport default () => {\n  const isShowDuplicateMusicModal = ref(false)\n  const duplicateListInfo = reactive({ id: '', name: '' })\n\n  const handleDuplicateList = (listInfo: LX.List.MyListInfo) => {\n    duplicateListInfo.id = listInfo.id\n    duplicateListInfo.name = listInfo.name\n    isShowDuplicateMusicModal.value = true\n  }\n\n  return {\n    isShowDuplicateMusicModal,\n    duplicateListInfo,\n    handleDuplicateList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useEditList.ts",
    "content": "import { ref, nextTick, useCssModule, type Ref } from '@common/utils/vueTools'\nimport { userLists } from '@renderer/store/list/state'\nimport { updateUserList, createUserList } from '@renderer/store/list/action'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport default ({ dom_lists_list }: {\n  dom_lists_list: Ref<HTMLElement | null>\n}) => {\n  const isShowNewList = ref(false)\n  const isNewListLeave = ref(false)\n  const editIndex = ref(-1)\n  const styles = useCssModule()\n\n  const handleRename = (index: number) => {\n    // console.log(index)\n    const dom = dom_lists_list.value?.querySelectorAll('.user-list')[index]\n    if (!dom) return\n    void nextTick(() => {\n      dom.classList.add(styles.editing)\n      dom.querySelector('input')?.focus()\n    })\n  }\n\n  const handleSaveListName = async() => {\n    let dom_target = dom_lists_list.value?.querySelector('.' + styles.editing) as HTMLElement\n    if (!dom_target) return\n    const dom_input: HTMLInputElement = dom_target.querySelector('.' + styles.listsInput)!\n    if (!dom_input) return\n    let name = dom_input.value.trim()\n    if (dom_target.dataset.index == null) return\n    const targetList = userLists[parseInt(dom_target.dataset.index)]\n    if (name.length) await updateUserList([{ ...targetList, name }])\n    dom_input.value = targetList.name\n    dom_target.classList.remove(styles.editing)\n  }\n\n  const handleCreateList = async(event: Event) => {\n    const target = event.target as HTMLInputElement\n    if (target.readOnly) return\n    let name = target.value.trim()\n    target.readOnly = true\n\n    if (name == '' || (\n      userLists.some(l => l.name == name) && !(await dialog.confirm(window.i18n.t('list_duplicate_tip'))))\n    ) {\n      isShowNewList.value = false\n      return\n    }\n\n    await createUserList({ name })\n    isNewListLeave.value = true\n    void nextTick(() => {\n      isShowNewList.value = false\n    })\n  }\n\n\n  return {\n    isShowNewList,\n    isNewListLeave,\n    editIndex,\n    handleRename,\n    handleSaveListName,\n    handleCreateList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useListScroll.ts",
    "content": "import { onMounted, useCssModule, type Ref } from '@common/utils/vueTools'\n\n\nexport default ({ dom_lists_list }: {\n  dom_lists_list: Ref<HTMLElement | null>\n}) => {\n  const styles = useCssModule()\n\n  const setListsScroll = () => {\n    if (!dom_lists_list.value) return\n    let target = dom_lists_list.value.querySelector('.' + styles.active) as HTMLElement\n    if (!target) return\n    let offsetTop = target.offsetTop\n    let location = offsetTop - 150\n    if (location > 0) dom_lists_list.value.scrollTop = location\n  }\n\n  onMounted(() => {\n    setListsScroll()\n  })\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useListUpdate.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport syncSourceList from '@renderer/store/list/syncSourceList'\nimport { useI18n } from '@renderer/plugins/i18n'\n\n\nexport default () => {\n  const isShowListUpdateModal = ref(false)\n\n  const t = useI18n()\n\n  const handleUpdateSourceList = (listInfo: LX.List.UserListInfo) => {\n    void dialog.confirm({\n      message: t('lists__sync_confirm_tip', { name: listInfo.name }),\n      confirmButtonText: t('lists__remove_tip_button'),\n    }).then(isSync => {\n      if (!isSync) return\n      void syncSourceList(listInfo)\n    })\n  }\n\n  return {\n    isShowListUpdateModal,\n    handleUpdateSourceList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useMenu.js",
    "content": "import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { userLists, defaultList, loveList } from '@renderer/store/list/state'\nimport musicSdk from '@renderer/utils/musicSdk'\nimport { addLocalFile } from './actions'\n\nexport default ({\n  emit,\n\n  handleRename,\n  handleDuplicateList,\n  handleSortList,\n  handleOpenSourceDetailPage,\n  handleImportList,\n  handleExportList,\n  handleUpdateSourceList,\n  handleRemove,\n}) => {\n  const menuControl = reactive({\n    rename: true,\n    duplicate: true,\n    sort: true,\n    local_file: true,\n    sourceDetail: true,\n    import: true,\n    export: true,\n    sync: false,\n    remove: true,\n  })\n  const t = useI18n()\n  const menuLocation = reactive({ x: 0, y: 0 })\n  const isShowMenu = ref(false)\n\n  const menus = computed(() => {\n    return [\n      {\n        name: t('lists__rename'),\n        action: 'rename',\n        disabled: !menuControl.rename,\n      },\n      {\n        name: t('lists__sort_list'),\n        action: 'sort',\n        disabled: !menuControl.sort,\n      },\n      {\n        name: t('lists__duplicate'),\n        action: 'duplicate',\n        disabled: !menuControl.duplicate,\n      },\n      {\n        name: t('lists__select_local_file'),\n        action: 'local_file',\n        disabled: !menuControl.local_file,\n      },\n      {\n        name: t('lists__sync'),\n        action: 'sync',\n        disabled: !menuControl.sync,\n      },\n      {\n        name: t('lists__source_detail'),\n        action: 'sourceDetail',\n        disabled: !menuControl.sourceDetail,\n      },\n      {\n        name: t('lists__import'),\n        action: 'import',\n        disabled: !menuControl.export,\n      },\n      {\n        name: t('lists__export'),\n        action: 'export',\n        disabled: !menuControl.export,\n      },\n      {\n        name: t('lists__remove'),\n        action: 'remove',\n        disabled: !menuControl.remove,\n      },\n    ]\n  })\n\n  const assertSupportDetail = (source, index) => {\n    if (source) {\n      const { sourceListId } = userLists[index]\n      if (sourceListId) {\n        if (/board__/.test(sourceListId)) {\n          // const id = sourceListId.replace(/board__/, '')\n          return !!musicSdk[source]?.leaderboard?.getDetailPageUrl\n        } else {\n          return !!musicSdk[source]?.songList?.getDetailPageUrl\n        }\n      }\n    }\n    return false\n  }\n\n  const showMenu = (event, index) => {\n    let source\n    switch (index) {\n      case -1:\n      case -2:\n        menuControl.rename = false\n        menuControl.remove = false\n        menuControl.sync = false\n        break\n      default:\n        menuControl.rename = true\n        menuControl.remove = true\n        source = userLists[index].source\n        menuControl.sync = !!source && !!musicSdk[source]?.songList\n        break\n    }\n    // menuControl.sort = !!getList(this.getTargetListInfo(index)?.id).length\n    menuControl.sourceDetail = assertSupportDetail(source, index)\n\n    menuLocation.x = event.pageX\n    menuLocation.y = event.pageY\n\n    if (isShowMenu.value) return\n    emit('show-menu')\n    nextTick(() => {\n      isShowMenu.value = true\n    })\n  }\n\n  const hideMenu = () => {\n    isShowMenu.value = false\n  }\n\n  const getListInfo = (index) => {\n    let list\n    switch (index) {\n      case -2:\n        list = defaultList\n        break\n      case -1:\n        list = loveList\n        break\n      default:\n        list = userLists[index]\n        if (!list) return null\n        break\n    }\n    return list\n  }\n\n  const menuClick = (action, index) => {\n    // console.log(action)\n    hideMenu()\n    if (!action) return\n    const listInfo = getListInfo(index)\n    switch (action.action) {\n      case 'rename':\n        handleRename(index)\n        break\n      case 'duplicate':\n        handleDuplicateList(listInfo)\n        break\n      case 'sort':\n        handleSortList(listInfo)\n        break\n      case 'local_file':\n        addLocalFile(listInfo)\n        break\n      case 'sourceDetail':\n        handleOpenSourceDetailPage(listInfo)\n        break\n      case 'import':\n        handleImportList(listInfo, index)\n        break\n      case 'export':\n        handleExportList(listInfo)\n        break\n      case 'sync':\n        handleUpdateSourceList(listInfo)\n        break\n      case 'remove':\n        handleRemove(listInfo)\n        break\n    }\n  }\n\n  return {\n    menus,\n    menuLocation,\n    isShowMenu,\n    showMenu,\n    menuClick,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useShare.ts",
    "content": "import { toRaw } from '@common/utils/vueTools'\nimport { openSaveDir, showSelectDialog } from '@renderer/utils/ipc'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { filterFileName, toNewMusicInfo, fixNewMusicInfoQuality, filterMusicList } from '@renderer/utils'\nimport { getListMusics, updateUserList, addListMusics, overwriteListMusics, createUserList } from '@renderer/store/list/action'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/state'\nimport useImportTip from '@renderer/utils/compositions/useImportTip'\nimport { dialog } from '@renderer/plugins/Dialog'\n\n\nexport default () => {\n  const t = useI18n()\n  const showImportTip = useImportTip()\n\n  const handleExportList = (listInfo: LX.List.MyListInfo) => {\n    if (!listInfo) return\n    void openSaveDir({\n      title: t('lists__export_part_desc'),\n      defaultPath: `lx_list_part_${filterFileName(listInfo.name)}.lxmc`,\n    }).then(async result => {\n      if (result.canceled || !result.filePath) return\n      void window.lx.worker.main.saveLxConfigFile(result.filePath, {\n        type: 'playListPart_v2',\n        data: { ...toRaw(listInfo), list: toRaw(await getListMusics(listInfo.id)) },\n      })\n    })\n  }\n  const handleImportList = (listInfo: LX.List.MyListInfo, index: number) => {\n    void showSelectDialog({\n      title: t('lists__import_part_desc'),\n      properties: ['openFile'],\n      filters: [\n        { name: 'Play List Part', extensions: ['json', 'lxmc'] },\n        { name: 'All Files', extensions: ['*'] },\n      ],\n    }).then(async result => {\n      if (result.canceled) return\n      const filePath = result.filePaths[0]\n      if (!filePath) return\n      let configData: any\n      try {\n        configData = await window.lx.worker.main.readLxConfigFile(filePath)\n      } catch (error) {\n        return\n      }\n      let listData: LX.ConfigFile.MyListInfoPart['data']\n      switch (configData.type) {\n        case 'playListPart':\n          listData = configData.data\n          listData.list = filterMusicList(listData.list.map(m => toNewMusicInfo(m)))\n          break\n        case 'playListPart_v2':\n          listData = configData.data\n          listData.list = filterMusicList(listData.list).map(m => fixNewMusicInfoQuality(m))\n          break\n        default:\n          showImportTip(configData.type)\n          return\n      }\n\n      const targetList = [defaultList, loveList, ...userLists].find(l => l.id == listData.id)\n      if (targetList) {\n        const confirm = await dialog.confirm({\n          message: t('lists__import_part_confirm', { importName: listData.name, localName: targetList.name }),\n          cancelButtonText: t('lists__import_part_button_cancel'),\n          confirmButtonText: t('lists__import_part_button_confirm'),\n        })\n        if (confirm) {\n          listData.name = targetList.name\n          switch (listData.id) {\n            case defaultList.id:\n            case loveList.id:\n              break\n            default:\n              void updateUserList([\n                {\n                  name: listData.name,\n                  id: listData.id,\n                  source: (listData as LX.List.UserListInfo).source,\n                  sourceListId: (listData as LX.List.UserListInfo).sourceListId,\n                  locationUpdateTime: (targetList as LX.List.UserListInfo).locationUpdateTime,\n                },\n              ])\n              break\n          }\n          void overwriteListMusics({\n            listId: listData.id,\n            musicInfos: listData.list.map(m => fixNewMusicInfoQuality(m)),\n          })\n          return\n        }\n        listData.id += `__${Date.now()}`\n      }\n      void createUserList({\n        position: index,\n        name: listData.name,\n        id: listData.id,\n        source: (listData as LX.List.UserListInfo).source,\n        sourceListId: (listData as LX.List.UserListInfo).sourceListId,\n      })\n      void addListMusics(listData.id, listData.list.map(m => fixNewMusicInfoQuality(m)))\n    })\n  }\n\n  return {\n    handleExportList,\n    handleImportList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/MyList/useSort.js",
    "content": "import { ref, reactive } from '@common/utils/vueTools'\n\n\nexport default () => {\n  const isShowListSortModal = ref(false)\n  const sortListInfo = reactive({ id: '', name: '' })\n\n  const handleSortList = (listInfo) => {\n    sortListInfo.id = listInfo.id\n    sortListInfo.name = listInfo.name\n    isShowListSortModal.value = true\n  }\n\n  return {\n    isShowListSortModal,\n    sortListInfo,\n    handleSortList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/List/index.vue",
    "content": "<template>\n  <div id=\"my-list\" :class=\"$style.container\" @click=\"handleContainerClick\">\n    <MyList ref=\"myList\" :list-id=\"listId\" @show-menu=\"$refs.musicList.handleMenuClick()\" />\n    <MusicList ref=\"musicList\" :list-id=\"listId\" @show-menu=\"$refs.myList.handleMenuClick()\" />\n  </div>\n</template>\n\n<script>\nimport { getListPrevSelectId } from '@renderer/utils/data'\n\nimport MyList from './MyList/index.vue'\nimport MusicList from './MusicList/index.vue'\n\nexport default {\n  name: 'List',\n  components: {\n    MyList,\n    MusicList,\n  },\n  async beforeRouteEnter(to, from, next) {\n    let id = to.query.id\n    if (!id) {\n      id = await getListPrevSelectId()\n      next({\n        path: to.path,\n        query: { id },\n      })\n    } else next()\n  },\n  beforeRouteUpdate(to, from) {\n    // console.log(to, from)\n    if (to.query.updated) return\n    let id = to.query.id\n    if (id == null) return\n    // if (!getList(id)) {\n    //   id = defaultList.id\n    // }\n    this.listId = id\n    const scrollIndex = to.query.scrollIndex\n    const isAnimation = from.query.id == to.query.id\n    this.$refs.musicList?.handleRestoreScroll(scrollIndex, isAnimation)\n\n    return {\n      path: '/list',\n      query: { id, updated: true },\n    }\n  },\n  beforeRouteLeave(to, from) {\n    this.$refs.musicList?.saveListPosition()\n  },\n  data() {\n    return {\n      listId: null,\n    }\n  },\n  created() {\n    this.listId = this.$route.query.id\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  overflow: hidden;\n  height: 100%;\n  display: flex;\n  position: relative;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Search/MusicList/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <material-online-list\n      ref=\"listRef\"\n      :page=\"listInfo.page\"\n      :limit=\"listInfo.limit\"\n      :total=\"listInfo.total\"\n      :list=\"listInfo.list\"\n      :no-item=\"listInfo.noItemLabel\"\n      :source-tag=\"sourceId == 'all'\"\n      check-api-source\n      @toggle-page=\"handleTogglePage\"\n      @play-list=\"handlePlayList\"\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { watch } from '@common/utils/vueTools'\nimport { searchText } from '@renderer/store/search/state'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\nimport useList, { type SearchSource } from './useList'\n\ninterface Props {\n  sourceId: SearchSource\n  page: number\n}\n\nconst props = defineProps<Props>()\nconst router = useRouter()\nconst route = useRoute()\n\nconst {\n  listRef,\n  listInfo,\n  search,\n  handlePlayList,\n} = useList()\n\nwatch(() => [props.sourceId, props.page], ([sourceId, page]) => {\n  setTimeout(() => {\n    search(searchText.value, sourceId as SearchSource, page as number || 1)\n  })\n})\nwatch(searchText, (searchText) => {\n  setTimeout(() => {\n    search(searchText, props.sourceId, props.page)\n  })\n}, {\n  immediate: true,\n})\n\nconst handleTogglePage = (page: number) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      ...route.query,\n      page,\n    },\n  })\n}\n\n\n</script>\n\n\n<style lang=\"less\" module>\n.container {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n}\n\n.list {\n  overflow: hidden;\n  height: 100%;\n  flex: auto;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Search/MusicList/useList.ts",
    "content": "import { LIST_IDS } from '@common/constants'\nimport { ref } from '@common/utils/vueTools'\nimport { playList } from '@renderer/core/player/action'\nimport { getListMusics, addListMusics } from '@renderer/store/list/action'\nimport { addHistoryWord } from '@renderer/store/search/action'\n// import { useI18n } from '@renderer/plugins/i18n'\n// import { } from '@renderer/store/search/state'\nimport { search as searchMusic, listInfos, type ListInfo } from '@renderer/store/search/music'\nimport { assertApiSupport } from '@renderer/store/utils'\n\nexport type SearchSource = LX.OnlineSource | 'all'\n\nexport default () => {\n  const listRef = ref<any>(null)\n\n  const listInfo = ref<ListInfo>({\n    page: 1,\n    maxPage: 0,\n    limit: 30,\n    total: 0,\n    list: [],\n    key: null,\n    noItemLabel: '',\n  })\n\n  const search = (text: string, source: SearchSource, page: number) => {\n    listInfo.value = listInfos[source] as ListInfo\n    if (text.length) void addHistoryWord(text)\n    void searchMusic(text, page, source).then((list: LX.Music.MusicInfo[]) => {\n      if (list.length) {\n        setTimeout(() => {\n          if (listRef.value) listRef.value.scrollToTop()\n        })\n      }\n    })\n  }\n\n  const handlePlayList = async(index: number) => {\n    let targetSong = listInfo.value.list[index]\n\n    if (!assertApiSupport(targetSong.source)) return\n\n    const defaultListMusics = await getListMusics(LIST_IDS.DEFAULT)\n\n    await addListMusics(LIST_IDS.DEFAULT, [targetSong])\n\n    let targetIndex = defaultListMusics.findIndex(s => s.id === targetSong.id)\n    if (targetIndex > -1) playList(LIST_IDS.DEFAULT, targetIndex)\n  }\n\n  return {\n    listRef,\n    listInfo,\n    search,\n    handlePlayList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Search/SongListList/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <SongList ref=\"listRef\" :list-info=\"listInfo\" :visible-source=\"sourceId == 'all'\" @toggle-page=\"togglePage\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { watch } from '@common/utils/vueTools'\nimport { searchText } from '@renderer/store/search/state'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\nimport useList, { type SearchSource } from './useList'\nimport SongList from '@renderer/views/songList/List/components/SongList.vue'\n\ninterface Props {\n  sourceId: SearchSource\n  page: number\n}\n\nconst props = defineProps<Props>()\nconst router = useRouter()\nconst route = useRoute()\n\nconst {\n  listRef,\n  listInfo,\n  search,\n} = useList()\n\nwatch(() => [props.sourceId, props.page], ([sourceId, page]) => {\n  setTimeout(() => {\n    search(searchText.value, sourceId as SearchSource, page as number || 1)\n  })\n})\nwatch(searchText, (searchText) => {\n  setTimeout(() => {\n    search(searchText, props.sourceId, props.page)\n  })\n}, {\n  immediate: true,\n})\n\nconst togglePage = (page: number) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      ...route.query,\n      page,\n    },\n  })\n  // search(searchText.value, props.sourceId, page)\n}\n\n\n</script>\n\n\n<style lang=\"less\" module>\n.container {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  padding-top: 5px;\n}\n\n// .list {\n//   overflow: hidden;\n//   height: 100%;\n//   flex: auto;\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Search/SongListList/useList.ts",
    "content": "import { onBeforeRouteLeave } from '@common/utils/vueRouter'\nimport { ref, nextTick } from '@common/utils/vueTools'\nimport { addHistoryWord } from '@renderer/store/search/action'\n// import { useI18n } from '@renderer/plugins/i18n'\n// import { } from '@renderer/store/search/state'\nimport type { SearchListInfo, ListInfoItem } from '@renderer/store/search/songlist'\nimport { search as searchSongList, listInfos } from '@renderer/store/search/songlist'\n\nexport type SearchSource = LX.OnlineSource | 'all'\n\nexport default () => {\n  const listRef = ref<any>(null)\n\n  const listInfo = ref<SearchListInfo>({\n    page: 1,\n    limit: 30,\n    total: 0,\n    list: [],\n    key: null,\n    noItemLabel: '',\n    tagId: '',\n    sortId: '',\n  })\n\n  const search = (text: string, source: SearchSource, page: number) => {\n    // console.log(text, source, page)\n    listInfo.value = listInfos[source] as SearchListInfo\n    if (text.length) void addHistoryWord(text)\n    void searchSongList(text, page, source).then((list: ListInfoItem[]) => {\n      // console.log(list)\n      if (listInfo.value.key == window.lx.songListInfo.searchKey && window.lx.songListInfo.searchPosition) {\n        void nextTick(() => {\n          listRef.value?.scrollTo(window.lx.songListInfo.searchPosition)\n        })\n      } else if (list.length && listRef.value) {\n        window.lx.songListInfo.searchKey = null\n        void nextTick(() => {\n          listRef.value.scrollTo(0)\n        })\n      }\n    })\n  }\n\n  onBeforeRouteLeave(() => {\n    window.lx.songListInfo.searchKey = listInfo.value.key\n    if (listRef.value) window.lx.songListInfo.searchPosition = listRef.value.getScrollTop()\n  })\n\n\n  return {\n    listRef,\n    listInfo,\n    search,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Search/components/BlankView.vue",
    "content": "<template>\n  <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated-fast fadeOut\">\n    <div v-show=\"props.visible\" :class=\"$style.noitem\">\n      <div v-if=\"appSetting['search.isShowHotSearch'] || (appSetting['search.isShowHistorySearch'] && historyList.length)\" class=\"scroll\" :class=\"$style.noitemListContainer\">\n        <dl v-if=\"appSetting['search.isShowHotSearch']\" :class=\"[$style.noitemList, $style.noitemHotSearchList]\">\n          <dt :class=\"$style.noitemListTitle\">{{ $t('search__hot_search') }}</dt>\n          <dd v-for=\"(item, index) in hotSearchList\" :key=\"index\" :class=\"$style.noitemListItem\" @click=\"handleSearch(item)\">{{ item }}</dd>\n        </dl>\n        <dl v-if=\"appSetting['search.isShowHistorySearch'] && historyList.length\" :class=\"$style.noitemList\">\n          <dt :class=\"$style.noitemListTitle\">\n            <span>{{ $t('history_search') }}</span><span :class=\"$style.historyClearBtn\" :aria-label=\"$t('history_clear')\" @click=\"clearHistoryList\">\n              <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 512 512\" space=\"preserve\">\n                <use xlink:href=\"#icon-eraser\" />\n              </svg></span>\n          </dt>\n          <dd v-for=\"(item, index) in historyList\" :key=\"index + item\" :class=\"$style.noitemListItem\" :aria-label=\"$t('history_remove')\" @contextmenu=\"removeHistoryWord(index)\" @click=\"handleSearch(item)\">{{ item }}</dd>\n        </dl>\n      </div>\n      <div v-else :class=\"$style.noitem_label\">\n        <p>{{ $t('search__welcome') }}</p>\n      </div>\n    </div>\n  </transition>\n</template>\n\n<script setup>\nimport { watch, shallowRef } from '@common/utils/vueTools'\nimport { historyList } from '@renderer/store/search/state'\nimport { getHistoryList, removeHistoryWord, clearHistoryList } from '@renderer/store/search/action'\nimport { getList } from '@renderer/store/hotSearch'\nimport { appSetting } from '@renderer/store/setting'\nimport { useRouter } from '@common/utils/vueRouter'\n\nconst props = defineProps({\n  visible: Boolean,\n  source: {\n    type: String,\n    required: true,\n  },\n})\n\nconst hotSearchList = shallowRef([])\n\nif (appSetting['search.isShowHotSearch']) {\n  watch(() => props.visible, (visible) => {\n    if (!visible) return\n    void getList(props.source).then(list => {\n      hotSearchList.value = list\n    })\n  }, {\n    immediate: true,\n  })\n\n  watch(() => props.source, (source) => {\n    if (!props.visible) return\n    void getList(source).then(list => {\n      if (source != props.source) return\n      hotSearchList.value = list\n    })\n  })\n}\n\nif (appSetting['search.isShowHistorySearch']) {\n  void getHistoryList()\n}\n\nconst router = useRouter()\nconst handleSearch = (text) => {\n  void router.replace({\n    path: '/search',\n    query: {\n      text,\n    },\n  })\n}\n\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.noitem {\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  overflow: hidden;\n  display: flex;\n  flex-flow: column nowrap;\n  // justify-content: center;\n}\n.noitemListContainer {\n  padding: 3% 15px 15px;\n  // margin-top: -20px;\n  min-height: 250px;\n  max-height: 94.7%;\n}\n.noitemList {\n  +.noitemList {\n    margin-top: 15px;\n  }\n}\n.noitemHotSearchList {\n  min-height: 106px;\n}\n.noitemListTitle {\n  color: var(--color-font);\n  padding: 5px 5px 8px;\n  font-size: 14px;\n}\n.noitemListItem {\n  display: inline-block;\n  margin: 3px 5px;\n  background-color: var(--color-button-background);\n  padding: 7px 10px;\n  border-radius: @radius-progress-border;\n  transition: background-color @transition-normal;\n  cursor: pointer;\n  color: var(--color-button-font);\n  .mixin-ellipsis-1();\n  max-width: 150px;\n  font-size: 13px;\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n.historyClearBtn {\n  padding: 0 5px;\n  margin-left: 5px;\n  color: var(--color-font-label);\n  cursor: pointer;\n  transition: @transition-normal;\n  transition-property: color, opacity;\n  opacity: .3;\n  &:hover {\n    color: var(--color-primary-font-hover);\n    opacity: .8;\n  }\n  &:active {\n    color: var(--color-primary-font-active);\n    opacity: 1;\n  }\n  svg {\n    vertical-align: middle;\n    width: 15px;\n  }\n}\n\n.noitem_label {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n    text-align: center;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Search/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <div :class=\"$style.header\">\n      <base-tab v-model=\"source\" :list=\"sources\" @change=\"handleSourceChange\" />\n      <base-tab v-model=\"searchType\" :list=\"searchTypes\" @change=\"handleTypeChange\" />\n    </div>\n    <div :class=\"$style.main\">\n      <song-list-list v-if=\"searchType == 'songlist'\" v-show=\"searchText\" :page=\"page\" :source-id=\"source\" />\n      <music-list v-else v-show=\"searchText\" :page=\"page\" :source-id=\"source\" />\n      <blank-view :visible=\"!searchText\" :source=\"source\" />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\nimport { searchText } from '@renderer/store/search/state'\nimport { getSearchSetting, setSearchSetting } from '@renderer/utils/data'\nimport { sources as _sources } from '@renderer/store/search/music'\n\nimport MusicList from './MusicList/index.vue'\nimport SongListList from './SongListList/index.vue'\nimport BlankView from './components/BlankView.vue'\nimport { computed, ref } from '@common/utils/vueTools'\nimport { sourceNames } from '@renderer/store'\n\nconst source = ref('kw')\nconst searchType = ref(null)\nconst page = ref(1)\n\nconst verifyQueryParams = async(to, from, next) => {\n  let _source = to.query.source\n  let _type = to.query.type\n  let _page = to.query.page\n\n  if (_source == null || _type == null) {\n    const setting = await getSearchSetting()\n    _source ??= setting.source\n    _type ??= setting.type\n\n    next({\n      path: to.path,\n      query: { ...to.query, source: _source, type: _type, page: _page },\n    })\n    return\n  }\n  source.value = _source\n  searchType.value = _type\n\n  if (_page) page.value = parseInt(_page)\n\n  if (to.query.text != null) {\n    searchText.value = to.query.text\n    if (!_page) page.value = 1\n  }\n  next()\n  void setSearchSetting({ source: _source, type: _type })\n}\n\nexport default {\n  components: {\n    MusicList,\n    SongListList,\n    BlankView,\n  },\n  beforeRouteEnter: verifyQueryParams,\n  beforeRouteUpdate: verifyQueryParams,\n  setup() {\n    const route = useRoute()\n    const router = useRouter()\n\n    const sources = _sources.map(id => {\n      return {\n        id,\n        label: sourceNames.value[id],\n      }\n    })\n    const handleSourceChange = (id) => {\n      void router.replace({\n        path: route.path,\n        query: {\n          ...route.query,\n          source: id,\n          page: 1,\n        },\n      })\n    }\n\n    const searchTypes = computed(() => {\n      return [\n        { label: window.i18n.t('search__type_music'), id: 'music' },\n        { label: window.i18n.t('search__type_songlist'), id: 'songlist' },\n      ]\n    })\n    const handleTypeChange = (type) => {\n      void router.replace({\n        path: route.path,\n        query: {\n          ...route.query,\n          type,\n          page: 1,\n        },\n      })\n    }\n\n\n    return {\n      sources,\n      source,\n      handleSourceChange,\n      searchTypes,\n      searchType,\n      handleTypeChange,\n      page,\n      searchText,\n    }\n  },\n}\n\n\n</script>\n\n<style lang=\"less\" module>\n.container {\n  display: flex;\n  flex-flow: column nowrap;\n}\n\n.header {\n  // padding: 5px 0;\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  justify-content: space-between;\n}\n\n.main {\n  position: relative;\n  flex: auto;\n  // min-height: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/DislikeListModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"modelValue\" teleport=\"#view\" height=\"80%\" width=\"80%\" @close=\"$emit('update:modelValue', false)\")\n  main(:class=\"$style.main\")\n    h2 {{ $t('setting__dislike_list_title') }}\n    div(:class=\"$style.content\")\n      textarea.scroll(v-model=\"rules\" :class=\"$style.textarea\" :placeholder=\"$t('setting__dislike_list_input_tip')\")\n  div(:class=\"$style.footer\")\n    div(:class=\"$style.tips\") {{ $t('setting__dislike_list_tips') }}\n    base-btn(:class=\"$style.btn\" @click=\"handleSave\") {{ $t('setting__dislike_list_save_btn') }}\n</template>\n\n<script>\nimport { watch, ref } from '@common/utils/vueTools'\nimport { overwirteDislikeInfo } from '@renderer/core/dislikeList'\nimport { dislikeInfo } from '@renderer/store/dislikeList'\n\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue'],\n  setup(props, { emit }) {\n    const rules = ref('')\n\n    const handleSave = async() => {\n      if (rules.value.trim() != dislikeInfo.rules.trim()) {\n        await overwirteDislikeInfo(rules.value)\n      }\n      emit('update:modelValue', false)\n    }\n\n    watch(() => props.modelValue, (visible) => {\n      if (!visible) return\n      rules.value = dislikeInfo.rules.length ? dislikeInfo.rules + '\\n' : dislikeInfo.rules\n    })\n\n    return {\n      rules,\n      handleSave,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  // padding: 15px;\n  // max-width: 400px;\n  // min-width: 460px;\n  // min-height: 200px;\n  // width: ;\n  flex: auto;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  // min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    margin: 20px;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n\n.content {\n  flex: auto;\n  // min-height: 100px;\n  max-height: 100%;\n  display: flex;\n  padding: 0 15px;\n}\n.textarea {\n  width: 100%;\n  height: 100%;\n  border: none;\n  outline: none;\n  border-radius: 4px;\n  padding: 5px;\n  background-color: var(--color-primary-light-200-alpha-900);\n  box-sizing: border-box;\n  font-family: inherit;\n  resize: none;\n}\n\n.footer {\n  box-sizing: border-box;\n  flex: none;\n  // width: @width;\n  padding: 15px 15px;\n  // padding: 2px 0;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n.tips {\n  // padding: 10px 15px;\n  font-size: 12px;\n  line-height: 1.25;\n  color: var(--color-550);\n  white-space: pre-wrap;\n}\n.btn {\n  min-width: 80px;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/PlayTimeoutModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"modelValue\" teleport=\"#view\" @close=\"handleCloseModal\" @after-enter=\"$refs.dom_input.focus()\")\n  main(:class=\"$style.main\")\n    h2 {{ $t('play_timeout') }}\n    div(:class=\"$style.content\")\n      div(:class=\"[$style.row, $style.inputGroup]\")\n        base-input(ref=\"dom_input\" v-model=\"time\" :class=\"$style.input\" type=\"number\")\n        p(:class=\"$style.inputLabel\") {{ $t('play_timeout_unit') }}\n      div(:class=\"$style.row\")\n        base-checkbox(id=\"play_timeout_end\" :model-value=\"appSetting['player.waitPlayEndStop']\" :label=\"$t('play_timeout_end')\" @update:model-value=\"updateSetting({'player.waitPlayEndStop': $event})\")\n      div(:class=\"[$style.row, $style.tip, { [$style.show]: !!timeLabel }]\")\n        p {{ $t('play_timeout_tip', { time: timeLabel }) }}\n    div(:class=\"$style.footer\")\n      base-btn(:class=\"$style.footerBtn\" @click=\"handleCancel\") {{ $t(timeLabel ? 'play_timeout_stop' : 'play_timeout_close') }}\n      base-btn(:class=\"$style.footerBtn\" @click=\"handleConfirm\") {{ $t(timeLabel ? 'play_timeout_update' : 'play_timeout_confirm') }}\n</template>\n\n<script>\nimport { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/core/player/timeoutStop'\nimport { ref } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nconst MAX_MIN = 1440\n\nconst rxp = /([1-9]\\d*)/\n\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue'],\n  setup(props, { emit }) {\n    const { timeLabel } = useTimeout()\n    const time = ref(appSetting['player.waitPlayEndStopTime'])\n\n    const handleCloseModal = () => {\n      emit('update:modelValue', false)\n    }\n    const handleCancel = () => {\n      if (timeLabel.value) {\n        stopTimeoutStop()\n      }\n      handleCloseModal()\n    }\n    const verify = () => {\n      const orgText = time.value\n      let text = time.value\n\n      if (rxp.test(text)) {\n        text = RegExp.$1\n        if (parseInt(text) > MAX_MIN) {\n          text = MAX_MIN\n        }\n      } else {\n        text = ''\n      }\n      time.value = text\n      return text && orgText == text ? parseInt(text) : ''\n    }\n    const handleConfirm = () => {\n      let time = verify()\n      if (time == '') return\n      if (appSetting['player.waitPlayEndStopTime'] != time) updateSetting({ 'player.waitPlayEndStopTime': time })\n      startTimeoutStop(time * 60)\n      handleCloseModal()\n    }\n    return {\n      appSetting,\n      updateSetting,\n      timeLabel,\n      time,\n      handleCloseModal,\n      handleCancel,\n      handleConfirm,\n    }\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px;\n  max-width: 530px;\n  min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n.content {\n  padding-top: 15px;\n  font-size: 14px;\n}\n.row {\n  padding-top: 5px;\n}\n.inputGroup {\n  display: flex;\n  align-items: center;\n}\n.input {\n  flex: auto;\n}\n.inputLabel {\n  flex: none;\n  margin-left: 10px;\n}\n.tip {\n  visibility: hidden;\n\n  &.show {\n    visibility: visible;\n  }\n}\n.footer {\n  margin-top: 20px;\n  display: flex;\n  flex-flow: row nowrap;\n}\n.footerBtn {\n  flex: auto;\n  height: 36px;\n  line-height: 36px;\n  padding: 0 10px !important;\n  width: 150px;\n  .mixin-ellipsis-1();\n  + .footerBtn {\n    margin-left: 15px;\n  }\n}\n.ruleLink {\n  .mixin-ellipsis-1();\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingAbout.vue",
    "content": "<template lang=\"pug\">\ndt#about {{ $t('setting__about') }}\ndd\n  .p.small\n    | 本软件完全免费，代码已开源。开源地址：\n    span.hover.underline(:aria-label=\"$t('setting__click_open')\" @click=\"openUrl('https://github.com/lyswhut/lx-music-desktop#readme')\") https://github.com/lyswhut/lx-music-desktop\n  .p.small\n    | 最新版下载地址：\n    span.hover.underline(:aria-label=\"$t('setting__click_open')\" @click=\"openUrl('https://github.com/lyswhut/lx-music-desktop/releases')\") GitHub Releases\n  .p.small\n    | 软件的常见问题可转至：\n    span.hover.underline(:aria-label=\"$t('setting__click_open')\" @click=\"openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')\") 桌面版常见问题\n  .p.small\n    strong 本软件没有客服\n    | ，但我们整理了一些常见的使用问题。\n    strong 仔细、仔细、仔细\n    | 地阅读常见问题后，\n  .p.small\n    | 仍有问题可到&nbsp;GitHub&nbsp;\n    span.hover.underline(:aria-label=\"$t('setting__click_open')\" @click=\"openUrl('https://github.com/lyswhut/lx-music-desktop/issues?q=is%3Aissue+')\") 提交&nbsp;Issue\n    | 。\n  br\n  .p.small 由于软件开发的初衷仅是为了对新技术的学习与研究，因此软件直至停止维护都将会一直保持纯净。\n  .p.small\n    | 目前本项目的原始发布地址\n    strong 只有&nbsp;GitHub\n    | ，其他渠道均为第三方转载发布，可信度请自行鉴别。\n  .p.small\n    strong 本项目没有微信公众号之类的所谓「官方账号」，谨防被骗！\n\n  .p.small\n    | 你已签署本软件的\n    base-btn(min @click=\"handleShowPact\") 许可协议\n    | ，协议的在线版本在\n    strong.hover.underline(:aria-label=\"$t('setting__click_open')\" @click=\"openUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')\") 这里\n    | 。\n  br\n\n  .p.small\n    | By:&nbsp;\n    strong 落雪无痕\n</template>\n\n<script>\n// import { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { isShowPact } from '@renderer/store'\nimport { openUrl, clipboardWriteText } from '@common/utils/electron'\n\nexport default {\n  name: 'SettingAbout',\n  setup() {\n    const handleShowPact = () => {\n      isShowPact.value = true\n    }\n    return {\n      openUrl,\n      clipboardWriteText,\n      handleShowPact,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingBackup.vue",
    "content": "<template lang=\"pug\">\ndt#backup {{ $t('setting__backup') }}\ndd\n  h3#backup_part {{ $t('setting__backup_part') }}\n  div\n    base-btn.btn.gap-left(min @click=\"handleImportPlayList\") {{ $t('setting__backup_part_import_list') }}\n    base-btn.btn.gap-left(min @click=\"handleExportPlayList\") {{ $t('setting__backup_part_export_list') }}\n    base-btn.btn.gap-left(min @click=\"handleImportSetting\") {{ $t('setting__backup_part_import_setting') }}\n    base-btn.btn.gap-left(min @click=\"handleExportSetting\") {{ $t('setting__backup_part_export_setting') }}\ndd\n  h3#backup_all {{ $t('setting__backup_all') }}\n  div\n    base-btn.btn.gap-left(min @click=\"handleImportAllData\") {{ $t('setting__backup_all_import') }}\n    base-btn.btn.gap-left(min @click=\"handleExportAllData\") {{ $t('setting__backup_all_export') }}\ndd\n  h3#backup_other {{ $t('setting__backup_other') }}\n  div\n    base-btn.btn.gap-left(min @click=\"handleExportPlayListToText\") {{ $t('setting__backup_other_export_list_text') }}\n    base-btn.btn.gap-left(min @click=\"handleExportPlayListToCsv\") {{ $t('setting__backup_other_export_list_csv') }}\n</template>\n\n<script>\nimport { toRaw } from '@common/utils/vueTools'\n// import { mergeSetting } from '@common/utils'\n// import { base as eventBaseName } from '@renderer/event/names'\n// import { defaultList, loveList, userLists } from '@renderer/core/share/list'\nimport {\n  toNewMusicInfo,\n  // toOldMusicInfo,\n  filterMusicList,\n  fixNewMusicInfoQuality,\n} from '@renderer/utils'\nimport {\n  showSelectDialog,\n  openSaveDir,\n} from '@renderer/utils/ipc'\n// import { currentStting } from '../setting'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport useImportTip from '@renderer/utils/compositions/useImportTip'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { getListMusics, overwriteListFull, overwriteListMusics } from '@renderer/store/list/action'\nimport { LIST_IDS } from '@common/constants'\nimport { defaultList, loveList, userLists } from '@renderer/store/list/state'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport migrateSetting from '@common/utils/migrateSetting'\n\n\nexport default {\n  name: 'SettingUpdate',\n  setup() {\n    const t = useI18n()\n    // const setting = useRefGetter('setting')\n    // const settingVersion = useRefGetter('settingVersion')\n    // const setSettingVersion = useCommit('setSettingVersion')\n    // const setList = useCommit('list', 'setList')\n    const showImportTip = useImportTip()\n\n    const getAllLists = async() => {\n      const lists = []\n      lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics) })))\n      lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics) })))\n\n      for await (const list of userLists) {\n        lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics) })))\n      }\n\n      return lists\n    }\n\n    const importOldListData = async(lists) => {\n      const allLists = await getAllLists()\n      for (const list of lists) {\n        try {\n          const targetList = allLists.find(l => l.id == list.id)\n          if (targetList) {\n            targetList.list = filterMusicList(list.list.map(m => toNewMusicInfo(m)))\n          } else {\n            allLists.push({\n              name: list.name,\n              id: list.id,\n              list: filterMusicList(list.list.map(m => toNewMusicInfo(m))),\n              source: list.source,\n              sourceListId: list.sourceListId,\n              locationUpdateTime: list.locationUpdateTime ?? null,\n            })\n          }\n        } catch (err) {\n          console.log(err)\n        }\n      }\n      const defaultList = allLists.shift().list\n      const loveList = allLists.shift().list\n      await overwriteListFull({ defaultList, loveList, userList: allLists })\n    }\n    const importNewListData = async(lists) => {\n      const allLists = await getAllLists()\n      for (const list of lists) {\n        try {\n          const targetList = allLists.find(l => l.id == list.id)\n          if (targetList) {\n            targetList.list = filterMusicList(list.list).map(m => fixNewMusicInfoQuality(m))\n          } else {\n            allLists.push({\n              name: list.name,\n              id: list.id,\n              list: filterMusicList(list.list).map(m => fixNewMusicInfoQuality(m)),\n              source: list.source,\n              sourceListId: list.sourceListId,\n              locationUpdateTime: list.locationUpdateTime ?? null,\n            })\n          }\n        } catch (err) {\n          console.log(err)\n        }\n      }\n      const defaultList = allLists.shift().list\n      const loveList = allLists.shift().list\n      await overwriteListFull({ defaultList, loveList, userList: allLists })\n    }\n    const importOldSettingData = (setting) => {\n      console.log(setting)\n      setting = migrateSetting(setting)\n      setting['common.isAgreePact'] = false\n      updateSetting(setting)\n    }\n    const importNewSettingData = (setting) => {\n      setting['common.isAgreePact'] = false\n      updateSetting(setting)\n    }\n\n\n    const importAllData = async(path) => {\n      let allData\n      try {\n        allData = await window.lx.worker.main.readLxConfigFile(path)\n      } catch (error) {\n        return\n      }\n\n      switch (allData.type) {\n        case 'allData':\n          // 兼容0.6.2及以前版本的列表数据\n          if (allData.defaultList) await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: filterMusicList(allData.defaultList.list.map(m => toNewMusicInfo(m))) })\n          else await importOldListData(allData.playList)\n          importOldSettingData(allData.setting)\n          break\n        case 'allData_v2':\n          await importNewListData(allData.playList)\n          importNewSettingData(allData.setting)\n          break\n        default: { showImportTip(allData.type) }\n      }\n    }\n    const handleImportAllData = () => {\n      void showSelectDialog({\n        title: t('setting__backup_all_import_desc'),\n        properties: ['openFile'],\n        filters: [\n          { name: 'Setting', extensions: ['json', 'lxmc'] },\n          { name: 'All Files', extensions: ['*'] },\n        ],\n      }).then(result => {\n        if (result.canceled) return\n        void dialog.confirm({\n          message: t('setting__backup_part_import_list_confirm'),\n          cancelButtonText: t('cancel_button_text'),\n          confirmButtonText: t('confirm_button_text'),\n        }).then(confirm => {\n          if (!confirm) return\n          void importAllData(result.filePaths[0])\n        })\n      })\n    }\n\n    const exportAllData = async(path) => {\n      let allData = {\n        type: 'allData_v2',\n        setting: { ...appSetting },\n        playList: await getAllLists(),\n      }\n      void window.lx.worker.main.saveLxConfigFile(path, allData)\n    }\n    const handleExportAllData = () => {\n      void openSaveDir({\n        title: t('setting__backup_all_export_desc'),\n        defaultPath: 'lx_datas_v2.lxmc',\n      }).then(result => {\n        if (result.canceled) return\n        void exportAllData(result.filePath)\n      })\n    }\n\n    const exportSetting = (path) => {\n      const data = {\n        type: 'setting_v2',\n        data: { ...appSetting },\n      }\n      void window.lx.worker.main.saveLxConfigFile(path, data)\n    }\n    const handleExportSetting = () => {\n      void openSaveDir({\n        title: t('setting__backup_part_export_setting_desc'),\n        defaultPath: 'lx_setting_v2.lxmc',\n      }).then(result => {\n        if (result.canceled) return\n        exportSetting(result.filePath)\n      })\n    }\n\n    const importSetting = async(path) => {\n      let settingData\n      try {\n        settingData = await window.lx.worker.main.readLxConfigFile(path)\n      } catch (error) {\n        return\n      }\n\n      switch (settingData.type) {\n        case 'setting':\n          importOldSettingData(settingData.data)\n          break\n        case 'setting_v2':\n          importNewSettingData(settingData.data)\n          break\n        default: { showImportTip(settingData.type) }\n      }\n    }\n    const handleImportSetting = () => {\n      void showSelectDialog({\n        title: t('setting__backup_part_import_setting_desc'),\n        properties: ['openFile'],\n        filters: [\n          { name: 'Setting', extensions: ['json', 'lxmc'] },\n          { name: 'All Files', extensions: ['*'] },\n        ],\n      }).then(result => {\n        if (result.canceled) return\n        void importSetting(result.filePaths[0])\n      })\n    }\n\n    const exportPlayList = async(path) => {\n      const data = {\n        type: 'playList_v2',\n        data: await getAllLists(),\n      }\n      void window.lx.worker.main.saveLxConfigFile(path, data)\n    }\n    const handleExportPlayList = () => {\n      void openSaveDir({\n        title: t('setting__backup_part_export_list_desc'),\n        defaultPath: 'lx_list.lxmc',\n      }).then(result => {\n        if (result.canceled) return\n        void exportPlayList(result.filePath)\n      })\n    }\n\n    const importPlayList = async(path) => {\n      let listData\n      try {\n        listData = await window.lx.worker.main.readLxConfigFile(path)\n      } catch (error) {\n        return\n      }\n      console.log(listData.type)\n\n      switch (listData.type) {\n        case 'defautlList': // 兼容0.6.2及以前版本的列表数据\n          await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: filterMusicList(listData.data.list.map(m => toNewMusicInfo(m))) })\n          break\n        case 'playList':\n          await importOldListData(listData.data)\n          break\n        case 'playList_v2':\n          await importNewListData(listData.data)\n          break\n        default: { showImportTip(listData.type) }\n      }\n    }\n    const handleImportPlayList = () => {\n      void showSelectDialog({\n        title: t('setting__backup_part_import_list_desc'),\n        properties: ['openFile'],\n        filters: [\n          { name: 'Play List', extensions: ['json', 'lxmc'] },\n          { name: 'All Files', extensions: ['*'] },\n        ],\n      }).then(result => {\n        if (result.canceled) return\n        void dialog.confirm({\n          message: t('setting__backup_part_import_list_confirm'),\n          cancelButtonText: t('cancel_button_text'),\n          confirmButtonText: t('confirm_button_text'),\n        }).then(confirm => {\n          if (!confirm) return\n          void importPlayList(result.filePaths[0])\n        })\n      })\n    }\n\n    const exportPlayListToText = async(savePath, isMerge) => {\n      const lists = await getAllLists()\n      await window.lx.worker.main.exportPlayListToText(savePath, lists, isMerge)\n    }\n    const handleExportPlayListToText = async() => {\n      const confirm = await dialog.confirm({\n        message: t('setting__backup_other_export_list_text_confirm'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('confirm_button_text'),\n      })\n      if (confirm) {\n        void openSaveDir({\n          title: t('setting__backup_other_export_dir'),\n          defaultPath: 'lx_list_all.txt',\n        }).then(result => {\n          if (result.canceled) return\n          let path = result.filePath\n          if (!path.endsWith('.txt')) path += '.txt'\n          void exportPlayListToText(path, true)\n        })\n      } else {\n        void showSelectDialog({\n          title: t('setting__backup_other_export_dir'),\n          // defaultPath: currentStting.value.download.savePath,\n          properties: ['openDirectory'],\n        }).then(result => {\n          if (result.canceled) return\n          void exportPlayListToText(result.filePaths[0], false)\n        })\n      }\n    }\n\n    const exportPlayListToCsv = async(savePath, isMerge) => {\n      const lists = await getAllLists()\n      await window.lx.worker.main.exportPlayListToCSV(savePath, lists, isMerge, `${t('music_name')},${t('music_singer')},${t('music_album')}\\n`)\n    }\n    const handleExportPlayListToCsv = async() => {\n      const confirm = await dialog.confirm({\n        message: t('setting__backup_other_export_list_text_confirm'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('confirm_button_text'),\n      })\n      if (confirm) {\n        void openSaveDir({\n          title: t('setting__backup_other_export_dir'),\n          defaultPath: 'lx_list_all.csv',\n        }).then(result => {\n          if (result.canceled) return\n          let path = result.filePath\n          if (!path.endsWith('.csv')) path += '.csv'\n          void exportPlayListToCsv(path, true)\n        })\n      } else {\n        void showSelectDialog({\n          title: t('setting__backup_other_export_dir'),\n          // defaultPath: currentStting.value.download.savePath,\n          properties: ['openDirectory'],\n        }).then(result => {\n          if (result.canceled) return\n          void exportPlayListToCsv(result.filePaths[0], false)\n        })\n      }\n    }\n\n    // window.eventHub.on(eventBaseName.set_config, handleUpdateSetting)\n\n    // onBeforeUnmount(() => {\n    //   window.eventHub.off(eventBaseName.set_config, handleUpdateSetting)\n    // })\n\n    return {\n      // currentStting,\n      handleExportPlayList,\n      handleImportPlayList,\n      handleExportSetting,\n      handleImportSetting,\n      handleExportAllData,\n      handleImportAllData,\n      handleExportPlayListToText,\n      handleExportPlayListToCsv,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n.savePath {\n  font-size: 12px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingBasic.vue",
    "content": "<template lang=\"pug\">\ndt#basic {{ $t('setting__basic') }}\ndd\n  div\n    .gap-top\n      base-checkbox(id=\"setting_show_animate\" :model-value=\"appSetting['common.isShowAnimation']\" :label=\"$t('setting__basic_show_animation')\" @update:model-value=\"updateSetting({'common.isShowAnimation': $event})\")\n    .gap-top\n      base-checkbox(id=\"setting_animate\" :disabled=\"!appSetting['common.isShowAnimation']\" :model-value=\"appSetting['common.randomAnimate']\" :label=\"$t('setting__basic_animation')\" @update:model-value=\"updateSetting({'common.randomAnimate': $event})\")\n    .gap-top\n      base-checkbox(id=\"setting_start_in_fullscreen\" :model-value=\"appSetting['common.startInFullscreen']\" :label=\"$t('setting__basic_start_in_fullscreen')\" @update:model-value=\"updateSetting({'common.startInFullscreen': $event})\")\n    .gap-top\n      base-checkbox(id=\"setting_to_tray\" :model-value=\"appSetting['tray.enable']\" :label=\"$t('setting__basic_to_tray')\" @update:model-value=\"updateSetting({'tray.enable': $event})\")\n    .p.gap-top\n      base-btn.btn(min @click=\"isShowPlayTimeoutModal = true\") {{ $t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}\n\ndd\n  h3#basic_theme {{ $t('setting__basic_theme') }}\n  div\n    ul(:class=\"$style.theme\")\n      li(v-for=\"theme in themeList\" :key=\"theme.id\" :aria-label=\"theme.name\" :style=\"theme.styles\" :class=\"[$style.themeItem, {[$style.active]: themeId == theme.id}]\" @click=\"toggleTheme(theme)\" @contextmenu=\"handleEditTheme(theme)\")\n        div(:class=\"$style.bg\")\n        span(:class=\"$style.label\") {{ theme.name }}\n      li(v-if=\"showAllTheme || themeId == 'auto'\" :aria-label=\"$t('theme_auto_tip')\" :style=\"autoTheme\" :class=\"[$style.themeItem, $style.auto, {[$style.active]: themeId == 'auto'}]\" @click=\"handleSetThemeAuto\" @contextmenu=\"isShowThemeSelectorModal = true\")\n        div(:class=\"$style.bg\")\n          div(:class=\"$style.bgContent\")\n            div(:class=\"$style.light\")\n            div(:class=\"$style.dark\")\n        span(:class=\"$style.label\") {{ $t('theme_auto') }}\n      li(v-if=\"showAllTheme\" :aria-label=\"$t('theme_add')\" :class=\"[$style.themeItem, $style.add]\" @click=\"handleEditTheme()\")\n        div(:class=\"$style.bg\")\n          div(:class=\"$style.bgContent\")\n            svg-icon(:class=\"$style.icon\" name=\"plus\")\n        span(:class=\"$style.label\") {{ $t('theme_add') }}\n      li(v-if=\"!showAllTheme\" :aria-label=\"$t('theme_more_btn_show')\" :class=\"[$style.themeItem, $style.moreThme]\" @click=\"showAllTheme = true\")\n        span(:class=\"$style.label\") {{ $t('theme_more_btn_show') }}\n        svg-icon(name=\"angle-right-solid\" :class=\"$style.activeIcon\")\n\ndd\n  h3#basic_source {{ $t('setting__basic_source') }}\n  div\n    .gap-top(v-for=\"item in apiSources\" :key=\"item.id\")\n      base-checkbox(\n        :id=\"`setting_api_source_${item.id}`\" name=\"setting_api_source\"\n        need :model-value=\"appSetting['common.apiSource']\" :disabled=\"item.disabled\" :value=\"item.id\" :aria-label=\"item.label\" @update:model-value=\"updateSetting({'common.apiSource': $event})\")\n        span(:class=\"$style.sourceLabel\")\n          | {{ item.name }}\n          span(v-if=\"item.desc\" :class=\"$style.desc\") {{ item.desc }}\n          span(v-if=\"item.statusLabel\" :class=\"$style.status\") {{ item.statusLabel }}\n    .p.gap-top\n      base-btn.btn(min @click=\"isShowUserApiModal = true\") {{ $t('setting__basic_source_user_api_btn') }}\n\ndd\n  h3#basic_window_size {{ $t('setting__basic_window_size') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in windowSizeList\" :id=\"`setting_window_size_${item.id}`\" :key=\"item.id\"\n      name=\"setting_window_size\" need :model-value=\"appSetting['common.windowSizeId']\" :disabled=\"isFullscreen\" :value=\"item.id\" :label=\"$t('setting__basic_window_size_' + item.name)\"\n      @update:model-value=\"updateSetting({'common.windowSizeId': $event})\")\n\ndd\n  h3#basic_font_size {{ $t('setting__basic_font_size') }}\n  div\n    //- base-selection.gap-teft(:list=\"fontSizeList\" :model-value=\"appSetting['common.fontSize']\" @update:model-value=\"updateSetting({'common.fontSize': $event})\")\n    base-checkbox.gap-left(\n      v-for=\"item in fontSizeList\" :id=\"`setting_basic_font_size_${item.id}`\" :key=\"item.id\"\n      name=\"setting_basic_font_size\" need :model-value=\"appSetting['common.fontSize']\" :value=\"item.id\"\n      :label=\"item.label\" :disabled=\"isFullscreen\" @update:model-value=\"updateSetting({'common.fontSize': $event})\")\n\ndd\n  h3#basic_font {{ $t('setting__basic_font') }}\n  div(style=\"--selection-width: 12rem;\")\n    base-selection.gap-left(:list=\"fontList\" :model-value=\"fonts[0]\" item-key=\"id\" item-name=\"label\" @update:model-value=\"updateFonts($event, fonts[1])\")\n    base-selection.gap-left(v-if=\"fonts[0]\" :list=\"fontList\" :model-value=\"fonts[1]\" item-key=\"id\" item-name=\"label\" @update:model-value=\"updateFonts(fonts[0], $event)\")\n    //- base-selection.gap-teft(:list=\"fontList\" :model-value=\"appSetting['common.font']\" item-key=\"id\" item-name=\"label\" @update:model-value=\"updateSetting({'common.font': $event})\")\n\ndd\n  h3#basic_lang {{ $t('setting__basic_lang') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in langList\" :id=\"`setting_lang_${item.locale}`\" :key=\"item.locale\" name=\"setting_lang\"\n      need :model-value=\"appSetting['common.langId']\" :value=\"item.locale\" :label=\"item.name\" @update:model-value=\"updateSetting({'common.langId': $event})\")\n\ndd\n  h3#basic_sourcename {{ $t('setting__basic_sourcename') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in sourceNameTypes\" :id=\"`setting_abasic_sourcename_${item.id}`\" :key=\"item.id\"\n      name=\"setting_basic_sourcename\" need :model-value=\"appSetting['common.sourceNameType']\" :value=\"item.id\" :label=\"item.label\" @update:model-value=\"updateSetting({'common.sourceNameType': $event})\")\ndd\n  h3#basic_control_btn_position {{ $t('setting__basic_control_btn_position') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in controlBtnPositionList\" :id=\"`setting_basic_control_btn_position_${item.id}`\" :key=\"item.id\"\n      name=\"setting_basic_control_btn_position\" need :model-value=\"appSetting['common.controlBtnPosition']\" :value=\"item.id\" :label=\"item.name\" @update:model-value=\"updateSetting({'common.controlBtnPosition': $event})\")\ndd\n  h3#basic_playbar_progress_style {{ $t('setting__basic_playbar_progress_style') }}\n  div\n    base-checkbox.gap-left(\n      id=\"setting_basic_playbar_progress_style_mini\" name=\"setting_basic_playbar_progress_style\"\n      need :model-value=\"appSetting['common.playBarProgressStyle']\" value=\"mini\" :label=\"$t('setting__basic_playbar_progress_style_mini')\" @update:model-value=\"updateSetting({'common.playBarProgressStyle': $event})\")\n    base-checkbox.gap-left(\n      id=\"setting_basic_playbar_progress_style_middle\" name=\"setting_basic_playbar_progress_style\"\n      need :model-value=\"appSetting['common.playBarProgressStyle']\" value=\"middle\" :label=\"$t('setting__basic_playbar_progress_style_middle')\" @update:model-value=\"updateSetting({'common.playBarProgressStyle': $event})\")\n    base-checkbox.gap-left(\n      id=\"setting_basic_playbar_progress_style_full\" name=\"setting_basic_playbar_progress_style\"\n      need :model-value=\"appSetting['common.playBarProgressStyle']\" value=\"full\" :label=\"$t('setting__basic_playbar_progress_style_full')\" @update:model-value=\"updateSetting({'common.playBarProgressStyle': $event})\")\n\nThemeSelectorModal(v-model=\"isShowThemeSelectorModal\")\nThemeEditModal(v-model=\"isShowThemeEditModal\" :theme-id=\"editThemeId\" @submit=\"handleRefreshTheme\")\nplay-timeout-modal(v-model=\"isShowPlayTimeoutModal\")\nuser-api-modal(v-model=\"isShowUserApiModal\")\n</template>\n\n<script>\nimport { computed, ref, watch, reactive, shallowReactive } from '@common/utils/vueTools'\nimport { windowSizeList, userApi, isFullscreen, themeId } from '@renderer/store'\nimport { langList, useI18n } from '@root/lang'\nimport { getSystemFonts } from '@renderer/utils/ipc'\nimport apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'\nimport { useTimeout } from '@renderer/core/player/timeoutStop'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nimport ThemeSelectorModal from './ThemeSelectorModal.vue'\nimport ThemeEditModal from './ThemeEditModal/index.vue'\nimport PlayTimeoutModal from './PlayTimeoutModal.vue'\nimport UserApiModal from './UserApiModal.vue'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { getThemes, applyTheme, findTheme, buildBgUrl } from '@renderer/store/utils'\n\nexport default {\n  name: 'SettingBasic',\n  components: {\n    ThemeSelectorModal,\n    ThemeEditModal,\n    PlayTimeoutModal,\n    UserApiModal,\n  },\n  setup() {\n    const t = useI18n()\n\n    const showAllTheme = ref(false)\n    const defaultThemesRaw = shallowReactive([])\n    const defaultThemes = computed(() => {\n      return defaultThemesRaw.map(theme => ({ ...theme, isDefault: true, name: t('theme_' + theme.id) }))\n    })\n    const userThemes = shallowReactive([])\n    const allThemes = computed(() => {\n      return [...defaultThemes.value, ...userThemes]\n    })\n    const themeList = computed(() => {\n      if (!allThemes.value.length) return []\n      return showAllTheme.value\n        ? allThemes.value\n        : themeId.value == 'auto'\n          ? []\n          : [allThemes.value.find(t => t.id == themeId.value) ?? allThemes.value[0]]\n    })\n    const autoTheme = reactive({})\n    const updateAutoTheme = (info) => {\n      let light = findTheme(info, appSetting['theme.lightId'])\n      light ??= info.themes.find(theme => theme.id == 'green')\n      let dark = findTheme(info, appSetting['theme.darkId'])\n      dark ??= info.themes.find(theme => theme.id == 'black')\n      autoTheme['--color-primary-theme-light'] = light.config.themeColors['--color-theme']\n      autoTheme['--background-image-theme-light'] = light.isCustom\n        ? light.config.extInfo['--background-image'] == 'none'\n          ? 'none'\n          : buildBgUrl(light.config.extInfo['--background-image'], info.dataPath)\n        : light.config.extInfo['--background-image']\n      autoTheme['--color-primary-theme-dark'] = dark.config.themeColors['--color-theme']\n      autoTheme['--background-image-theme-dark'] = dark.isCustom\n        ? dark.config.extInfo['--background-image'] == 'none'\n          ? 'none'\n          : buildBgUrl(dark.config.extInfo['--background-image'], info.dataPath)\n        : dark.config.extInfo['--background-image']\n    }\n\n    let dataPath = ''\n    const init = () => {\n      getThemes((info) => {\n        // console.log(info)\n        dataPath = info.dataPath\n        defaultThemesRaw.splice(0, defaultThemesRaw.length, ...info.themes.map(t => {\n          return {\n            id: t.id,\n            styles: {\n              '--color-primary-theme': t.config.themeColors['--color-theme'],\n              '--background-image-theme': t.config.extInfo['--background-image'],\n            },\n          }\n        }))\n        userThemes.splice(0, userThemes.length, ...info.userThemes.map(t => {\n          return {\n            id: t.id,\n            name: t.name,\n            styles: {\n              '--color-primary-theme': t.config.themeColors['--color-theme'],\n              '--background-image-theme': t.config.extInfo['--background-image'] == 'none'\n                ? 'none'\n                : buildBgUrl(t.config.extInfo['--background-image'], info.dataPath),\n            },\n          }\n        }))\n        updateAutoTheme(info)\n      })\n    }\n    const editThemeId = ref('')\n    const handleEditTheme = (theme) => {\n      // console.log(theme)\n      if (theme?.isDefault) return\n      if (!theme && userThemes.length >= 10) {\n        void dialog({\n          message: t('theme_max_tip'),\n          confirmButtonText: t('alert_button_text'),\n        })\n        return\n      }\n      editThemeId.value = theme ? theme.id : ''\n      isShowThemeEditModal.value = true\n    }\n    const handleRefreshTheme = () => {\n      init()\n    }\n    init()\n    const toggleTheme = (theme) => {\n      if (themeId.value == theme.id) return\n      themeId.value = theme.id\n      applyTheme(theme.id, appSetting['theme.lightId'], appSetting['theme.darkId'], dataPath)\n      updateSetting({ 'theme.id': theme.id })\n    }\n\n    watch(() => [appSetting['theme.lightId'], appSetting['theme.darkId']], () => {\n      getThemes(updateAutoTheme)\n    })\n    const isShowThemeSelectorModal = ref(false)\n    const handleSetThemeAuto = () => {\n      if (themeId.value == 'auto') return\n      if (window.localStorage.getItem('theme-auto-tip') != 'true') {\n        window.localStorage.setItem('theme-auto-tip', 'true')\n        void dialog({\n          message: t('setting__basic_theme_auto_tip'),\n          confirmButtonText: t('ok'),\n        })\n      }\n      toggleTheme({ id: 'auto' })\n    }\n    const isShowThemeEditModal = ref(false)\n\n    const isShowPlayTimeoutModal = ref(false)\n    const { timeLabel } = useTimeout()\n\n    const isShowUserApiModal = ref(false)\n    const getApiStatus = () => {\n      let status\n      if (userApi.status) status = t('setting__basic_source_status_success')\n      else if (userApi.message == 'initing') status = t('setting__basic_source_status_initing')\n      else status = `${t('setting__basic_source_status_failed')}`\n\n      return status\n    }\n    const apiSources = computed(() => {\n      return [\n        ...apiSourceInfo.map(api => ({\n          id: api.id,\n          name: api.name,\n          label: api.name,\n          disabled: api.disabled,\n        })),\n        ...userApi.list.map(api => ({\n          id: api.id,\n          name: api.name,\n          label: `${api.name}${api.id == appSetting['common.apiSource'] ? `[${getApiStatus()}]` : ''}`,\n          desc: [/^\\d/.test(api.version) ? `v${api.version}` : api.version].filter(Boolean).join(', '),\n          statusLabel: api.id == appSetting['common.apiSource'] ? `[${getApiStatus()}]` : '',\n          status: api.status,\n          message: api.message,\n          disabled: false,\n        })),\n      ]\n    })\n\n    const sourceNameTypes = computed(() => {\n      return [\n        { id: 'real', label: t('setting__basic_sourcename_real') },\n        { id: 'alias', label: t('setting__basic_sourcename_alias') },\n      ]\n    })\n\n\n    const controlBtnPositionList = computed(() => {\n      return [\n        { id: 'left', name: t('setting__basic_control_btn_position_left') },\n        { id: 'right', name: t('setting__basic_control_btn_position_right') },\n      ]\n    })\n\n    const systemFontList = ref([])\n    const fontList = computed(() => {\n      return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]\n    })\n    void getSystemFonts().then(fonts => {\n      systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^\"|\"$)/g, '') }))\n    })\n\n    const fonts = computed(() => {\n      if (!appSetting['common.font']) return ['', '']\n      let [f1 = '', f2 = ''] = appSetting['common.font'].split(',')\n      return [f1.trim(), f2.trim()]\n    })\n    const updateFonts = (font1, font2) => {\n      let font = []\n      if (font1) font.push(font1)\n      if (font2) font.push(font2)\n      updateSetting({ 'common.font': font.join(', ') })\n    }\n    const fontSizeList = computed(() => {\n      return [\n        { id: 14, label: t('setting__basic_font_size_14px') },\n        { id: 15, label: t('setting__basic_font_size_15px') },\n        { id: 16, label: t('setting__basic_font_size_16px') },\n        { id: 17, label: t('setting__basic_font_size_17px') },\n        { id: 18, label: t('setting__basic_font_size_18px') },\n        { id: 19, label: t('setting__basic_font_size_19px') },\n      ]\n    })\n\n\n    return {\n      appSetting,\n      updateSetting,\n      userThemes,\n      autoTheme,\n      showAllTheme,\n      themeList,\n      fonts,\n      updateFonts,\n      // currentStting,\n      // themes,\n      // themeClassName,\n      isShowThemeSelectorModal,\n      isShowThemeEditModal,\n      handleSetThemeAuto,\n      isShowPlayTimeoutModal,\n      timeLabel,\n      apiSources,\n      isShowUserApiModal,\n      windowSizeList,\n      langList,\n      sourceNameTypes,\n      controlBtnPositionList,\n      fontList,\n      isFullscreen,\n      toggleTheme,\n      themeId,\n      handleRefreshTheme,\n      editThemeId,\n      handleEditTheme,\n      fontSizeList,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.theme {\n  display: flex;\n  flex-flow: row wrap;\n  // padding: 0 15px;\n  margin-bottom: -20px;\n\n  .themeItem {\n    display: flex;\n    flex-flow: column nowrap;\n    align-items: center;\n    cursor: pointer;\n    // color: var(--color-primary);\n    margin-right: 8px;\n    transition: .3s ease;\n    transition-property: color, opacity;\n    margin-bottom: 18px;\n    width: 86px;\n\n    &:hover {\n      opacity: .7;\n    }\n\n    &:last-child {\n      margin-right: 0;\n    }\n\n    &.active {\n      color: var(--color-primary-font-active);\n      .bg {\n        border-color: var(--color-primary-font-active);\n      }\n\n      &:hover {\n        opacity: 1;\n      }\n    }\n\n    .bg {\n      display: block;\n      width: 36px;\n      height: 36px;\n      margin-bottom: 5px;\n      border: 2Px solid transparent;\n      padding: 2Px;\n      transition: border-color .3s ease;\n      border-radius: 5px;\n      &:after {\n        display: block;\n        content: ' ';\n        width: 100%;\n        height: 100%;\n        border-radius: @radius-border;\n        background-position: center;\n        background-size: cover;\n        background-repeat: no-repeat;\n        background-color: var(--color-primary-theme);\n        background-image: var(--background-image-theme);\n      }\n    }\n\n    .label {\n      width: 100%;\n      text-align: center;\n      height: 1.2em;\n    }\n\n    &.auto {\n\n      &.active {\n        color: var(--color-primary-font-active);\n        .bg {\n          border-color: var(--color-primary-font-active);\n        }\n      }\n\n      >.bg {\n        &:after {\n          content: none;\n        }\n      }\n      .bgContent {\n        position: relative;\n        height: 100%;\n        overflow: hidden;\n        border-radius: 5px;\n      }\n      .light, .dark {\n        position: absolute;\n        left: 0;\n        top: 0;\n        width: 100%;\n        height: 100%;\n        &:after {\n          display: block;\n          content: ' ';\n          width: 100%;\n          height: 100%;\n          background-position: center;\n          background-size: cover;\n          background-repeat: no-repeat;\n        }\n      }\n      .light {\n        &:after {\n          clip-path: polygon(0 0, 100% 0, 0 100%);\n        }\n        svg {\n          fill: var(--color-primary-theme-light);\n        }\n        &:after {\n          background-color: var(--color-primary-theme-light);\n          background-image: var(--background-image-theme-light);\n        }\n      }\n      .dark {\n        &:after {\n          clip-path: polygon(0 100%, 100% 0, 100% 100%);\n        }\n        svg {\n          fill: var(--color-primary-theme-dark);\n        }\n        &:after {\n          background-color: var(--color-primary-theme-dark);\n          background-image: var(--background-image-theme-dark);\n        }\n      }\n    }\n\n    &.add {\n      >.bg {\n        &:after {\n          content: none;\n        }\n        .bgContent {\n          transition: .3s ease;\n          transition-property: border, color;\n          box-sizing: border-box;\n          border: 1Px dashed var(--color-primary-light-100-alpha-300);\n          color: var(--color-primary-light-100-alpha-300);\n          position: relative;\n          height: 100%;\n          overflow: hidden;\n          border-radius: 5px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        .icon {\n          // position: absolute;\n          // font-size: 16px;\n          width: 66%;\n          height: auto;\n        }\n      }\n      .label {\n        color: var(--color-primary-dark-100-alpha-300);\n      }\n    }\n\n    &.moreThme {\n      flex-direction: row;\n      width: auto;\n      gap: 5px;\n      color: var(--color-primary-font-active);\n      .label {\n        height: auto;\n      }\n    }\n  }\n}\n\n.sourceLabel {\n  flex: auto;\n  margin-left: 5px;\n  line-height: 1.5;\n  cursor: pointer;\n\n  .desc {\n    color: var(--color-500);\n    font-size: 12px;\n    margin-left: 5px;\n  }\n\n  .status {\n    margin-left: 5px;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingDesktopLyric.vue",
    "content": "<template lang=\"pug\">\ndt#desktop_lyric {{ $t('setting__desktop_lyric') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_enable\" :model-value=\"appSetting['desktopLyric.enable']\" :label=\"$t('setting__desktop_lyric_enable')\" @update:model-value=\"updateSetting({ 'desktopLyric.enable': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_lock\" :model-value=\"appSetting['desktopLyric.isLock']\" :label=\"$t('setting__desktop_lyric_lock')\" @update:model-value=\"updateSetting({ 'desktopLyric.isLock': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_fullscreen_hide\" :model-value=\"appSetting['desktopLyric.fullscreenHide']\" :label=\"$t('setting__desktop_lyric_fullscreen_hide')\" @update:model-value=\"updateSetting({ 'desktopLyric.fullscreenHide': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_pause_hide\" :model-value=\"appSetting['desktopLyric.pauseHide']\" :label=\"$t('setting__desktop_lyric_pause_hide')\" @update:model-value=\"updateSetting({ 'desktopLyric.pauseHide': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_audio_visualization\" :model-value=\"appSetting['desktopLyric.audioVisualization']\" :label=\"$t('setting__desktop_lyric_audio_visualization')\" @update:model-value=\"updateSetting({ 'desktopLyric.audioVisualization': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_delayScroll\" :model-value=\"appSetting['desktopLyric.isDelayScroll']\" :label=\"$t('setting__desktop_lyric_delay_scroll')\" @update:model-value=\"updateSetting({ 'desktopLyric.isDelayScroll': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_alwaysOnTop\" :model-value=\"appSetting['desktopLyric.isAlwaysOnTop']\" :label=\"$t('setting__desktop_lyric_always_on_top')\" @update:model-value=\"updateSetting({ 'desktopLyric.isAlwaysOnTop': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_showTaskbar\" :model-value=\"appSetting['desktopLyric.isShowTaskbar']\" :label=\"$t('setting__desktop_lyric_show_taskbar')\" @update:model-value=\"updateSetting({ 'desktopLyric.isShowTaskbar': $event })\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__desktop_lyric_show_taskbar_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_alwaysOnTopLoop\" :model-value=\"appSetting['desktopLyric.isAlwaysOnTopLoop']\" :label=\"$t('setting__desktop_lyric_always_on_top_loop')\" @update:model-value=\"updateSetting({ 'desktopLyric.isAlwaysOnTopLoop': $event })\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__desktop_lyric_always_on_top_loop_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_lockScreen\" :model-value=\"appSetting['desktopLyric.isLockScreen']\" :label=\"$t('setting__desktop_lyric_lock_screen')\" @update:model-value=\"updateSetting({ 'desktopLyric.isLockScreen': $event })\")\n  .gap-top(v-if=\"!isLinux\")\n    base-checkbox(id=\"setting_desktop_lyric_hoverHide\" :model-value=\"appSetting['desktopLyric.isHoverHide']\" :label=\"$t('setting__desktop_lyric_hover_hide')\" @update:model-value=\"updateSetting({ 'desktopLyric.isHoverHide': $event })\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__desktop_lyric_hover_hide_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_ellipsis\" :model-value=\"appSetting['desktopLyric.style.ellipsis']\" :label=\"$t('setting__desktop_lyric_ellipsis')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.ellipsis': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_zoom\" :model-value=\"appSetting['desktopLyric.style.isZoomActiveLrc']\" :label=\"$t('desktop_lyric__lrc_active_zoom_on')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.isZoomActiveLrc': $event })\")\n  //- .gap-top\n    base-checkbox(id=\"setting_desktop_lyric_fontWeight\" :model-value=\"appSetting['desktopLyric.style.fontWeight']\" @update:model-value=\"updateSetting({ 'desktopLyric.style.fontWeight': $event })\" :label=\"$t('setting__desktop_lyric_font_weight')\")\n\ndd\n  h3#setting__desktop_lyric_font_weight {{ $t('setting__desktop_lyric_font_weight') }}\n  div\n    base-checkbox.gap-left(id=\"setting_setting__desktop_lyric_font_weight_font\" :model-value=\"appSetting['desktopLyric.style.isFontWeightFont']\" :label=\"$t('setting__desktop_lyric_font_weight_font')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.isFontWeightFont': $event })\")\n    base-checkbox.gap-left(id=\"setting_setting__desktop_lyric_font_weight_line\" :model-value=\"appSetting['desktopLyric.style.isFontWeightLine']\" :label=\"$t('setting__desktop_lyric_font_weight_line')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.isFontWeightLine': $event })\")\n    base-checkbox.gap-left(id=\"setting_setting__desktop_lyric_font_weight_extended\" :model-value=\"appSetting['desktopLyric.style.isFontWeightExtended']\" :label=\"$t('setting__desktop_lyric_font_weight_extended')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.isFontWeightExtended': $event })\")\n\n\ndd\n  h3#desktop_lyric_direction {{ $t('setting__desktop_lyric_direction') }}\n  div\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_direction_horizontal\" :model-value=\"appSetting['desktopLyric.direction']\" need value=\"horizontal\" :label=\"$t('setting__desktop_lyric_direction_horizontal')\" @update:model-value=\"updateSetting({ 'desktopLyric.direction': $event })\")\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_direction_vertical\" :model-value=\"appSetting['desktopLyric.direction']\" need value=\"vertical\" :label=\"$t('setting__desktop_lyric_direction_vertical')\" @update:model-value=\"updateSetting({ 'desktopLyric.direction': $event })\")\n\ndd\n  h3#desktop_lyric_scroll_align {{ $t('setting__desktop_lyric_scroll_align') }}\n  div\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_scroll_align_top\" :model-value=\"appSetting['desktopLyric.scrollAlign']\" need value=\"top\" :label=\"$t('setting__desktop_lyric_scroll_align_top')\" @update:model-value=\"updateSetting({ 'desktopLyric.scrollAlign': $event })\")\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_scroll_align_center\" :model-value=\"appSetting['desktopLyric.scrollAlign']\" need value=\"center\" :label=\"$t('setting__desktop_lyric_scroll_align_center')\" @update:model-value=\"updateSetting({ 'desktopLyric.scrollAlign': $event })\")\n\ndd\n  h3#desktop_lyric_align {{ $t('setting__desktop_lyric_align') }}\n  div\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_align_left\" :model-value=\"appSetting['desktopLyric.style.align']\" need value=\"left\" :label=\"$t('setting__desktop_lyric_align_left')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.align': $event })\")\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_align_center\" :model-value=\"appSetting['desktopLyric.style.align']\" need value=\"center\" :label=\"$t('setting__desktop_lyric_align_center')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.align': $event })\")\n    base-checkbox.gap-left(id=\"setting_desktop_lyric_align_right\" :model-value=\"appSetting['desktopLyric.style.align']\" need value=\"right\" :label=\"$t('setting__desktop_lyric_align_right')\" @update:model-value=\"updateSetting({ 'desktopLyric.style.align': $event })\")\n\ndd\n  h3#desktop_lyric_line_gap {{ $t('setting__desktop_lyric_line_gap', { num: appSetting['desktopLyric.style.lineGap'] }) }}\n  div\n    .p\n      base-btn.btn(min @click=\"changeLineGap(-1)\") {{ $t('setting__desktop_lyric_line_gap_dec') }}\n      base-btn.btn(min @click=\"changeLineGap(1)\") {{ $t('setting__desktop_lyric_line_gap_add') }}\ndd\n  h3#desktop_lyric_color {{ $t('setting__desktop_lyric_color') }}\n  div\n    .p.gap-top\n      div(:class=\"$style.groupContent\")\n        div(:class=\"$style.item\")\n          div(ref=\"lyric_unplay_color_ref\" :class=\"$style.color\")\n          div(:class=\"$style.label\") {{ $t('setting__desktop_lyric_unplay_color') }}\n        div(:class=\"$style.item\")\n          div(ref=\"lyric_played_color_ref\" :class=\"$style.color\")\n          div(:class=\"$style.label\") {{ $t('setting__desktop_lyric_played_color') }}\n        div(:class=\"$style.item\")\n          div(ref=\"lyric_shadow_color_ref\" :class=\"$style.color\")\n          div(:class=\"$style.label\") {{ $t('setting__desktop_lyric_shadow_color') }}\n    .p.gap-top\n      base-btn.btn(min @click=\"resetColor\") {{ $t('setting__desktop_lyric_color_reset') }}\ndd\n  h3#desktop_lyric_font {{ $t('setting__desktop_lyric_font') }}\n  div\n    base-selection.gap-teft(:list=\"fontList\" :model-value=\"appSetting['desktopLyric.style.font']\" item-key=\"id\" item-name=\"label\" @update:model-value=\"updateSetting({ 'desktopLyric.style.font': $event })\")\n\ndd\n  h3#desktop_lyric_reset {{ $t('setting__desktop_lyric_reset') }}\n  div\n    .p.gap-top\n      base-btn.btn(min @click=\"resetWindowSetting\") {{ $t('setting__desktop_lyric_reset_window') }}\n\n</template>\n\n<script>\nimport { ref, computed, onMounted, onBeforeUnmount } from '@common/utils/vueTools'\nimport { getSystemFonts } from '@renderer/utils/ipc'\nimport { isLinux } from '@common/utils'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { pickrTools } from '@renderer/utils/pickrTools'\n\nconst defaultUnplayColors = [\n  'rgba(255, 255, 255, 1)',\n  'rgba(255, 236, 144, 1)',\n  'rgba(144, 255, 206, 1)',\n  'rgba(32, 255, 132, 1)',\n  'rgba(255, 226, 32, 1)',\n  'rgba(57, 203, 255, 1)',\n  'rgba(217, 57, 255, 1)',\n  'rgba(255, 57, 71, 1)',\n]\nconst defaultPlayedColors = [\n  'rgba(255, 236, 144, 1)',\n  'rgba(144, 255, 206, 1)',\n  'rgba(32, 255, 132, 1)',\n  'rgba(255, 226, 32, 1)',\n  'rgba(57, 203, 255, 1)',\n  'rgba(7, 197, 86, 1)',\n  'rgba(25, 181, 254, 1)',\n  'rgba(217, 57, 255, 1)',\n  'rgba(255, 57, 71, 1)',\n]\nconst defaultShadowColors = [\n  'rgba(0, 0, 0, 0.15)',\n]\n\nconst useLyricUnplayColor = () => {\n  const lyric_unplay_color_ref = ref(null)\n  let tools\n\n  const initLyricUnplayColor = (color, changed, reset) => {\n    if (!lyric_unplay_color_ref.value) return\n    tools = pickrTools.create(lyric_unplay_color_ref.value, color, defaultUnplayColors, changed, reset)\n  }\n  const destroyLyricUnplayColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setLyricUnplayColor = (color) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    lyric_unplay_color_ref,\n    initLyricUnplayColor,\n    destroyLyricUnplayColor,\n    setLyricUnplayColor,\n  }\n}\n\nconst useLyricPlayedColor = () => {\n  const lyric_played_color_ref = ref(null)\n  let tools\n\n  const initLyricPlayedColor = (color, changed, reset) => {\n    if (!lyric_played_color_ref.value) return\n    tools = pickrTools.create(lyric_played_color_ref.value, color, defaultPlayedColors, changed, reset)\n  }\n  const destroyLyricPlayedColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setLyricPlayedColor = (color) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    lyric_played_color_ref,\n    initLyricPlayedColor,\n    destroyLyricPlayedColor,\n    setLyricPlayedColor,\n  }\n}\n\nconst useLyricShadowColor = () => {\n  const lyric_shadow_color_ref = ref(null)\n  let tools\n\n  const initLyricShadowColor = (color, changed, reset) => {\n    if (!lyric_shadow_color_ref.value) return\n    tools = pickrTools.create(lyric_shadow_color_ref.value, color, defaultShadowColors, changed, reset)\n  }\n  const destroyLyricShadowColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setLyricShadowColor = (color) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    lyric_shadow_color_ref,\n    initLyricShadowColor,\n    destroyLyricShadowColor,\n    setLyricShadowColor,\n  }\n}\n\nconst useLyricColor = () => {\n  const { lyric_unplay_color_ref, initLyricUnplayColor, destroyLyricUnplayColor, setLyricUnplayColor } = useLyricUnplayColor()\n  const { lyric_played_color_ref, initLyricPlayedColor, destroyLyricPlayedColor, setLyricPlayedColor } = useLyricPlayedColor()\n  const { lyric_shadow_color_ref, initLyricShadowColor, destroyLyricShadowColor, setLyricShadowColor } = useLyricShadowColor()\n\n  const initColors = () => {\n    initLyricUnplayColor(appSetting['desktopLyric.style.lyricUnplayColor'], (color) => {\n      updateSetting({ 'desktopLyric.style.lyricUnplayColor': color })\n    })\n    initLyricPlayedColor(appSetting['desktopLyric.style.lyricPlayedColor'], (color) => {\n      updateSetting({ 'desktopLyric.style.lyricPlayedColor': color })\n    })\n    initLyricShadowColor(appSetting['desktopLyric.style.lyricShadowColor'], (color) => {\n      updateSetting({ 'desktopLyric.style.lyricShadowColor': color })\n    })\n  }\n\n  const destroyColors = () => {\n    destroyLyricUnplayColor()\n    destroyLyricPlayedColor()\n    destroyLyricShadowColor()\n  }\n\n  const resetColor = () => {\n    const defaultSetting = {\n      'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',\n      'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',\n      'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.18)',\n    }\n    updateSetting(defaultSetting)\n    setLyricUnplayColor(defaultSetting['desktopLyric.style.lyricUnplayColor'])\n    setLyricPlayedColor(defaultSetting['desktopLyric.style.lyricPlayedColor'])\n    setLyricShadowColor(defaultSetting['desktopLyric.style.lyricShadowColor'])\n  }\n\n  onMounted(() => {\n    initColors()\n  })\n  onBeforeUnmount(() => {\n    destroyColors()\n  })\n\n  return {\n    lyric_unplay_color_ref,\n    lyric_played_color_ref,\n    lyric_shadow_color_ref,\n    resetColor,\n  }\n}\n\nexport default {\n  name: 'SettingDesktopLyric',\n  setup() {\n    const t = useI18n()\n\n    const changeLineGap = (step) => {\n      let gap = appSetting['desktopLyric.style.lineGap'] + step\n      updateSetting({ 'desktopLyric.style.lineGap': Math.min(Math.max(gap, 0), 25) })\n    }\n\n    const {\n      lyric_unplay_color_ref,\n      lyric_played_color_ref,\n      lyric_shadow_color_ref,\n      resetColor,\n    } = useLyricColor()\n\n    const systemFontList = ref([])\n    const fontList = computed(() => {\n      return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]\n    })\n    void getSystemFonts().then(fonts => {\n      systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^\"|\"$)/g, '') }))\n    })\n\n    const resetWindowSetting = () => {\n      updateSetting({\n        'desktopLyric.width': 450,\n        'desktopLyric.height': 300,\n        'desktopLyric.x': null,\n        'desktopLyric.y': null,\n      })\n    }\n\n    return {\n      appSetting,\n      updateSetting,\n      changeLineGap,\n      lyric_unplay_color_ref,\n      lyric_played_color_ref,\n      lyric_shadow_color_ref,\n      resetColor,\n      resetWindowSetting,\n\n      fontList,\n      isLinux,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.groupContent {\n  display: flex;\n  flex-flow: row wrap;\n}\n.item {\n  padding-right: 40px;\n  width: 70px;\n  display: flex;\n  flex-flow: column nowrap;\n  align-items: center;\n}\n.color {\n  width: 80%;\n  aspect-ratio: 1 / 1;\n  background-color: var(--pcr-color);\n  border-radius: @radius-border;\n  cursor: pointer;\n  transition: @transition-fast !important;\n  transition-property: background-color, opacity !important;\n  box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);\n  &:hover {\n    opacity: .7;\n  }\n}\n.label {\n  .mixin-ellipsis-2();\n  padding-top: 10px;\n  text-align: center;\n  line-height: 1.1;\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingDownload.vue",
    "content": "<template lang=\"pug\">\ndt#download {{ $t('setting__download') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_download_enable\" :model-value=\"appSetting['download.enable']\" :label=\"$t('setting__download_enable')\" @update:model-value=\"updateSetting({'download.enable': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_skip_exist_file\" :model-value=\"appSetting['download.skipExistFile']\" :label=\"$t('setting__download_skip_exist_file')\" @update:model-value=\"updateSetting({'download.skipExistFile': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_save_group_list_name\" :model-value=\"appSetting['download.isSavePathGroupByListName']\" :label=\"$t('setting_download_save_group_list_name')\" @update:model-value=\"updateSetting({'download.isSavePathGroupByListName': $event})\")\ndd(:aria-label=\"$t('setting__download_path_title')\")\n  h3#download_path {{ $t('setting__download_path') }}\n  div\n    .p\n      | {{ $t('setting__download_path_label') }}\n      span.auto-hidden.hover(:class=\"$style.savePath\" :aria-label=\"$t('setting__download_path_open_label')\" @click=\"openDirInExplorer(appSetting['download.savePath'])\") {{ appSetting['download.savePath'] }}\n    .p\n      base-btn.btn(min @click=\"handleChangeSavePath\") {{ $t('setting__download_path_change_btn') }}\n\ndd\n  h3#download_max_num\n    | {{ $t('setting__download_max_num') }}\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__download_max_num_tooltip')\")\n  div\n    p\n      base-selection.gap-left(:class=\"$style.selectWidth\" :model-value=\"appSetting['download.maxDownloadNum']\" :list=\"maxNums\" item-key=\"id\" item-name=\"id\" @change=\"handleUpdateMaxNum\")\n\ndd\n  h3#download_use_other_source\n    | {{ $t('setting__download_use_other_source') }}\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__download_use_other_source_tip')\")\n  div\n    base-checkbox(id=\"setting_download_isUseOtherSource\" :model-value=\"appSetting['download.isUseOtherSource']\" :label=\"$t('setting__is_enable')\" @update:model-value=\"updateSetting({'download.isUseOtherSource': $event})\")\n  div\ndd(:aria-label=\"$t('setting__download_name_title')\")\n  h3#download_name {{ $t('setting__download_name') }}\n  div\n    base-checkbox.gap-left(\n        v-for=\"item in musicNames\" :id=\"`setting_download_musicName_${item.value}`\" :key=\"item.value\" name=\"setting_download_musicName\" :value=\"item.value\"\n        need :model-value=\"appSetting['download.fileName']\" :label=\"item.name\" @update:model-value=\"updateSetting({'download.fileName': $event})\")\ndd\n  h3#download_data_embed {{ $t('setting__download_data_embed') }}\n  .gap-top\n    base-checkbox(id=\"setting_download_isEmbedPic\" :model-value=\"appSetting['download.isEmbedPic']\" :label=\"$t('setting__download_embed_pic')\" @update:model-value=\"updateSetting({'download.isEmbedPic': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isEmbedLyric\" :model-value=\"appSetting['download.isEmbedLyric']\" :label=\"$t('setting__download_embed_lyric')\" @update:model-value=\"updateSetting({'download.isEmbedLyric': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isEmbedLyricT\" :disabled=\"!appSetting['download.isEmbedLyric']\" :model-value=\"appSetting['download.isEmbedLyricT']\" :label=\"$t('setting__download_embed_tlyric')\" @update:model-value=\"updateSetting({'download.isEmbedLyricT': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isEmbedLyricR\" :disabled=\"!appSetting['download.isEmbedLyric']\" :model-value=\"appSetting['download.isEmbedLyricR']\" :label=\"$t('setting__download_embed_rlyric')\" @update:model-value=\"updateSetting({'download.isEmbedLyricR': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isEmbedLyricLx\" :disabled=\"!appSetting['download.isEmbedLyric']\" :model-value=\"appSetting['download.isEmbedLyricLx']\" :label=\"$t('setting__download_embed_lxlyric')\" @update:model-value=\"updateSetting({'download.isEmbedLyricLx': $event})\")\ndd(:aria-label=\"$t('setting__download_lyric_title')\")\n  h3#download_lyric {{ $t('setting__download_lyric') }}\n  .gap-top\n    base-checkbox(id=\"setting_download_isDownloadLrc\" :model-value=\"appSetting['download.isDownloadLrc']\" :label=\"$t('setting__is_enable')\" @update:model-value=\"updateSetting({'download.isDownloadLrc': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isDownloadTLrc\" :disabled=\"!appSetting['download.isDownloadLrc']\" :model-value=\"appSetting['download.isDownloadTLrc']\" :label=\"$t('setting__download_tlyric')\" @update:model-value=\"updateSetting({'download.isDownloadTLrc': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isDownloadRLrc\" :disabled=\"!appSetting['download.isDownloadLrc']\" :model-value=\"appSetting['download.isDownloadRLrc']\" :label=\"$t('setting__download_rlyric')\" @update:model-value=\"updateSetting({'download.isDownloadRLrc': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_download_isDownloadLxLrc\" :disabled=\"!appSetting['download.isDownloadLrc']\" :model-value=\"appSetting['download.isDownloadLxLrc']\" :label=\"$t('setting__download_lxlyric')\" @update:model-value=\"updateSetting({'download.isDownloadLxLrc': $event})\")\ndd\n  h3#download_lyric_format\n    | {{ $t('setting__download_lyric_format') }}\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__download_lyric_format_tip')\")\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in lrcFormatList\" :id=\"`setting_download_lrcFormat_${item.id}`\" :key=\"item.id\"\n      name=\"setting_download_lrcFormat\" need :model-value=\"appSetting['download.lrcFormat']\" :value=\"item.id\" :label=\"item.name\"\n      @update:model-value=\"updateSetting({'download.lrcFormat': $event})\")\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\n// import { getSystemFonts } from '@renderer/utils/tools'\nimport { showSelectDialog, openDirInExplorer } from '@renderer/utils/ipc'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nexport default {\n  name: 'SettingDownload',\n  setup() {\n    const t = useI18n()\n\n    const handleChangeSavePath = () => {\n      void showSelectDialog({\n        title: t('setting__download_select_save_path'),\n        defaultPath: appSetting['download.savePath'],\n        properties: ['openDirectory'],\n      }).then(result => {\n        if (result.canceled) return\n        updateSetting({ 'download.savePath': result.filePaths[0] })\n      })\n    }\n\n    const maxNums = new Array(6).fill(null).map((_, i) => ({ id: i + 1 }))\n    const handleUpdateMaxNum = async({ id }) => {\n      if (id > 3) {\n        if (!await dialog.confirm(window.i18n.t('setting__download_max_num_tip'))) return\n      }\n      updateSetting({ 'download.maxDownloadNum': id })\n    }\n\n    const musicNames = computed(() => {\n      return [\n        { value: '歌名 - 歌手', name: t('setting__download_name1') },\n        { value: '歌手 - 歌名', name: t('setting__download_name2') },\n        { value: '歌名', name: t('setting__download_name3') },\n      ]\n    })\n\n    const lrcFormatList = computed(() => {\n      return [\n        { id: 'utf8', name: t('setting__download_lyric_format_utf8') },\n        { id: 'gbk', name: t('setting__download_lyric_format_gbk') },\n      ]\n    })\n\n    return {\n      appSetting,\n      updateSetting,\n      openDirInExplorer,\n      handleChangeSavePath,\n      musicNames,\n      lrcFormatList,\n      maxNums,\n      handleUpdateMaxNum,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n// .savePath {\n//   font-size: 12px;\n// }\n.selectWidth {\n  width: 60px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingHotKey.vue",
    "content": "<template lang=\"pug\">\ndt#hot_key {{ $t('setting__hot_key') }}\ndd\n  h3#hot_key_local_title {{ $t('setting__hot_key_local_title') }}\n  div\n    base-checkbox(id=\"setting_download_hotKeyLocal\" v-model=\"current_hot_key.local.enable\" :label=\"$t('setting__is_enable')\" @change=\"handleHotKeySaveConfig\")\n  div(:class=\"$style.hotKeyContainer\" :style=\"{ opacity: current_hot_key.local.enable ? 1 : .6 }\")\n    div(v-for=\"(item, index) in allHotKeys.local\" :key=\"index\" :class=\"$style.hotKeyItem\")\n      h4(:class=\"$style.hotKeyItemTitle\") {{ $t('setting__hot_key_' + item.name) }}\n      base-input(\n        :class=\"$style.hotKeyItemInput\" readonly :auto-paste=\"false\"\n        :placeholder=\"$t('setting__hot_key_unset_input')\" :value=\"hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)\"\n        @keyup.prevent\n        @focus=\"handleHotKeyFocus($event, item, 'local')\"\n        @blur=\"handleHotKeyBlur($event, item, 'local')\")\ndd\n  h3#hot_key_global_title {{ $t('setting__hot_key_global_title') }}\n  div\n    base-checkbox(id=\"setting_download_hotKeyGlobal\" v-model=\"current_hot_key.global.enable\" :label=\"$t('setting__is_enable')\" @change=\"handleEnableHotKey\")\n  div(:class=\"$style.hotKeyContainer\" :style=\"{ opacity: current_hot_key.global.enable ? 1 : .6 }\")\n    div(v-for=\"(item, index) in allHotKeys.global\" :key=\"index\" :class=\"$style.hotKeyItem\")\n      h4(:class=\"$style.hotKeyItemTitle\") {{ $t('setting__hot_key_' + item.name) }}\n      base-input(\n        :class=\"[$style.hotKeyItemInput, hotKeyConfig.global[item.name] && hotKeyStatus[hotKeyConfig.global[item.name].key] && hotKeyStatus[hotKeyConfig.global[item.name].key].status === false ? $style.hotKeyFailed : null]\"\n        :value=\"hotKeyConfig.global[item.name] && formatHotKeyName(hotKeyConfig.global[item.name].key)\" :auto-paste=\"false\" readonly :placeholder=\"$t('setting__hot_key_unset_input')\" @input.prevent\n        @focus=\"handleHotKeyFocus($event, item, 'global')\"\n        @blur=\"handleHotKeyBlur($event, item, 'global')\")\n</template>\n\n<script>\nimport { ref, onBeforeUnmount, toRaw, shallowReactive } from '@common/utils/vueTools'\nimport { allHotKeys, hotKeySetEnable, hotKeySetConfig, hotKeyGetStatus } from '@renderer/utils/ipc'\nimport { isMac } from '@common/utils'\nimport { useI18n } from '@renderer/plugins/i18n'\n\n\nconst formatHotKeyName = (name) => {\n  if (name.includes('arrow')) {\n    name = name.replace(/arrow(left|right|up|down)/, s => {\n      switch (s) {\n        case 'arrowleft': return '←'\n        case 'arrowright': return '→'\n        case 'arrowup': return '↑'\n        case 'arrowdown': return '↓'\n      }\n    })\n  }\n  if (name.includes('mod')) name = name.replace('mod', isMac ? 'Command' : 'Ctrl')\n  name = name.replace(/(\\+|^)[a-z]/g, l => l.toUpperCase())\n  if (name.length > 1) name = name.replace(/\\+/g, ' + ')\n  return name\n}\n\nexport default {\n  name: 'SettingHotKey',\n  setup() {\n    const t = useI18n()\n    const current_hot_key = ref({\n      local: {\n        enable: false,\n        keys: {},\n      },\n      global: {\n        enable: false,\n        keys: {},\n      },\n    })\n\n    const hotKeyConfig = ref({\n      local: {},\n      global: {},\n    })\n\n    const hotKeyStatus = ref({})\n    let isEditHotKey = false\n    let hotKeyTargetInput\n    let newHotKey\n    let tip\n\n    const initHotKeyConfig = () => {\n      let config = {}\n      for (const [type, typeInfo] of Object.entries(current_hot_key.value)) {\n        let configInfo = config[type] = {}\n        for (const [key, info] of Object.entries(typeInfo.keys)) {\n          if (!info.name) continue\n          configInfo[info.name] = shallowReactive({\n            key,\n            info,\n          })\n        }\n      }\n      hotKeyConfig.value = config\n    }\n\n    const handleHotKeyFocus = (event, info, type) => {\n      setTimeout(async() => {\n        await hotKeySetEnable(false)\n        window.lx.isEditingHotKey = true\n        isEditHotKey = true\n        let config = hotKeyConfig.value[type][info.name]\n        newHotKey = config?.key\n        hotKeyTargetInput = event.target\n        event.target.value = tip = t('setting__hot_key_tip_input')\n      })\n    }\n\n    const handleHotKeyBlur = (event, info, type) => {\n      setTimeout(async() => {\n        await hotKeySetEnable(true)\n        window.lx.isEditingHotKey = false\n        isEditHotKey = false\n        const prevInput = hotKeyTargetInput\n        hotKeyTargetInput = null\n        if (prevInput?.value == tip) {\n          prevInput.value = newHotKey ? formatHotKeyName(newHotKey) : ''\n          return\n        }\n        let config = hotKeyConfig.value[type][info.name]\n        let originKey\n        if (type == 'global' && newHotKey && current_hot_key.value.global.enable) {\n          try {\n            await hotKeySetConfig({\n              action: 'register',\n              data: {\n                key: newHotKey,\n                info,\n              },\n            })\n          } catch (error) {\n            console.log(error)\n            return\n          }\n        }\n        if (config) {\n          if (config.key == newHotKey) return\n          originKey = config.key\n          // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n          delete current_hot_key.value[type].keys[config.key]\n        } else if (!newHotKey) return\n\n        if (newHotKey) {\n          for (const [tempType, tempInfo] of Object.entries(current_hot_key.value)) {\n            if (tempType == type) continue\n            config = tempInfo.keys[newHotKey]\n            if (config) {\n              console.log(newHotKey, info, config, info.name, config.name)\n              // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n              delete current_hot_key.value[tempType].keys[newHotKey]\n              break\n            }\n          }\n          current_hot_key.value[type].keys[newHotKey] = info\n        }\n\n        initHotKeyConfig()\n        // console.log(this.current_hot_key.global.keys)\n        if (originKey && current_hot_key.value.global.enable) {\n          try {\n            await hotKeySetConfig({\n              action: 'unregister',\n              data: originKey,\n            })\n          } catch (error) {\n            console.log(error)\n          }\n        }\n        await handleHotKeySaveConfig()\n        await getHotKeyStatus()\n      })\n    }\n\n    const handleKeyDown = ({ event, keys, key, type }) => {\n      // if (!event || event.repeat) return\n      if (!event || event.repeat || type == 'up' || !isEditHotKey) return\n      event.preventDefault()\n      // console.log(event, key)\n      switch (key) {\n        case 'delete':\n        case 'backspace':\n          key = ''\n          break\n      }\n      hotKeyTargetInput.value = formatHotKeyName(key)\n      // console.log(keys, key, type)\n      newHotKey = key\n    }\n    // const handleUpdateHotKeyConfig = (config) => {\n    //   // console.log(config)\n    //   for (const [type, info] of Object.entries(config)) {\n    //     current_hot_key.value[type] = info\n    //   }\n    // }\n    const handleHotKeySaveConfig = async() => {\n      // console.log(this.current_hot_key)\n      await hotKeySetConfig({\n        action: 'config',\n        data: toRaw(current_hot_key.value),\n      })\n    }\n    const handleEnableHotKey = async() => {\n      await hotKeySetConfig({\n        action: 'enable',\n        data: current_hot_key.value.global.enable,\n      })\n      await handleHotKeySaveConfig()\n      await getHotKeyStatus()\n    }\n    const getHotKeyStatus = async() => {\n      return hotKeyGetStatus().then(status => {\n        // console.log(status)\n        hotKeyStatus.value = status\n        return status\n      })\n    }\n\n    current_hot_key.value = window.lx.appHotKeyConfig\n    initHotKeyConfig()\n    void getHotKeyStatus()\n\n    window.app_event.on('keyDown', handleKeyDown)\n\n    onBeforeUnmount(() => {\n      window.app_event.off('keyDown', handleKeyDown)\n    })\n\n    return {\n      // appSetting,\n      allHotKeys,\n      current_hot_key,\n      hotKeyConfig,\n      hotKeyStatus,\n      handleHotKeyFocus,\n      handleHotKeyBlur,\n      handleEnableHotKey,\n      formatHotKeyName,\n      handleHotKeySaveConfig,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.hotKeyContainer {\n  display: flex;\n  flex-flow: row wrap;\n  // margin-top: -15px;\n  margin-bottom: 15px;\n  transition: opacity @transition-normal;\n}\n.hotKeyItem {\n  width: 30%;\n  padding-right: 35px;\n  margin-top: 15px;\n  box-sizing: border-box;\n}\n.hotKeyItemTitle {\n  .mixin-ellipsis-1();\n  padding-bottom: 5px;\n  color: var(--color-font-label);\n  font-size: 12px;\n}\n.hotKeyItemInput {\n  width: 100%;\n  box-sizing: border-box;\n  // font-family: monospace;\n  &:focus {\n    background-color: var(--color-primary-background-active);\n    text-decoration: none;\n  }\n  &::placeholder {\n    color: var(--color-200) !important;\n  }\n}\n.hotKeyFailed {\n  text-decoration: line-through;\n}\n\n// .delLine {\n//   position: relative;\n//   &:before {\n//     display: block;\n//     height: 1px;\n//     position: absolute;\n//     width: 110%;\n//     content: ' ';\n//     left: 0;\n//     background-color: #000;\n//     transform: rotate(-24deg);\n//     transform-origin: 0;\n//     top: 83%;\n//     z-index: 1;\n//   }\n//   &:after {\n//     display: block;\n//     height: 1px;\n//     position: absolute;\n//     width: 110%;\n//     content: ' ';\n//     left: 0;\n//     background-color: #000;\n//     transform: rotate(23deg);\n//     transform-origin: 0px;\n//     top: 2px;\n//     z-index: 1;\n//   }\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingList.vue",
    "content": "<template lang=\"pug\">\ndt#list {{ $t('setting__list') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_list_actionButtonsVisible_enable\" :model-value=\"appSetting['list.actionButtonsVisible']\" :label=\"$t('setting__list_action_btn')\" @update:model-value=\"updateSetting({'list.actionButtonsVisible': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_list_showSource_enable\" :model-value=\"appSetting['list.isShowSource']\" :label=\"$t('setting__list_source')\" @update:model-value=\"updateSetting({'list.isShowSource': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_list_scroll_enable\" :model-value=\"appSetting['list.isSaveScrollLocation']\" :label=\"$t('setting__list_scroll')\" @update:model-value=\"updateSetting({'list.isSaveScrollLocation': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_list_clickAction_enable\" :model-value=\"appSetting['list.isClickPlayList']\" :label=\"$t('setting__list_click_action')\" @update:model-value=\"updateSetting({'list.isClickPlayList': $event})\")\ndd(:aria-label=\"$t('setting__basic_sourcename_title')\")\n  h3#list_addMusicLocationType {{ $t('setting__list_add_music_location_type') }}\n  div\n    base-checkbox.gap-left(\n      id=\"setting_list_add_music_location_type_top\" name=\"setting_list_add_music_location_type\" need\n      :model-value=\"appSetting['list.addMusicLocationType']\" value=\"top\" :label=\"$t('setting__list_add_music_location_type_top')\"\n      @update:model-value=\"updateSetting({'list.addMusicLocationType': $event})\")\n    base-checkbox.gap-left(\n      id=\"setting_list_add_music_location_type_bottom\" name=\"setting_list_add_music_location_type\" need\n      :model-value=\"appSetting['list.addMusicLocationType']\" value=\"bottom\" :label=\"$t('setting__list_add_music_location_type_bottom')\"\n      @update:model-value=\"updateSetting({'list.addMusicLocationType': $event})\")\n\n</template>\n\n<script>\n// import { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingList',\n  setup() {\n    return {\n      appSetting,\n      updateSetting,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingNetwork.vue",
    "content": "<template lang=\"pug\">\ndt#network {{ $t('setting__network') }}\ndd\n  h3#network_proxy_title {{ $t('setting__network_proxy_title') }}\n  div\n    .p\n      base-checkbox(id=\"setting_network_proxy_enable\" :model-value=\"appSetting['network.proxy.enable']\" :label=\"$t('setting__is_enable')\" @update:model-value=\"updateSetting({'network.proxy.enable': $event})\")\n    .p\n      base-input(:model-value=\"appSetting['network.proxy.host']\" :placeholder=\"proxy.envProxy ? proxy.envProxy.host : $t('setting__network_proxy_host')\" @update:model-value=\"setHost\")\n    .p\n      base-input(:model-value=\"appSetting['network.proxy.port']\" :placeholder=\"proxy.envProxy ? proxy.envProxy.port : $t('setting__network_proxy_port')\" @update:model-value=\"setPort\")\n\n</template>\n\n<script>\nimport { onBeforeUnmount } from '@common/utils/vueTools'\nimport { proxy } from '@renderer/store'\nimport { debounce } from '@common/utils'\n\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingNetwork',\n  setup() {\n    const setHost = debounce(host => {\n      updateSetting({ 'network.proxy.host': host.trim() })\n    }, 500)\n    const setPort = debounce(port => {\n      updateSetting({ 'network.proxy.port': port.trim() })\n    }, 500)\n\n    onBeforeUnmount(() => {\n      if (appSetting['network.proxy.enable'] && !appSetting['network.proxy.host']) proxy.enable = false\n    })\n\n    return {\n      appSetting,\n      updateSetting,\n      setHost,\n      setPort,\n      proxy,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingOdc.vue",
    "content": "<template lang=\"pug\">\ndt#odc {{ $t('setting__odc') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_odc_isAutoClearSearchInput\" :model-value=\"appSetting['odc.isAutoClearSearchInput']\" :label=\"$t('setting__odc_clear_search_input')\" @update:model-value=\"updateSetting({'odc.isAutoClearSearchInput': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_odc_isAutoClearSearchList\" :model-value=\"appSetting['odc.isAutoClearSearchList']\" :label=\"$t('setting__odc_clear_search_list')\" @update:model-value=\"updateSetting({'odc.isAutoClearSearchList': $event})\")\n</template>\n\n<script>\n// import { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingOdc',\n  setup() {\n    return {\n      appSetting,\n      updateSetting,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingOpenAPI.vue",
    "content": "<template lang=\"pug\">\ndt#sync {{ $t('setting__open_api') }}\ndd.gap-top\n  div\n    .p\n      base-checkbox(id=\"setting_open_api_enable\" :model-value=\"appSetting['openAPI.enable']\" :label=\"$t('setting__open_api_enable')\" @update:model-value=\"updateSetting({ 'openAPI.enable': $event })\")\n    .p.gap-top\n      base-checkbox(id=\"setting_open_api_bind_lan\" :model-value=\"appSetting['openAPI.bindLan']\" :label=\"$t('setting__open_api_bind_lan')\" @update:model-value=\"updateSetting({ 'openAPI.bindLan': $event })\")\n    .p.gap-top.small\n      | {{ $t('setting__open_api_address') }}\n      span.select {{ openAPI.address }}\n    .p.small(v-if=\"openAPI.message\") {{ openAPI.message }}\n    .p\n      .p.small {{ $t('setting__open_api_port') }}\n      div\n        base-input.gap-left(:class=\"$style.portInput\" :model-value=\"appSetting['openAPI.port']\" type=\"number\" :placeholder=\"$t('setting__open_api_port_tip')\" @update:model-value=\"setPort\")\n\ndd.gap-top\n  div\n    .p\n      | {{ $t('setting__open_api_tip') }}\n      strong.hover.underline(aria-label=\"https://lyswhut.github.io/lx-music-doc/desktop/faq/open-api\" @click=\"openUrl('https://lyswhut.github.io/lx-music-doc/desktop/open-api')\") {{ $t('setting__open_api_tip_link') }}\n</template>\n\n<script>\n// import { computed } from '@common/utils/vueTools'\nimport { openAPI } from '@renderer/store'\nimport { openUrl } from '@common/utils/electron'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { debounce } from '@common/utils'\n\nexport default {\n  name: 'SettingOpenAPI',\n  setup() {\n    const setPort = debounce(port => {\n      updateSetting({ 'openAPI.port': port.trim() })\n    }, 500)\n\n    return {\n      appSetting,\n      updateSetting,\n      openAPI,\n      openUrl,\n      setPort,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.portInput[disabled], .hostInput[disabled] {\n  opacity: .8 !important;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingOther.vue",
    "content": "<template lang=\"pug\">\ndt#other {{ $t('setting__other') }}\ndd\n  div\n    .gap-top\n      base-checkbox(id=\"setting_transparent_window\" :model-value=\"appSetting['common.transparentWindow']\" :label=\"$t('setting__other_transparent_window')\" @update:model-value=\"updateSetting({'common.transparentWindow': $event})\")\n      svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__other_transparent_window_tip')\")\n\ndd\n  h3#other_tray_theme {{ $t('setting__other_tray_theme') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in trayThemeList\" :id=\"'setting_tray_theme_' + item.id\" :key=\"item.id\" :model-value=\"appSetting['tray.themeId']\" name=\"setting_tray_theme\"\n      need :label=\"item.label\" :value=\"item.id\" @update:model-value=\"updateSetting({'tray.themeId': $event})\")\ndd\n  h3#other_resource_cache\n    | {{ $t('setting__other_resource_cache') }}\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__other_resource_cache_tip')\")\n  div\n    .p\n      | {{ $t('setting__other_resource_cache_label') }}\n      span.auto-hidden {{ cacheSize }}\n    .p\n      base-btn.btn(min :disabled=\"isDisabledResourceCacheClear\" @click=\"clearResourceCache\") {{ $t('setting__other_resource_cache_clear_btn') }}\n\ndd\n  h3#other_other_source {{ $t('setting__other_other_cache') }}\n  div\n    .p\n      | {{ $t('setting__other_other_source_label') }}\n      span.auto-hidden {{ otherSourceCount }}\n    .p\n      | {{ $t('setting__other_music_url_label') }}\n      span.auto-hidden {{ musicUrlCount }}\n    .p\n      | {{ $t('setting__other_lyric_raw_label') }}\n      span.auto-hidden {{ lyricRawCount }}\n    .p\n      base-btn.btn(min :disabled=\"isDisabledOtherSourceCacheClear\" @click=\"handleClearOtherSourceCache\") {{ $t('setting__other_other_source_clear_btn') }}\n      base-btn.btn(min :disabled=\"isDisabledMusicUrlCacheClear\" @click=\"handleClearMusicUrlCache\") {{ $t('setting__other_music_url_clear_btn') }}\n      base-btn.btn(min :disabled=\"isDisabledLyricRawCacheClear\" @click=\"handleClearLyricRawCache\") {{ $t('setting__other_lyric_raw_clear_btn') }}\n\ndd\n  h3#other_lyric_edited {{ $t('setting__other_dislike_list') }}\n  div\n    .p\n      | {{ $t('setting__other_dislike_list_label') }}\n      span.auto-hidden {{ dislikeRuleCount }}\n    .p\n      base-btn.btn(min @click=\"isShowDislikeList = true\") {{ $t('setting__other_dislike_list_show_btn') }}\n  DislikeListModal(v-model=\"isShowDislikeList\")\n\ndd\n  h3#other_lyric_edited {{ $t('setting__other_lyric_edited_cache') }}\n  div\n    .p\n      | {{ $t('setting__other_lyric_edited_label') }}\n      span.auto-hidden {{ lyricEditedCount }}\n    .p\n      base-btn.btn(min :disabled=\"isDisabledLyricEditedCacheClear\" @click=\"handleClearLyricEditedCache\") {{ $t('setting__other_lyric_edited_clear_btn') }}\n\ndd\n  h3#other_lyric_edited {{ $t('setting__other_listdata') }}\n  div\n    .p\n      base-btn.btn(min @click=\"handleClearListData\") {{ $t('setting__other_listdata_clear_btn') }}\n\n</template>\n\n<script>\nimport { ref, computed } from '@common/utils/vueTools'\nimport {\n  clearCache, getCacheSize,\n  getOtherSourceCount, clearOtherSource,\n  getMusicUrlCount, clearMusicUrl,\n  getLyricRawCount, clearLyricRaw,\n  getLyricEditedCount, clearLyricEdited,\n} from '@renderer/utils/ipc'\nimport { sizeFormate } from '@common/utils/common'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { overwriteListFull } from '@renderer/store/list/listManage'\nimport { dislikeRuleCount } from '@renderer/store/dislikeList'\nimport DislikeListModal from './DislikeListModal.vue'\nimport { TRAY_AUTO_ID } from '@common/constants'\n\nexport default {\n  name: 'SettingOther',\n  components: {\n    DislikeListModal,\n  },\n  setup() {\n    const t = useI18n()\n\n    const trayThemeList = computed(() => {\n      return [\n        { id: 0, name: 'native', label: t('setting__other_tray_theme_native') },\n        { id: 2, name: 'black', label: t('setting__other_tray_theme_black') },\n        { id: 1, name: 'origin', label: t('setting__other_tray_theme_origin') },\n        { id: TRAY_AUTO_ID, name: 'auto', label: t('setting__other_tray_theme_auto') },\n      ]\n    })\n\n    const cacheSize = ref('0 B')\n    const isDisabledResourceCacheClear = ref(false)\n    // const isDisabledListCacheClear = ref(false)\n    const refreshCacheSize = () => {\n      void getCacheSize().then(size => {\n        cacheSize.value = sizeFormate(size)\n      })\n    }\n    const clearResourceCache = async() => {\n      if (!await dialog.confirm({\n        message: t('setting__other_resource_cache_tip_confirm'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('setting__other_resource_cache_confirm'),\n      })) return\n      isDisabledResourceCacheClear.value = true\n      void clearCache().then(() => {\n        refreshCacheSize()\n        isDisabledResourceCacheClear.value = false\n      })\n    }\n    refreshCacheSize()\n\n\n    const otherSourceCount = ref(0)\n    const isDisabledOtherSourceCacheClear = ref(false)\n    const refreshOtherSourceCount = () => {\n      void getOtherSourceCount().then(count => {\n        otherSourceCount.value = count\n      })\n    }\n    const handleClearOtherSourceCache = async() => {\n      isDisabledOtherSourceCacheClear.value = true\n      void clearOtherSource().then(() => {\n        refreshOtherSourceCount()\n        isDisabledOtherSourceCacheClear.value = false\n      })\n    }\n    refreshOtherSourceCount()\n\n\n    const musicUrlCount = ref(0)\n    const isDisabledMusicUrlCacheClear = ref(false)\n    const refreshMusicUrlCount = () => {\n      void getMusicUrlCount().then(count => {\n        musicUrlCount.value = count\n      })\n    }\n    const handleClearMusicUrlCache = async() => {\n      isDisabledMusicUrlCacheClear.value = true\n      void clearMusicUrl().then(() => {\n        refreshMusicUrlCount()\n        isDisabledMusicUrlCacheClear.value = false\n      })\n    }\n    refreshMusicUrlCount()\n\n    const isShowDislikeList = ref(false)\n\n    const lyricRawCount = ref(0)\n    const isDisabledLyricRawCacheClear = ref(false)\n    const refreshLyricRawCount = () => {\n      void getLyricRawCount().then(count => {\n        lyricRawCount.value = count\n      })\n    }\n    const handleClearLyricRawCache = async() => {\n      isDisabledLyricRawCacheClear.value = true\n      void clearLyricRaw().then(() => {\n        refreshLyricRawCount()\n        isDisabledLyricRawCacheClear.value = false\n      })\n    }\n    refreshLyricRawCount()\n\n\n    const lyricEditedCount = ref(0)\n    const isDisabledLyricEditedCacheClear = ref(false)\n    const refreshLyricEditedCount = () => {\n      void getLyricEditedCount().then(count => {\n        lyricEditedCount.value = count\n      })\n    }\n    const handleClearLyricEditedCache = async() => {\n      if (!await dialog.confirm({\n        message: t('setting__other_lyric_edited_clear_tip_confirm'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('setting__other_resource_cache_confirm'),\n      })) return\n      isDisabledLyricEditedCacheClear.value = true\n      void clearLyricEdited().then(() => {\n        refreshLyricEditedCount()\n        isDisabledLyricEditedCacheClear.value = false\n      })\n    }\n    refreshLyricEditedCount()\n\n    const handleClearListData = async() => {\n      if (!await dialog.confirm({\n        message: t('setting__other_listdata_clear_tip_confirm'),\n        cancelButtonText: t('cancel_button_text'),\n        confirmButtonText: t('setting__other_resource_cache_confirm'),\n      })) return\n      void overwriteListFull({\n        defaultList: [],\n        loveList: [],\n        userList: [],\n        tempList: [],\n      })\n    }\n\n    return {\n      appSetting,\n      updateSetting,\n      trayThemeList,\n      cacheSize,\n      isDisabledResourceCacheClear,\n      clearResourceCache,\n\n      otherSourceCount,\n      isDisabledOtherSourceCacheClear,\n      handleClearOtherSourceCache,\n\n      musicUrlCount,\n      isDisabledMusicUrlCacheClear,\n      handleClearMusicUrlCache,\n\n      dislikeRuleCount,\n      isShowDislikeList,\n\n      lyricRawCount,\n      isDisabledLyricRawCacheClear,\n      handleClearLyricRawCache,\n\n      lyricEditedCount,\n      isDisabledLyricEditedCacheClear,\n      handleClearLyricEditedCache,\n\n      handleClearListData,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingPlay.vue",
    "content": "<template lang=\"pug\">\ndt#play {{ $t('setting__play') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_player_startup_auto_play\" :model-value=\"appSetting['player.startupAutoPlay']\" :label=\"$t('setting__play_startup_auto_play')\" @update:model-value=\"updateSetting({'player.startupAutoPlay': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_power_save_blocker\" :model-value=\"appSetting['player.powerSaveBlocker']\" :label=\"$t('setting__play_power_save_blocker')\" @update:model-value=\"handleUpdatePowerSaveBlocker\")\n  .gap-top\n    base-checkbox(id=\"setting_player_save_play_time\" :model-value=\"appSetting['player.isSavePlayTime']\" :label=\"$t('setting__play_save_play_time')\" @update:model-value=\"updateSetting({'player.isSavePlayTime': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_auto_clean_played_list\" :model-value=\"appSetting['player.isAutoCleanPlayedList']\" :label=\"$t('setting__play_auto_clean_played_list')\" @update:model-value=\"updateSetting({'player.isAutoCleanPlayedList': $event})\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__play_auto_clean_played_list_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_player_lyric_transition\" :model-value=\"appSetting['player.isShowLyricTranslation']\" :label=\"$t('setting__play_lyric_transition')\" @update:model-value=\"updateSetting({'player.isShowLyricTranslation': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_lyric_roma\" :model-value=\"appSetting['player.isShowLyricRoma']\" :label=\"$t('setting__play_lyric_roma')\" @update:model-value=\"updateSetting({'player.isShowLyricRoma': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_awap_lyric_trans_roma\" :model-value=\"appSetting['player.isSwapLyricTranslationAndRoma']\" :label=\"$t('setting__player_swap_lyric_trans_roma')\" @update:model-value=\"updateSetting({'player.isSwapLyricTranslationAndRoma': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_auto_skip_on_error\" :model-value=\"appSetting['player.autoSkipOnError']\" :label=\"$t('setting__play_auto_skip_on_error')\" @update:model-value=\"updateSetting({'player.autoSkipOnError': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_lyric_s2t\" :model-value=\"appSetting['player.isS2t']\" :label=\"$t('setting__play_lyric_s2t')\" @update:model-value=\"updateSetting({'player.isS2t': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_player_lyric_play_lxlrc\" :model-value=\"appSetting['player.isPlayLxlrc']\" :label=\"$t('setting__play_lyric_lxlrc')\" @update:model-value=\"updateSetting({'player.isPlayLxlrc': $event})\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__play_lyric_lxlrc_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_player_showTaskProgess\" :model-value=\"appSetting['player.isShowTaskProgess']\" :label=\"$t('setting__play_task_bar')\" @update:model-value=\"updateSetting({'player.isShowTaskProgess': $event})\")\n  .gap-top(v-if=\"isMac\")\n    base-checkbox(id=\"setting_player_showStatusBarLyric\" :model-value=\"appSetting['player.isShowStatusBarLyric']\" :label=\"$t('setting__play_statusbar_lyric')\" @update:model-value=\"updateSetting({'player.isShowStatusBarLyric': $event})\")\n    svg-icon(class=\"help-icon\" name=\"help-circle-outline\" :aria-label=\"$t('setting__play_statusbar_lyric_tip')\")\n  .gap-top\n    base-checkbox(id=\"setting_player_isMaxOutputChannelCount\" :model-value=\"isMaxOutputChannelCount\" :label=\"$t('setting__play_max_output_channel_count')\" @update:model-value=\"handleUpdateMaxOutputChannelCount\")\n  .gap-top\n    base-checkbox(id=\"setting_player_isMediaDeviceRemovedStopPlay\" :model-value=\"appSetting['player.isMediaDeviceRemovedStopPlay']\" :label=\"$t('setting__play_mediaDevice_remove_stop_play')\" @update:model-value=\"updateSetting({'player.isMediaDeviceRemovedStopPlay': $event})\")\n\ndd\n  h3#basic_play_quality {{ $t('setting__play_playQuality') }}\n  div\n    base-checkbox.gap-left(\n      v-for=\"item in playQualityList\" :id=\"`setting_play_quality_${item}`\" :key=\"item\"\n      name=\"setting_play_quality\" need :model-value=\"appSetting['player.playQuality']\" :value=\"item\" :label=\"item\"\n      @update:model-value=\"updateSetting({'player.playQuality': $event})\")\n\ndd(:aria-label=\"$t('setting__play_mediaDevice_title')\")\n  h3#play_mediaDevice {{ $t('setting__play_mediaDevice') }}\n  div\n    base-selection.gap-left(v-model=\"mediaDeviceId\" :list=\"mediaDevices\" item-key=\"deviceId\" item-name=\"label\" @change=\"handleMediaDeviceIdChnage\")\n</template>\n\n<script>\nimport { ref, onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { hasInitedAdvancedAudioFeatures, setMediaDeviceId } from '@renderer/plugins/player'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, saveMediaDeviceId, updateSetting } from '@renderer/store/setting'\nimport { setPowerSaveBlocker } from '@renderer/core/player/utils'\nimport { isPlay } from '@renderer/store/player/state'\nimport { TRY_QUALITYS_LIST } from '@renderer/core/music/utils'\nimport { isMac } from '@common/utils'\n\n\nexport default {\n  name: 'SettingPlay',\n  setup() {\n    const t = useI18n()\n    const playQualityList = [...TRY_QUALITYS_LIST, '128k'].reverse()\n\n    const mediaDevices = ref([])\n    const getMediaDevice = async() => {\n      const devices = await navigator.mediaDevices.enumerateDevices()\n      let audioDevices = devices.filter(device => device.kind === 'audiooutput')\n      mediaDevices.value = audioDevices\n      // console.log(this.mediaDevices)\n    }\n    void getMediaDevice()\n\n    navigator.mediaDevices.addEventListener('devicechange', getMediaDevice)\n    onBeforeUnmount(() => {\n      navigator.mediaDevices.removeEventListener('devicechange', getMediaDevice)\n    })\n\n    const mediaDeviceId = ref(appSetting['player.mediaDeviceId'])\n    const handleMediaDeviceIdChnage = async() => {\n      if (hasInitedAdvancedAudioFeatures()) {\n        await dialog({\n          message: t('setting__play_media_device_error_tip'),\n          confirmButtonText: t('alert_button_text'),\n        })\n        mediaDeviceId.value = appSetting['player.mediaDeviceId']\n      } else if (appSetting['player.audioVisualization']) {\n        const confirm = await dialog.confirm({\n          message: t('setting__play_media_device_tip'),\n          cancelButtonText: t('cancel_button_text'),\n          confirmButtonText: t('confirm_button_text'),\n        })\n        if (confirm) {\n          updateSetting({\n            'player.audioVisualization': false,\n            'player.mediaDeviceId': mediaDeviceId.value,\n          })\n        } else {\n          mediaDeviceId.value = appSetting['player.mediaDeviceId']\n        }\n      } else {\n        appSetting['player.mediaDeviceId'] = mediaDeviceId.value\n      }\n    }\n    watch(() => appSetting['player.mediaDeviceId'], val => {\n      mediaDeviceId.value = val\n    })\n\n    const handleUpdatePowerSaveBlocker = (enabled) => {\n      if (enabled) {\n        if (isPlay.value) setPowerSaveBlocker(true, true)\n      } else {\n        setPowerSaveBlocker(false, true)\n      }\n      updateSetting({ 'player.powerSaveBlocker': enabled })\n    }\n\n    const isMaxOutputChannelCount = ref(appSetting['player.isMaxOutputChannelCount'])\n    const handleUpdateMaxOutputChannelCount = async(enabled) => {\n      isMaxOutputChannelCount.value = enabled\n      if (appSetting['player.mediaDeviceId'] != 'default') {\n        const confirm = await dialog.confirm({\n          message: t('setting__play_advanced_audio_features_tip'),\n          cancelButtonText: t('cancel_button_text'),\n          confirmButtonText: t('confirm_button_text'),\n        })\n        if (!confirm) {\n          isMaxOutputChannelCount.value = false\n          return\n        }\n        await setMediaDeviceId('default').catch(_ => _)\n        saveMediaDeviceId('default')\n      }\n      updateSetting({ 'player.isMaxOutputChannelCount': enabled })\n    }\n\n\n    return {\n      appSetting,\n      updateSetting,\n      mediaDevices,\n      mediaDeviceId,\n      handleMediaDeviceIdChnage,\n      handleUpdatePowerSaveBlocker,\n      isMaxOutputChannelCount,\n      handleUpdateMaxOutputChannelCount,\n      playQualityList,\n      isMac,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingPlayDetail.vue",
    "content": "<template lang=\"pug\">\ndt#play_detail {{ $t('setting__play_detail') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_play_detail_font_zoom_enable\" :model-value=\"appSetting['playDetail.isZoomActiveLrc']\" :label=\"$t('setting__play_detail_font_zoom')\" @update:model-value=\"updateSetting({'playDetail.isZoomActiveLrc': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_play_detail_lyric_delayScroll\" :model-value=\"appSetting['playDetail.isDelayScroll']\" :label=\"$t('setting__play_detail_lyric_delay_scroll')\" @update:model-value=\"updateSetting({ 'playDetail.isDelayScroll': $event })\")\n  .gap-top\n    base-checkbox(id=\"setting_play_detail_lyric_progress_enable\" :model-value=\"appSetting['playDetail.isShowLyricProgressSetting']\" :label=\"$t('setting__play_detail_lyric_progress')\" @update:model-value=\"updateSetting({'playDetail.isShowLyricProgressSetting': $event})\")\n\ndd\n  h3#play_detail_align {{ $t('setting__play_detail_align') }}\n  div\n    base-checkbox.gap-left(id=\"setting_play_detail_align_left\" :model-value=\"appSetting['playDetail.style.align']\" need value=\"left\" :label=\"$t('setting__play_detail_align_left')\" @update:model-value=\"updateSetting({ 'playDetail.style.align': $event })\")\n    base-checkbox.gap-left(id=\"setting_play_detail_align_center\" :model-value=\"appSetting['playDetail.style.align']\" need value=\"center\" :label=\"$t('setting__play_detail_align_center')\" @update:model-value=\"updateSetting({ 'playDetail.style.align': $event })\")\n    base-checkbox.gap-left(id=\"setting_play_detail_align_right\" :model-value=\"appSetting['playDetail.style.align']\" need value=\"right\" :label=\"$t('setting__play_detail_align_right')\" @update:model-value=\"updateSetting({ 'playDetail.style.align': $event })\")\n\n</template>\n\n<script>\n// import { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingPlayDetail',\n  setup() {\n    return {\n      appSetting,\n      updateSetting,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingSearch.vue",
    "content": "<template lang=\"pug\">\ndt#search {{ $t('setting__search') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting_search_showHot_enable\" :model-value=\"appSetting['search.isShowHotSearch']\" :label=\"$t('setting__search_hot')\" @update:model-value=\"updateSetting({'search.isShowHotSearch': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_search_showHistory_enable\" :model-value=\"appSetting['search.isShowHistorySearch']\" :label=\"$t('setting__search_history')\" @update:model-value=\"updateSetting({'search.isShowHistorySearch': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting_search_focusSearchBox_enable\" :model-value=\"appSetting['search.isFocusSearchBox']\" :label=\"$t('setting__search_focus_search_box')\" @update:model-value=\"updateSetting({'search.isFocusSearchBox': $event})\")\n\n</template>\n\n<script>\n// import { ref, onBeforeUnmount } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingSearch',\n  setup() {\n    return {\n      appSetting,\n      updateSetting,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingSync/ServerDeviceListModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"modelValue\" bg-close teleport=\"#view\" @close=\"$emit('update:modelValue', false)\")\n  main(:class=\"$style.main\")\n    h2 {{ $t('setting__sync_server_device_list_title') }}\n    ul.scroll(v-if=\"historyDeviceList.length\" :class=\"$style.content\")\n      li(v-for=\"(device, index) in historyDeviceList\" :key=\"device.id\" :class=\"$style.listItem\")\n        div(:class=\"$style.listLeft\")\n          span(:class=\"$style.name\")\n            svg-icon(v-if=\"device.isMobile\" name=\"phone\" style=\"margin-right: 0.2rem; vertical-align: -0.2em;\")\n            | {{ device.name }}\n          span(:class=\"$style.desc\") {{ $t('setting__sync_server_device_list_time', { time: device.lastConnectDate }) }}\n        base-btn(:class=\"$style.listBtn\" outline :aria-label=\"$t('setting__sync_server_device_list_btn_remove')\" @click.stop=\"handleRemove(index)\")\n          svg(v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\")\n            use(xlink:href=\"#icon-delete\")\n    div(v-else :class=\"$style.content\")\n      div(:class=\"$style.noitem\") {{ $t('setting__sync_server_device_list_noitem') }}\n  div(:class=\"$style.footer\")\n    div(:class=\"$style.tips\") {{ $t('setting__sync_server_device_list_tips') }}\n</template>\n\n<script>\nimport { watch, ref } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { getSyncServerDevices, removeSyncServerDevice } from '@renderer/utils/ipc'\nimport { dateFormat } from '@common/utils/common'\n\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue'],\n  setup(props) {\n    const historyDeviceList = ref([])\n\n    const getList = () => {\n      void getSyncServerDevices().then((list) => {\n        historyDeviceList.value = list.map(d => {\n          return {\n            id: d.clientId,\n            name: d.deviceName,\n            lastConnectDate: d.lastConnectDate ? dateFormat(d.lastConnectDate) : '-',\n            isMobile: d.isMobile,\n          }\n        })\n      })\n    }\n\n    watch(() => sync.server.status.devices.length, () => {\n      if (!props.modelValue) return\n      getList()\n    })\n    watch(() => props.modelValue, (val) => {\n      if (!val) return\n      getList()\n    })\n\n    const handleRemove = (index) => {\n      void removeSyncServerDevice(historyDeviceList.value[index].id).then(getList)\n    }\n\n    return {\n      historyDeviceList,\n      handleRemove,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  // padding: 15px;\n  // max-width: 400px;\n  min-width: 460px;\n  min-height: 200px;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  // min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    margin: 15px;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n\n.name {\n  color: var(--color-font);\n  font-size: 14px;\n  word-break: break-all;\n  line-height: 1.2;\n}\n\n.desc {\n  color: var(--color-font-label);\n  margin-top: 8px;\n  font-size: 12px;\n  word-break: break-all;\n}\n\n.content {\n  flex: auto;\n  min-height: 100px;\n  max-height: 100%;\n}\n.listItem {\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  transition: background-color 0.2s ease;\n  padding: 10px;\n  // border-radius: @radius-border;\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n}\n.noitem {\n  height: 100px;\n  font-size: 18px;\n  color: var(--color-font-label);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.listLeft {\n  flex: auto;\n  min-width: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n}\n.listBtn {\n  flex: none;\n  height: 30px;\n  width: 30px;\n  padding: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  svg {\n    width: 60%;\n  }\n}\n\n// .footer {\n//   width: @width;\n// }\n.tips {\n  padding: 8px 15px;\n  font-size: 13px;\n  line-height: 1.25;\n  color: var(--color-font);\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingSync/SyncClient.vue",
    "content": "<template lang=\"pug\">\ndd\n  h3 {{ $t('setting__sync_client_mode') }}\n  div\n    .p.small {{ $t('setting__sync_client_status', { status: clientStatus }) }}\n    .p.small {{ $t('setting__sync_client_address', { address: sync.client.status.address.join(', ') || '' }) }}\n    .p\n      .p.small {{ $t('setting__sync_client_host') }}\n      div\n        base-input.gap-left(:class=\"$style.hostInput\" :model-value=\"appSetting['sync.client.host']\" :disabled=\"sync.enable\" :placeholder=\"$t('setting__sync_client_host_tip')\" @update:model-value=\"setSyncClientHost\")\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { debounce } from '@common/utils/common'\nimport { SYNC_CODE } from '@common/constants_sync'\n\nexport default {\n  name: 'SettingSyncClient',\n  setup() {\n    const t = useI18n()\n\n    const clientStatus = computed(() => {\n      let status\n      switch (sync.client.status.message) {\n        case SYNC_CODE.msgBlockedIp:\n          status = t('setting__sync_code_blocked_ip')\n          break\n        case SYNC_CODE.authFailed:\n          status = t('setting__sync_code_fail')\n          break\n        default:\n          status = sync.client.status.message\n            ? sync.client.status.message\n            : sync.client.status.status\n              ? t('setting_sync_status_enabled')\n              : t('sync_status_disabled')\n          break\n      }\n      return status\n    })\n\n    const setSyncClientHost = debounce(host => {\n      updateSetting({ 'sync.client.host': host.trim() })\n    }, 500)\n\n\n    return {\n      appSetting,\n      sync,\n      setSyncClientHost,\n      clientStatus,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n.hostInput[disabled] {\n  opacity: .8 !important;\n}\n\n.hostInput {\n  min-width: 380px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingSync/SyncServer.vue",
    "content": "<template lang=\"pug\">\ndd\n  h3 {{ syncEnableServerTitle }}\n  div\n    .p.small {{ $t('setting__sync_server_auth_code', { code: sync.server.status.code || '' }) }}\n    .p.small {{ $t('setting__sync_server_address', { address: sync.server.status.address.join(', ') || '' }) }}\n    .p.small {{ $t('setting__sync_server_device', { devices: syncDevices }) }}\n    .p.gap-top\n      .p.small {{ $t('setting__sync_server_port') }}\n      div\n        base-input.gap-left(:class=\"$style.portInput\" :model-value=\"appSetting['sync.server.port']\" :disabled=\"sync.enable\" type=\"number\" :placeholder=\"$t('setting__sync_server_port_tip')\" @update:model-value=\"setSyncServerPort\")\n\n    .p.gap-top\n      base-btn.btn(min :disabled=\"!sync.server.status.status\" @click=\"refreshSyncCode\") {{ $t('setting__sync_server_refresh_code') }}\n      base-btn.btn(min @click=\"isShowDeviceListModal = true\") {{ $t('setting__sync_server_show_device_list') }}\n  ServerDeviceListModal(v-model=\"isShowDeviceListModal\")\n</template>\n\n<script>\nimport { computed, ref } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { sendSyncAction } from '@renderer/utils/ipc'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { debounce } from '@common/utils/common'\nimport ServerDeviceListModal from './ServerDeviceListModal.vue'\n\nexport default {\n  name: 'SettingSyncServer',\n  components: {\n    ServerDeviceListModal,\n  },\n  setup() {\n    const t = useI18n()\n    const isShowDeviceListModal = ref(false)\n\n    const syncEnableServerTitle = computed(() => {\n      let title = t('setting__sync_server_mode')\n      if (sync.server.status.message) {\n        title += ` [${sync.server.status.message}]`\n      }\n      // else if (this.sync.server.status.address.length) {\n      //   // title += ` [${this.sync.server.status.address.join(', ')}]`\n      // }\n      return title\n    })\n\n    const syncDevices = computed(() => {\n      return sync.server.status.devices.length\n        ? sync.server.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')\n        : ''\n    })\n\n    const refreshSyncCode = () => {\n      void sendSyncAction({ action: 'generate_code' })\n    }\n\n    const setSyncServerPort = debounce(port => {\n      updateSetting({ 'sync.server.port': port.trim() })\n    }, 500)\n\n    return {\n      appSetting,\n      sync,\n      syncEnableServerTitle,\n      setSyncServerPort,\n      syncDevices,\n      refreshSyncCode,\n      isShowDeviceListModal,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.portInput[disabled], .hostInput[disabled] {\n  opacity: .8 !important;\n}\n\n.hostInput {\n  min-width: 380px;\n}\n\n\n.list {\n  // background-color: @color-search-form-background;\n  font-size: 13px;\n  transition-property: height;\n  position: relative;\n  .listItem {\n    position: relative;\n    padding: 15px 10px 15px 15px;\n    transition: .3s ease;\n    transition-property: background-color, opacity;\n    line-height: 1.3;\n    // overflow: hidden;\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n\n    &:hover {\n      background-color: var(--color-primary-background-hover);\n    }\n    // border-radius: 4px;\n    // &:last-child {\n    //   border-bottom-left-radius: 4px;\n    //   border-bottom-right-radius: 4px;\n    // }\n    &.fetching {\n      opacity: .5;\n    }\n  }\n}\n\n.listLeft {\n  flex: auto;\n  min-width: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n}\n\n.text {\n  flex: auto;\n  margin-bottom: 2px;\n  .mixin-ellipsis-1();\n}\n\n.label {\n  flex: none;\n  font-size: 12px;\n  opacity: 0.5;\n  // padding: 0 10px;\n  // display: flex;\n  // align-items: center;\n  // transform: rotate(45deg);\n  // background-color:\n}\n.btns {\n  flex: none;\n  font-size: 12px;\n  padding: 0 5px;\n  display: flex;\n  align-items: center;\n}\n.btn {\n  background-color: transparent;\n  border: none;\n  border-radius: @form-radius;\n  margin-right: 5px;\n  cursor: pointer;\n  padding: 4px 7px;\n  color: var(--color-button-font);\n  outline: none;\n  transition: background-color 0.2s ease;\n  line-height: 0;\n  &:last-child {\n    margin-right: 0;\n  }\n\n  svg {\n    height: 22px;\n    width: 22px;\n  }\n\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &:active {\n    background-color: var(--color-primary-font-active);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingSync/index.vue",
    "content": "<template lang=\"pug\">\ndt#sync\n  | {{ $t('setting__sync') }}\n  button(class=\"help-btn\" :aria-label=\"$t('setting__sync_tip')\" @click=\"openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')\")\n    svg-icon(name=\"help-circle-outline\")\ndd\n  base-checkbox(id=\"setting_sync_enable\" :model-value=\"appSetting['sync.enable']\" :label=\"$t('setting__sync_enable')\" @update:model-value=\"updateSetting({ 'sync.enable': $event })\")\n\ndd\n  h3#sync_mode {{ $t('setting__sync_mode') }}\n  div\n    base-checkbox.gap-left(id=\"setting_sync_mode_server\" :disabled=\"sync.enable\" :model-value=\"appSetting['sync.mode']\" need value=\"server\" :label=\"$t('setting__sync_mode_server')\" @update:model-value=\"updateSetting({ 'sync.mode': $event })\")\n    base-checkbox.gap-left(id=\"setting_sync_mode_client\" :disabled=\"sync.enable\" :model-value=\"appSetting['sync.mode']\" need value=\"client\" :label=\"$t('setting__sync_mode_client')\" @update:model-value=\"updateSetting({ 'sync.mode': $event })\")\n\n\nSyncClient(v-if=\"sync.mode == 'client'\")\nSyncServer(v-else)\n\n</template>\n\n<script>\n// import { computed } from '@common/utils/vueTools'\nimport { sync } from '@renderer/store'\nimport { openUrl } from '@common/utils/electron'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport SyncServer from './SyncServer.vue'\nimport SyncClient from './SyncClient.vue'\n\nexport default {\n  name: 'SettingSync',\n  components: {\n    SyncServer,\n    SyncClient,\n  },\n  setup() {\n    return {\n      appSetting,\n      updateSetting,\n      sync,\n      openUrl,\n    }\n  },\n}\n</script>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/SettingUpdate.vue",
    "content": "<template lang=\"pug\">\ndt#update {{ $t('setting__update') }}\ndd\n  .gap-top\n    base-checkbox(id=\"setting__update_tryAutoUpdate\" :model-value=\"appSetting['common.tryAutoUpdate']\" :label=\"$t('setting__update_try_auto_update')\" @update:model-value=\"updateSetting({'common.tryAutoUpdate': $event})\")\n  .gap-top\n    base-checkbox(id=\"setting__update_showChangeLog\" :model-value=\"appSetting['common.showChangeLog']\" :label=\"$t('setting__update_show_change_log')\" @update:model-value=\"updateSetting({'common.showChangeLog': $event})\")\n  .gap-top\n    .gap-top\n      .p.small(@click=\"handleOpenDevTools\") {{ $t('setting__update_current_label') }}{{ versionInfo.version }}\n      .p.small(v-if=\"commit_id\")\n        | {{ $t('setting__update_commit_id') }}\n        span.select {{ commit_id }}\n      .p.small(v-if=\"commit_date\") {{ $t('setting__update_commit_date') }}{{ commit_date }}\n\n    .p.small.gap-top\n      | {{ $t('setting__update_latest_label') }}{{ versionInfo.newVersion && versionInfo.newVersion.version != '0.0.0' ? versionInfo.newVersion.version : $t('setting__update_unknown') }}\n    .p.small(v-if=\"downloadProgress\" style=\"line-height: 1.5;\")\n      | {{ $t('setting__update_downloading') }}\n      br\n      | {{ $t('setting__update_progress') }}{{ downloadProgress }}\n    template(v-if=\"versionInfo.newVersion\")\n      .p(v-if=\"versionInfo.isLatest\")\n        span {{ $t('setting__update_latest') }}\n      .p(v-else-if=\"versionInfo.isUnknown\")\n        span {{ $t('setting__update_unknown_tip') }}\n      .p(v-else-if=\"versionInfo.status != 'downloading'\")\n        span {{ $t('setting__update_new_version') }}\n      .p\n        base-btn.btn.gap-left(min @click=\"showUpdateModal\") {{ $t('setting__update_open_version_modal_btn') }}\n    .p.small(v-else-if=\"versionInfo.status =='checking'\") {{ $t('setting__update_checking') }}\n</template>\n\n<script>\nimport { computed } from '@common/utils/vueTools'\nimport { versionInfo } from '@renderer/store'\nimport { dateFormat, sizeFormate } from '@common/utils/common'\n// import { openDirInExplorer, selectDir } from '@renderer/utils'\nimport { openDevTools } from '@renderer/utils/ipc'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\n\nexport default {\n  name: 'SettingUpdate',\n  setup() {\n    let lastClickTime = 0\n    let clickNum = 0\n    const commit_id = COMMIT_ID\n    const commit_date = dateFormat(COMMIT_DATE)\n\n    const t = useI18n()\n\n    const handleOpenDevTools = () => {\n      if (window.performance.now() - lastClickTime > 1000) {\n        if (clickNum > 0) clickNum = 0\n      } else {\n        if (clickNum > 4) {\n          openDevTools()\n          clickNum = 0\n          return\n        }\n      }\n      clickNum++\n      lastClickTime = window.performance.now()\n    }\n\n    const downloadProgress = computed(() => {\n      return versionInfo.status == 'downloading'\n        ? versionInfo.downloadProgress\n          ? `${versionInfo.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(versionInfo.downloadProgress.transferred)}/${sizeFormate(versionInfo.downloadProgress.total)} - ${sizeFormate(versionInfo.downloadProgress.bytesPerSecond)}/s`\n          : t('setting__update_init')\n        : ''\n    })\n\n    const showUpdateModal = () => {\n      versionInfo.showModal = true\n    }\n\n    return {\n      versionInfo,\n      downloadProgress,\n      handleOpenDevTools,\n      showUpdateModal,\n      appSetting,\n      updateSetting,\n      commit_id,\n      commit_date,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n// .savePath {\n//   font-size: 12px;\n// }\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/index.vue",
    "content": "<template>\n  <material-modal :show=\"modelValue\" max-height=\"90%\" teleport=\"#view\" @close=\"handleCancel\">\n    <main :class=\"$style.main\">\n      <h2>{{ themeId ? $t('theme_edit_modal__title_edit') : $t('theme_edit_modal__title_add') }}</h2>\n      <div class=\"scroll\" :class=\"$style.content\">\n        <div :class=\"[$style.group, $style.base]\">\n          <div :class=\"$style.groupContent\">\n            <div :class=\"$style.item\">\n              <div ref=\"primary_color_ref\" :class=\"$style.color\" />\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__primary') }}</div>\n            </div>\n            <div :class=\"$style.item\">\n              <div ref=\"font_color_ref\" :class=\"$style.color\" />\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__font') }}</div>\n            </div>\n            <div :class=\"$style.item\">\n              <div ref=\"app_bg_color_ref\" :class=\"$style.color\" />\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__app_bg') }}</div>\n            </div>\n            <div :class=\"$style.item\">\n              <div ref=\"aside_font_color_ref\" :class=\"$style.color\" />\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__aside_color') }}</div>\n            </div>\n            <div :class=\"$style.item\">\n              <div ref=\"main_bg_color_ref\" :class=\"$style.color\" />\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__main_bg') }}</div>\n            </div>\n            <div :class=\"[$style.item, $style.bg]\">\n              <div :class=\"[$style.bgImg, {[$style.hasBg]: !!bgImg}]\" @click=\"selectBgImg\">\n                <img v-if=\"bgImg\" loading=\"lazy\" decoding=\"async\" :class=\"$style.img\" :src=\"bgImg\" alt=\"Background Image\">\n                <svg-icon v-else :class=\"$style.icon\" name=\"plus\" />\n                <button :class=\"$style.removeBtn\" type=\"button\" @click.stop=\"removeBgImg\">\n                  <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\">\n                    <use xlink:href=\"#icon-delete\" />\n                  </svg>\n                </button>\n              </div>\n              <div :class=\"$style.label\">{{ $t('theme_edit_modal__bg_image') }}</div>\n            </div>\n          </div>\n        </div>\n        <div :class=\"$style.groupContent\">\n          <div :class=\"$style.group\">\n            <div :class=\"$style.groupTitle\">\n              <span :class=\"$style.title\">{{ $t('theme_edit_modal__badge') }}</span>\n              <span class=\"badge badge-theme-primary\">{{ $t('tag__lossless') }}</span>\n              <span class=\"badge badge-theme-secondary\">{{ $t('tag__high_quality') }}</span>\n              <span class=\"badge badge-theme-tertiary\">kw</span>\n            </div>\n            <div :class=\"$style.groupContent\">\n              <div :class=\"$style.item\">\n                <div ref=\"badge_primary_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__badge_primary') }}</div>\n              </div>\n              <div :class=\"$style.item\">\n                <div ref=\"badge_secondary_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__badge_secondary') }}</div>\n              </div>\n              <div :class=\"$style.item\">\n                <div ref=\"badge_tertiary_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__badge_tertiary') }}</div>\n              </div>\n            </div>\n          </div>\n          <div :class=\"$style.group\">\n            <div :class=\"$style.groupTitle\">\n              <span>{{ $t('theme_edit_modal__control_btn') }}</span>\n              <div :class=\"$style.controlBtn\">\n                <button type=\"button\" :class=\"$style.hide\">\n                  <svg :class=\"$style.controlBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"80%\" viewBox=\"0 0 30.727 30.727\" space=\"preserve\">\n                    <use xlink:href=\"#icon-window-hide\" />\n                  </svg>\n                </button>\n                <button type=\"button\" :class=\"$style.min\">\n                  <svg :class=\"$style.controlBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n                    <use xlink:href=\"#icon-window-minimize\" />\n                  </svg>\n                </button>\n                <button type=\"button\" :class=\"$style.close\">\n                  <svg :class=\"$style.controlBtnIcon\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" width=\"100%\" viewBox=\"0 0 24 24\" space=\"preserve\">\n                    <use xlink:href=\"#icon-window-close\" />\n                  </svg>\n                </button>\n              </div>\n            </div>\n            <div :class=\"$style.groupContent\">\n              <div :class=\"$style.item\">\n                <div ref=\"close_btn_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__close_btn') }}</div>\n              </div>\n              <div :class=\"$style.item\">\n                <div ref=\"min_btn_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__min_btn') }}</div>\n              </div>\n              <div :class=\"$style.item\">\n                <div ref=\"hide_btn_color_ref\" :class=\"$style.color\" />\n                <div :class=\"$style.label\">{{ $t('theme_edit_modal__hide_btn') }}</div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div :class=\"$style.footer\">\n        <div :class=\"$style.subContent\" style=\"flex-wrap: wrap;\">\n          <base-input v-model=\"themeName\" :class=\"$style.input\" :placeholder=\"$t('theme_selector_modal__theme_name')\" />\n          <div :class=\"$style.subContent\" style=\"flex-wrap: wrap;\">\n            <base-checkbox id=\"theme_edit_modal__dark\" v-model=\"isDark\" :class=\"$style.checkbox\" :label=\"$t('theme_edit_modal__dark')\" @change=\"handleDark\" />\n            <div :class=\"$style.subContent\" style=\"flex-wrap: wrap;\">\n              <base-checkbox id=\"theme_edit_modal__dark_font\" v-model=\"isDarkFont\" :class=\"$style.checkbox\" :label=\"$t('theme_edit_modal__dark_font')\" @change=\"handleDarkFont\" />\n              <base-checkbox id=\"theme_edit_modal__preview\" v-model=\"preview\" :class=\"$style.checkbox\" :label=\"$t('theme_edit_modal__preview')\" @change=\"handlePreview\" />\n            </div>\n          </div>\n        </div>\n        <div :class=\"$style.subContent\" style=\"flex: none;\">\n          <base-btn v-if=\"themeId\" :class=\"$style.btn\" @click=\"handleRemove\">{{ $t('theme_edit_modal__remove') }}</base-btn>\n          <base-btn v-if=\"themeId\" :class=\"$style.btn\" @click=\"handleSaveNew\">{{ $t('theme_edit_modal__save_new') }}</base-btn>\n          <!-- <base-btn :class=\"$style.btn\" @click=\"handleCancel\">{{ $t('btn_cancel') }}</base-btn> -->\n          <base-btn :class=\"$style.btn\" @click=\"handleSubmit\">{{ $t('btn_save') }}</base-btn>\n        </div>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { joinPath, extname, copyFile, checkPath, createDir, removeFile, moveFile, basename } from '@common/utils/nodejs'\nimport { nextTick, ref, watch } from '@common/utils/vueTools'\nimport { applyTheme, buildThemeColors, getThemes, copyTheme } from '@renderer/store/utils'\nimport { isUrl, encodePath } from '@common/utils/common'\n// import { appSetting, updateSetting } from '@renderer/store/setting'\n// import { applyTheme, getThemes } from '@renderer/store/utils'\nimport { createThemeColors } from '@common/theme/utils'\nimport useMainColor from './useMainColor'\nimport useFontColor from './useFontColor'\nimport useAppBgColor from './useAppBgColor'\nimport useMainBgColor from './useMainBgColor'\nimport useAsideFontColor from './useAsideFontColor'\nimport useBadgePrimaryColor from './useBadgePrimaryColor'\nimport useBadgeSecondaryColor from './useBadgeSecondaryColor'\nimport useBadgeTertiaryColor from './useBadgeTertiaryColor'\nimport useCloseBtnColor from './useCloseBtnColor'\nimport useMinBtnColor from './useMinBtnColor'\nimport useHideBtnColor from './useHideBtnColor'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { removeTheme, saveTheme, showSelectDialog } from '@renderer/utils/ipc'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { themeInfo } from '@renderer/store'\n\n\nexport default {\n  name: 'ThemeSelectorModal',\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n    themeId: {\n      type: String,\n      default: '',\n    },\n  },\n  emits: ['update:modelValue', 'submit'],\n  setup(props, { emit }) {\n    const themeName = ref('')\n    const isDark = ref(false)\n    const isDarkFont = ref(false)\n    const preview = ref(false)\n    const bgImg = ref('')\n    let bgImgRaw = ''\n    let originBgName = ''\n    let currentBgPath = ''\n\n    let theme\n\n    const getColor = (color, theme) => {\n      return color.startsWith('var')\n        ? theme.config.themeColors[color.replace(/var\\((.+)\\)/, '$1')]\n        : color\n    }\n\n    const createPreview = () => {\n      if (!preview.value) return\n      window.setTheme(buildThemeColors(theme, themeInfo.dataPath))\n    }\n\n    // '--color-app-background': string\n    // '--color-main-background': string\n    // '--color-nav-font': string\n    // '--background-image': string\n    // '--background-image-position': string\n    // '--background-image-size': string\n\n    // // 关闭按钮颜色\n    // '--color-btn-hide': string\n    // '--color-btn-min': string\n    // '--color-btn-close': string\n\n    // // 徽章颜色\n    // '--color-badge-primary': string\n    // '--color-badge-secondary': string\n    // '--color-badge-tertiary': string\n    const { primary_color_ref, initMainColor, destroyMainColor } = useMainColor()\n    const { font_color_ref, initFontColor, destroyFontColor } = useFontColor()\n    const { app_bg_color_ref, initAppBgColor, destroyAppBgColor, setAppBgColor } = useAppBgColor()\n    const { aside_font_color_ref, initAsideFontColor, destroyAsideFontColor, setAsideFontColor } = useAsideFontColor()\n    const { main_bg_color_ref, initMainBgColor, destroyMainBgColor, setMainBgColor } = useMainBgColor()\n    const { badge_primary_color_ref, initBadgePrimaryColor, destroyBadgePrimaryColor, setBadgePrimaryColor } = useBadgePrimaryColor()\n    const { badge_secondary_color_ref, initBadgeSecondaryColor, destroyBadgeSecondaryColor, setBadgeSecondaryColor } = useBadgeSecondaryColor()\n    const { badge_tertiary_color_ref, initBadgeTertiaryColor, destroyBadgeTertiaryColor, setBadgeTertiaryColor } = useBadgeTertiaryColor()\n    const { close_btn_color_ref, initCloseBtnColor, destroyCloseBtnColor, setCloseBtnColor } = useCloseBtnColor()\n    const { min_btn_color_ref, initMinBtnColor, destroyMinBtnColor, setMinBtnColor } = useMinBtnColor()\n    const { hide_btn_color_ref, initHideBtnColor, destroyHideBtnColor, setHideBtnColor } = useHideBtnColor()\n\n    let appBgColorOrigin\n    let appBgColor\n    let asideFontColorOrigin\n    let asideFontColor\n    let mainBgColorOrigin\n    let mainBgColor\n    let badgePrimaryColorOrigin\n    let badgePrimaryColor\n    let badgeSecondaryColorOrigin\n    let badgeSecondaryColor\n    let badgeTertiaryColorOrigin\n    let badgeTertiaryColor\n    let closeBtnColorOrigin\n    let closeBtnColor\n    let minBtnColorOrigin\n    let minBtnColor\n    let hideBtnColorOrigin\n    let hideBtnColor\n\n    const applyPrimaryColor = (color, fontColor, isDark, isDarkFont) => {\n      theme.config.themeColors = createThemeColors(color, fontColor, isDark, isDarkFont)\n      if (theme.config.extInfo['--color-app-background'].startsWith('var')) setAppBgColor(getColor(appBgColorOrigin, theme))\n      if (theme.config.extInfo['--color-nav-font'].startsWith('var')) setAsideFontColor(getColor(asideFontColorOrigin, theme))\n      if (theme.config.extInfo['--color-main-background'].startsWith('var')) setMainBgColor(getColor(mainBgColorOrigin, theme))\n      if (theme.config.extInfo['--color-badge-primary'].startsWith('var')) setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme))\n      if (theme.config.extInfo['--color-badge-secondary'].startsWith('var')) setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme))\n      if (theme.config.extInfo['--color-badge-tertiary'].startsWith('var')) setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme))\n      if (theme.config.extInfo['--color-btn-close'].startsWith('var')) setCloseBtnColor(getColor(closeBtnColorOrigin, theme))\n      if (theme.config.extInfo['--color-btn-min'].startsWith('var')) setMinBtnColor(getColor(minBtnColorOrigin, theme))\n      if (theme.config.extInfo['--color-btn-hide'].startsWith('var')) setHideBtnColor(getColor(hideBtnColorOrigin, theme))\n\n      createPreview()\n    }\n\n    const initColors = (_theme) => {\n      theme = _theme\n      // console.log(theme)\n      themeName.value = theme.name\n      isDark.value = theme.isDark\n      isDarkFont.value = theme.isDarkFont ?? false\n      currentBgPath = ''\n      if (theme.config.extInfo['--background-image'] == 'none') {\n        bgImg.value = ''\n        bgImgRaw = ''\n        originBgName = ''\n      } else {\n        bgImgRaw = isUrl(theme.config.extInfo['--background-image'])\n          ? theme.config.extInfo['--background-image']\n          : joinPath(themeInfo.dataPath, theme.config.extInfo['--background-image'])\n        bgImg.value = encodePath(bgImgRaw)\n        originBgName = theme.config.extInfo['--background-image']\n      }\n      appBgColorOrigin = theme.config.extInfo['--color-app-background']\n      appBgColor = getColor(appBgColorOrigin, theme)\n      asideFontColorOrigin = theme.config.extInfo['--color-nav-font']\n      asideFontColor = getColor(asideFontColorOrigin, theme)\n      mainBgColorOrigin = theme.config.extInfo['--color-main-background']\n      mainBgColor = getColor(mainBgColorOrigin, theme)\n      badgePrimaryColorOrigin = theme.config.extInfo['--color-badge-primary']\n      badgePrimaryColor = getColor(badgePrimaryColorOrigin, theme)\n      badgeSecondaryColorOrigin = theme.config.extInfo['--color-badge-secondary']\n      badgeSecondaryColor = getColor(badgeSecondaryColorOrigin, theme)\n      badgeTertiaryColorOrigin = theme.config.extInfo['--color-badge-tertiary']\n      badgeTertiaryColor = getColor(badgeTertiaryColorOrigin, theme)\n      closeBtnColorOrigin = theme.config.extInfo['--color-btn-close']\n      closeBtnColor = getColor(closeBtnColorOrigin, theme)\n      minBtnColorOrigin = theme.config.extInfo['--color-btn-min']\n      minBtnColor = getColor(minBtnColorOrigin, theme)\n      hideBtnColorOrigin = theme.config.extInfo['--color-btn-hide']\n      hideBtnColor = getColor(hideBtnColorOrigin, theme)\n\n      initMainColor(theme.config.themeColors['--color-primary'], (color) => {\n        applyPrimaryColor(color, theme.config.themeColors['--color-1000'], theme.isDark, theme.isDarkFont)\n      })\n      initFontColor(theme.config.themeColors['--color-1000'] ?? (isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)'), (color) => {\n        applyPrimaryColor(theme.config.themeColors['--color-primary'], color, theme.isDark, theme.isDarkFont)\n      })\n      initAppBgColor(appBgColor, (color) => {\n        // console.log('appBgColor', color)\n        theme.config.extInfo['--color-app-background'] = color == appBgColor ? appBgColorOrigin : color\n        createPreview()\n      }, () => { setAppBgColor(getColor(appBgColorOrigin, theme)) })\n      initAsideFontColor(asideFontColor, (color) => {\n        theme.config.extInfo['--color-nav-font'] = color == asideFontColor ? asideFontColorOrigin : color\n        createPreview()\n      }, () => { setAsideFontColor(getColor(asideFontColorOrigin, theme)) })\n      initMainBgColor(mainBgColor, (color) => {\n        theme.config.extInfo['--color-main-background'] = color == mainBgColor ? mainBgColorOrigin : color\n        createPreview()\n      }, () => { setMainBgColor(getColor(mainBgColorOrigin, theme)) })\n      initBadgePrimaryColor(badgePrimaryColor, (color) => {\n        theme.config.extInfo['--color-badge-primary'] = color == badgePrimaryColor ? badgePrimaryColorOrigin : color\n        createPreview()\n      }, () => { setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme)) })\n      initBadgeSecondaryColor(badgeSecondaryColor, (color) => {\n        theme.config.extInfo['--color-badge-secondary'] = color == badgeSecondaryColor ? badgeSecondaryColorOrigin : color\n        createPreview()\n      }, () => { setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme)) })\n      initBadgeTertiaryColor(badgeTertiaryColor, (color) => {\n        theme.config.extInfo['--color-badge-tertiary'] = color == badgeTertiaryColor ? badgeTertiaryColorOrigin : color\n        createPreview()\n      }, () => { setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme)) })\n      initCloseBtnColor(closeBtnColor, (color) => {\n        theme.config.extInfo['--color-btn-close'] = color == closeBtnColor ? closeBtnColorOrigin : color\n        createPreview()\n      }, () => { setCloseBtnColor(getColor(closeBtnColorOrigin, theme)) })\n      initMinBtnColor(minBtnColor, (color) => {\n        theme.config.extInfo['--color-btn-min'] = color == minBtnColor ? minBtnColorOrigin : color\n        createPreview()\n      }, () => { setMinBtnColor(getColor(minBtnColorOrigin, theme)) })\n      initHideBtnColor(hideBtnColor, (color) => {\n        theme.config.extInfo['--color-btn-hide'] = color == hideBtnColor ? hideBtnColorOrigin : color\n        createPreview()\n      }, () => { setHideBtnColor(getColor(hideBtnColorOrigin, theme)) })\n\n      createPreview()\n    }\n    const destroyColors = () => {\n      destroyMainColor()\n      destroyFontColor()\n      destroyAppBgColor()\n      destroyAsideFontColor()\n      destroyMainBgColor()\n      destroyBadgePrimaryColor()\n      destroyBadgeSecondaryColor()\n      destroyBadgeTertiaryColor()\n      destroyCloseBtnColor()\n      destroyMinBtnColor()\n      destroyHideBtnColor()\n    }\n\n    watch(() => props.modelValue, (visible) => {\n      void nextTick(() => {\n        getThemes(({ themes, userThemes }) => {\n          if (visible) {\n            if (props.themeId) {\n              const theme = userThemes.find(t => t.id == props.themeId)\n              if (theme) {\n                initColors(copyTheme(theme))\n                return\n              }\n            }\n            const theme = copyTheme(themes[0])\n            theme.id = 'user_theme_' + Date.now()\n            theme.name = ''\n            theme.isCustom = true\n            initColors(theme)\n          } else {\n            destroyColors()\n            // 移除临时保存的背景\n            if (currentBgPath) removeFile(currentBgPath).catch(_ => _)\n          }\n        })\n      })\n    })\n\n    const selectBgImg = async() => {\n      const result = await showSelectDialog({\n        title: window.i18n.t('theme_edit_modal__select_bg_file'),\n        properties: ['openFile'],\n        filters: [\n          {\n            name: 'Image File',\n            extensions: [\n              'jpg', 'jpeg', 'jfif', 'pjpeg',\n              'pjp', 'png', 'apng', 'avif', 'gif', 'svg',\n              'webp', 'bmp'],\n          },\n        ],\n      })\n      if (result.canceled) return\n      const path = result.filePaths[0]\n      const fileName = `${theme.id}_${Date.now()}${extname(path)}`\n      const tempDir = joinPath(themeInfo.dataPath, 'temp')\n      const bgPath = joinPath(tempDir, fileName)\n      if (!await checkPath(tempDir)) await createDir(tempDir)\n      await copyFile(path, bgPath)\n      currentBgPath = bgImgRaw = bgPath\n      bgImg.value = encodePath(bgImgRaw)\n      theme.config.extInfo['--background-image'] = 'temp/' + fileName\n\n      createPreview()\n    }\n    const removeBgImg = async() => {\n      if (currentBgPath) {\n        void removeFile(currentBgPath)\n        currentBgPath = ''\n      }\n      bgImg.value = ''\n      bgImgRaw = ''\n      theme.config.extInfo['--background-image'] = 'none'\n      createPreview()\n    }\n    const handleDark = (val) => {\n      theme.isDark = val\n      applyPrimaryColor(theme.config.themeColors['--color-primary'], theme.config.themeColors['--color-1000'], theme.isDark, theme.isDarkFont)\n    }\n    const handleDarkFont = (val) => {\n      theme.isDarkFont = val\n      applyPrimaryColor(theme.config.themeColors['--color-primary'], theme.config.themeColors['--color-1000'], theme.isDark, theme.isDarkFont)\n    }\n    /**\n     * 预览主题\n     * @param {*} val 是否预览当前编辑的主题\n     */\n    const handlePreview = (val) => {\n      if (val) {\n        createPreview()\n      } else {\n        applyTheme(appSetting['theme.id'], appSetting['theme.lightId'], appSetting['theme.darkId'], themeInfo.dataPath)\n      }\n    }\n    const handleCancel = () => {\n      handlePreview(false)\n      emit('update:modelValue', false)\n    }\n    // 保存\n    const handleSubmit = async() => {\n      if (!themeName.value) return\n      theme.name = themeName.value.substring(0, 20)\n      // 保存新背景\n      if (currentBgPath && !isUrl(currentBgPath)) {\n        const name = basename(currentBgPath)\n        await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))\n        theme.config.extInfo['--background-image'] = name\n      }\n      // 移除旧背景\n      if (originBgName &&\n        theme.config.extInfo['--background-image'] != originBgName &&\n        !isUrl(theme.config.extInfo['--background-image'])) void removeFile(joinPath(themeInfo.dataPath, originBgName))\n      if (props.themeId) {\n        const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)\n        if (index > -1) themeInfo.userThemes.splice(index, 1, theme)\n      } else themeInfo.userThemes.push(theme)\n      handlePreview(false)\n      await saveTheme(theme)\n      emit('submit')\n      emit('update:modelValue', false)\n    }\n    // 删除\n    const handleRemove = async() => {\n      const confirm = await dialog.confirm({\n        message: window.i18n.t('theme_edit_modal__remove_tip'),\n        cancelButtonText: window.i18n.t('cancel_button_text'),\n        confirmButtonText: window.i18n.t('confirm_button_text'),\n      })\n      if (!confirm) return\n      let isRequireUpdateSetting = false\n      const newSetting = {}\n      if (appSetting['theme.id'] == props.themeId) {\n        newSetting['theme.id'] = 'green'\n        isRequireUpdateSetting = true\n      }\n      if (theme.isDark) {\n        if (appSetting['theme.darkId'] == props.themeId) {\n          newSetting['theme.darkId'] = 'black'\n          isRequireUpdateSetting = true\n        }\n      } else {\n        if (appSetting['theme.lightId'] == props.themeId) {\n          newSetting['theme.lightId'] = 'green'\n          isRequireUpdateSetting = true\n        }\n      }\n      if (isRequireUpdateSetting) updateSetting(newSetting)\n      if (originBgName) void removeFile(joinPath(themeInfo.dataPath, originBgName))\n      await removeTheme(props.themeId)\n      const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)\n      console.log(index)\n      if (index > -1) themeInfo.userThemes.splice(index, 1)\n      handlePreview(false)\n      emit('submit')\n      emit('update:modelValue', false)\n    }\n    // 另存为\n    const handleSaveNew = async() => {\n      if (!themeName.value) return\n      theme.name = themeName.value.substring(0, 20)\n      theme.id = 'user_theme_' + Date.now()\n      // 保存新背景\n      if (!isUrl(currentBgPath)) {\n        if (currentBgPath) {\n          const name = basename(currentBgPath)\n          await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))\n          theme.config.extInfo['--background-image'] = name\n        } else if (bgImgRaw) {\n          const fileName = `${theme.id}_${Date.now()}${extname(bgImgRaw)}`\n          await copyFile(bgImgRaw, joinPath(themeInfo.dataPath, fileName))\n          theme.config.extInfo['--background-image'] = fileName\n        }\n      }\n      themeInfo.userThemes.push(theme)\n      handlePreview(false)\n      await saveTheme(theme)\n      emit('submit')\n      emit('update:modelValue', false)\n    }\n\n    return {\n      themeName,\n      bgImg,\n      isDark,\n      handleDark,\n      isDarkFont,\n      handleDarkFont,\n      preview,\n      handlePreview,\n      handleCancel,\n      handleSubmit,\n      handleRemove,\n      handleSaveNew,\n      selectBgImg,\n      removeBgImg,\n\n      primary_color_ref,\n      font_color_ref,\n      app_bg_color_ref,\n      main_bg_color_ref,\n      aside_font_color_ref,\n      badge_primary_color_ref,\n      badge_secondary_color_ref,\n      badge_tertiary_color_ref,\n      close_btn_color_ref,\n      min_btn_color_ref,\n      hide_btn_color_ref,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.main {\n  // padding: 15px;\n  // max-width: 400px;\n  min-width: 300px;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n  h2 {\n    flex: none;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    padding: 15px;\n  }\n  h3 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    padding-bottom: 15px;\n    font-size: 15px;\n  }\n}\n.content {\n  flex: auto;\n  // padding: 15px 0;\n  font-size: 14px;\n  gap: 5px;\n  display: flex;\n  flex-flow: column nowrap;\n}\n.group {\n\n}\n.groupTitle {\n  padding: 20px 20px 0;\n  display: flex;\n  flex-flow: row wrap;\n\n  .title {\n    margin-right: 5px;\n  }\n}\n.groupContent {\n  display: flex;\n  flex-flow: row wrap;\n}\n.item {\n  padding: 15px 20px 0;\n  width: 74px;\n  display: flex;\n  flex-flow: column nowrap;\n  align-items: center;\n}\n.base {\n  .color {\n    width: 100%;\n  }\n}\n.color {\n  width: 80%;\n  aspect-ratio: 1 / 1;\n  background-color: var(--pcr-color);\n  border-radius: @radius-border;\n  cursor: pointer;\n  transition: @transition-fast !important;\n  transition-property: background-color, opacity !important;\n  box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);\n  &:hover {\n    opacity: .7;\n  }\n}\n.label {\n  padding-top: 10px;\n  text-align: center;\n}\n\n.bg {\n  // width: 0;\n  // flex: 1 1 auto;\n  width: 150px;\n  // min-width: 60px;\n  // max-width: 200px;\n}\n.bgImg {\n  width: 100%;\n  height: 60px;\n  border: 1Px dashed var(--color-primary-light-100-alpha-300);\n  transition: .3s ease;\n  transition-property: border, color;\n  box-sizing: border-box;\n  border: 1px dashed var(--color-primary-light-100-alpha-300);\n  color: var(--color-primary-light-100-alpha-300);\n  position: relative;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  transition: @transition-fast !important;\n  transition-property: background-color, opacity !important;\n  overflow: hidden;\n  &:hover {\n    opacity: .7;\n  }\n\n  &.hasBg {\n    border: none;\n\n    .removeBtn {\n      display: block;\n    }\n  }\n\n  .img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n  }\n  .removeBtn {\n    position: absolute;\n    right: 0;\n    top: 0;\n    border: none;\n    cursor: pointer;\n    padding: 6px 8px;\n    background-color: rgba(0, 0, 0, 0.6);\n    color: rgba(255, 255, 255, 0.9);\n    outline: none;\n    transition: background-color 0.2s ease;\n    line-height: 0;\n    display: none;\n\n    svg {\n      height: .7em;\n    }\n\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.7);\n    }\n    &:active {\n      background-color: rgba(0, 0, 0, 0.8);\n    }\n  }\n  .icon {\n    // position: absolute;\n    // font-size: 16px;\n    width: auto;\n    height: 66%;\n  }\n}\n\n@control-btn-width: @height-toolbar * .26;\n.controlBtn {\n  display: flex;\n  -webkit-app-region: no-drag;\n  align-items: center;\n  padding: 0 @control-btn-width;\n  flex-direction: row-reverse;\n  // height: @height-toolbar * .7;\n  transition: opacity @transition-normal;\n  opacity: .5;\n  &:hover {\n    opacity: .8;\n    .controlBtnIcon {\n      opacity: 1;\n    }\n  }\n\n  button {\n    display: flex;\n    position: relative;\n    background: none;\n    border: none;\n    outline: none;\n    padding: 1px;\n    cursor: pointer;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    width: @control-btn-width;\n    height: @control-btn-width;\n    border-radius: 50%;\n    color: var(--color-font);\n    + button {\n      margin-right: (@control-btn-width / 2);\n    }\n\n    &.hide {\n      background-color: var(--color-btn-hide);\n    }\n    &.min, &.fullscreenExit {\n      background-color: var(--color-btn-min);\n    }\n    // &.max {\n    //   background-color: var(--color-btn-max);\n    // }\n    &.close {\n      background-color: var(--color-btn-close);\n    }\n  }\n}\n\n.controlBtnIcon {\n  opacity: 0;\n  transition: opacity 0.2s ease-in-out;\n}\n\n.note {\n  padding: 8px 15px;\n  font-size: 13px;\n  line-height: 1.25;\n  color: var(--color-font);\n  // p {\n  //   + p {\n  //     margin-top: 5px;\n  //   }\n  // }\n}\n.footer {\n  padding: 15px;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  justify-content: space-between;\n  gap: 15px;\n  font-size: 14px;\n  .subContent {\n    display: flex;\n    flex-flow: row nowrap;\n    align-items: center;\n    gap: 10px;\n  }\n\n  .checkbox {\n    flex: none;\n  }\n  .input {\n    max-width: 150px;\n    flex: 0 1 auto;\n  }\n}\n\n\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useAppBgColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const app_bg_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initAppBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!app_bg_color_ref.value) return\n    tools = pickrTools.create(app_bg_color_ref.value, color, [\n      'rgba(255, 255, 255, 0)',\n      'rgba(255, 255, 255, 0.15)',\n      'rgba(21.34, 18.92, 44.61, 0.81)',\n    ], changed, reset)\n  }\n  const destroyAppBgColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setAppBgColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    app_bg_color_ref,\n    initAppBgColor,\n    destroyAppBgColor,\n    setAppBgColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useAsideFontColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const aside_font_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initAsideFontColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!aside_font_color_ref.value) return\n    tools = pickrTools.create(aside_font_color_ref.value, color, null, changed, reset)\n  }\n  const destroyAsideFontColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setAsideFontColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    aside_font_color_ref,\n    initAsideFontColor,\n    destroyAsideFontColor,\n    setAsideFontColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useBadgePrimaryColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const badge_primary_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initBadgePrimaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!badge_primary_color_ref.value) return\n    tools = pickrTools.create(badge_primary_color_ref.value, color, null, changed, reset)\n  }\n  const destroyBadgePrimaryColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setBadgePrimaryColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    badge_primary_color_ref,\n    initBadgePrimaryColor,\n    destroyBadgePrimaryColor,\n    setBadgePrimaryColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useBadgeSecondaryColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const badge_secondary_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initBadgeSecondaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!badge_secondary_color_ref.value) return\n    tools = pickrTools.create(badge_secondary_color_ref.value, color, [\n      'rgba(75, 174, 213, 1)',\n      'rgba(92, 191, 155, 1)',\n      'rgba(66, 6, 150, 7, 171, 1)',\n      'rgba(158, 212, 88, 1)',\n      'rgba(223, 187, 107, 1)',\n      'rgba(245, 182, 132, 1)',\n      'rgba(229, 163, 159, 1)',\n      'rgba(177, 155, 159, 1)',\n      'rgba(99, 118, 162, 1)',\n      'rgba(176, 128, 219, 1)',\n      'rgba(175, 148, 121, 1)',\n      'rgba(223, 187, 107, 1)',\n      'rgba(53.08, 107.67, 129.18, 1)',\n    ], changed, reset)\n  }\n  const destroyBadgeSecondaryColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setBadgeSecondaryColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    badge_secondary_color_ref,\n    initBadgeSecondaryColor,\n    destroyBadgeSecondaryColor,\n    setBadgeSecondaryColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useBadgeTertiaryColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const badge_tertiary_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initBadgeTertiaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!badge_tertiary_color_ref.value) return\n    tools = pickrTools.create(badge_tertiary_color_ref.value, color, [\n      'rgba(231, 170, 54, 1)',\n      'rgba(92, 191, 155, 1)',\n      'rgba(54, 196, 231, 1)',\n      'rgba(158, 212, 88, 1)',\n      'rgba(223, 187, 107, 1)',\n      'rgba(245, 182, 132, 1)',\n      'rgba(229, 163, 159, 1)',\n      'rgba(177, 155, 159, 1)',\n      'rgba(99, 118, 162, 1)',\n      'rgba(176, 128, 219, 1)',\n      'rgba(175, 148, 121, 1)',\n      'rgba(223, 187, 107, 1)',\n      'rgba(49.11, 161.05, 135.64, 1)',\n    ], changed, reset)\n  }\n  const destroyBadgeTertiaryColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setBadgeTertiaryColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    badge_tertiary_color_ref,\n    initBadgeTertiaryColor,\n    destroyBadgeTertiaryColor,\n    setBadgeTertiaryColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useCloseBtnColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const close_btn_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initCloseBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!close_btn_color_ref.value) return\n    tools = pickrTools.create(close_btn_color_ref.value, color, [\n      'rgba(59, 194, 178, 1)',\n      'rgba(133, 196, 59, 1)',\n      'rgba(250, 180, 160, 1)',\n      'rgba(104.51, 72.55, 107.98, 1)',\n    ], changed, reset)\n  }\n  const destroyCloseBtnColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setCloseBtnColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    close_btn_color_ref,\n    initCloseBtnColor,\n    destroyCloseBtnColor,\n    setCloseBtnColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useFontColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const font_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initFontColor = (color: string, changed: (color: string) => void) => {\n    if (!font_color_ref.value) return\n    tools = pickrTools.create(font_color_ref.value, color, [\n      'rgba(33, 33, 33, 1)',\n      'rgba(229, 229, 229, 1)',\n    ], changed, () => {})\n  }\n  const destroyFontColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n\n  return {\n    font_color_ref,\n    initFontColor,\n    destroyFontColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useHideBtnColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const hide_btn_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initHideBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!hide_btn_color_ref.value) return\n    tools = pickrTools.create(hide_btn_color_ref.value, color, [\n      'rgba(59, 194, 178, 1)',\n      'rgba(133, 196, 59, 1)',\n      'rgba(250, 180, 160, 1)',\n      'rgba(77.46, 103.73, 151.81, 1)',\n    ], changed, reset)\n  }\n  const destroyHideBtnColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setHideBtnColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    hide_btn_color_ref,\n    initHideBtnColor,\n    destroyHideBtnColor,\n    setHideBtnColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useMainBgColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const main_bg_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initMainBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!main_bg_color_ref.value) return\n    tools = pickrTools.create(main_bg_color_ref.value, color, [\n      'rgba(255, 255, 255, 1)',\n      'rgba(19, 19, 19, 0.9)',\n      'rgba(255, 255, 255, 0.9)',\n      'rgba(255, 255, 255, 0.8)',\n      'rgba(25.82, 23.65, 46.6, 0.54)',\n    ], changed, reset)\n  }\n  const destroyMainBgColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setMainBgColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    main_bg_color_ref,\n    initMainBgColor,\n    destroyMainBgColor,\n    setMainBgColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useMainColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const primary_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initMainColor = (color: string, changed: (color: string) => void) => {\n    if (!primary_color_ref.value) return\n    tools = pickrTools.create(primary_color_ref.value, color, [\n      'rgba(77, 175, 124, 1)',\n      'rgba(52, 152, 219, 1)',\n      'rgba(77, 131, 175, 1)',\n      'rgba(245, 171, 53, 1)',\n      'rgba(214, 69, 65, 1)',\n      'rgba(241, 130, 141, 1)',\n      'rgba(155, 89, 182, 1)',\n      'rgba(108, 122, 137, 1)',\n      'rgba(51, 110, 123, 1)',\n      'rgba(79, 98, 208, 1)',\n      'rgba(150, 150, 150, 1)',\n      'rgba(74, 55, 82, 1)',\n      'rgba(87, 144, 167, 1)',\n      'rgba(192, 57, 43, 1)',\n      'rgba(113.52, 107.21, 166.13, 1)',\n    ], changed, () => {})\n  }\n  const destroyMainColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n\n  return {\n    primary_color_ref,\n    initMainColor,\n    destroyMainColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeEditModal/useMinBtnColor.ts",
    "content": "import { ref } from '@common/utils/vueTools'\nimport { pickrTools, type PickrTools } from '@renderer/utils/pickrTools'\n\nexport default () => {\n  const min_btn_color_ref = ref(null)\n  let tools: PickrTools | null\n\n  const initMinBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {\n    if (!min_btn_color_ref.value) return\n    tools = pickrTools.create(min_btn_color_ref.value, color, [\n      'rgba(59, 194, 178, 1)',\n      'rgba(133, 196, 59, 1)',\n      'rgba(250, 180, 160, 1)',\n      'rgba(116, 87, 152, 1)',\n    ], changed, reset)\n  }\n  const destroyMinBtnColor = () => {\n    if (!tools) return\n    tools.destroy()\n    tools = null\n  }\n  const setMinBtnColor = (color: string) => {\n    tools?.setColor(color)\n  }\n\n  return {\n    min_btn_color_ref,\n    initMinBtnColor,\n    destroyMinBtnColor,\n    setMinBtnColor,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/Setting/components/ThemeSelectorModal.vue",
    "content": "<template>\n  <material-modal :show=\"modelValue\" bg-close=\"bg-close\" teleport=\"#view\" @close=\"$emit('update:modelValue', false)\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('theme_selector_modal__title') }}</h2>\n      <div class=\"scroll\" :class=\"$style.content\">\n        <div>\n          <h3>{{ $t('theme_selector_modal__light_title') }}</h3>\n          <ul :class=\"$style.theme\">\n            <li\n              v-for=\"theme in themeInfo.themeLights\" :key=\"theme.id\"\n              :style=\"theme.styles\" :aria-label=\"theme.name\"\n              :class=\"[{[$style.active]: appSetting['theme.lightId'] == theme.id}]\" @click=\"setLightId(theme.id)\"\n            >\n              <span :class=\"$style.bg\" />\n              <label>{{ theme.name }}</label>\n            </li>\n          </ul>\n        </div>\n        <div>\n          <h3>{{ $t('theme_selector_modal__dark_title') }}</h3>\n          <ul :class=\"$style.theme\">\n            <li\n              v-for=\"theme in themeInfo.themeDarks\" :key=\"theme.id\"\n              :style=\"theme.styles\" :aria-label=\"theme.name\"\n              :class=\"[{[$style.active]: appSetting['theme.darkId'] == theme.id}]\" @click=\"setDarkId(theme.id)\"\n            >\n              <span :class=\"$style.bg\" />\n              <label>{{ theme.name }}</label>\n            </li>\n          </ul>\n        </div>\n      </div>\n      <div :class=\"$style.note\">\n        <p>{{ $t('theme_selector_modal__title_tip') }}</p>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { markRaw, reactive, watch } from '@common/utils/vueTools'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { applyTheme, getThemes, buildBgUrl } from '@renderer/store/utils'\n\nexport default {\n  name: 'ThemeSelectorModal',\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue'],\n  setup(props) {\n    const themeInfo = reactive({\n      themeLights: [],\n      themeDarks: [],\n    })\n    let dataPath = ''\n\n    watch(() => props.modelValue, (val) => {\n      if (!val) return\n      getThemes((info) => {\n      // console.log(info)\n        const themes = [...info.themes, ...info.userThemes]\n        const lights = []\n        const darks = themes.filter(t => {\n          if (t.isDark) return true\n          lights.push(t)\n          return false\n        })\n        dataPath = info.dataPath\n        themeInfo.themeLights = lights.map(t => {\n          return {\n            id: t.id,\n            // @ts-expect-error\n            name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),\n            styles: {\n              '--color-primary-theme': t.config.themeColors['--color-theme'],\n              '--background-image-theme': t.isCustom\n                ? t.config.extInfo['--background-image'] == 'none'\n                  ? 'none'\n                  : buildBgUrl(t.config.extInfo['--background-image'], info.dataPath)\n                : t.config.extInfo['--background-image'],\n            },\n          }\n        })\n        themeInfo.themeDarks = markRaw(darks.map(t => {\n          return {\n            id: t.id,\n            // @ts-expect-error\n            name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),\n            styles: {\n              '--color-primary-theme': t.config.themeColors['--color-theme'],\n              '--background-image-theme': t.isCustom\n                ? t.config.extInfo['--background-image'] == 'none'\n                  ? 'none'\n                  : buildBgUrl(t.config.extInfo['--background-image'], info.dataPath)\n                : t.config.extInfo['--background-image'],\n            },\n          }\n        }))\n      })\n    })\n\n    const setLightId = (id) => {\n      if (appSetting['theme.lightId'] == id) return\n      updateSetting({ 'theme.lightId': id })\n      if (appSetting['theme.id'] == 'auto') applyTheme('auto', id, appSetting['theme.darkId'], dataPath)\n    }\n    const setDarkId = (id) => {\n      if (appSetting['theme.darkId'] == id) return\n      updateSetting({ 'theme.darkId': id })\n      if (appSetting['theme.id'] == 'auto') applyTheme('auto', appSetting['theme.lightId'], id, dataPath)\n    }\n    return {\n      appSetting,\n      themeInfo,\n      setLightId,\n      setDarkId,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.main {\n  // padding: 15px;\n  // max-width: 400px;\n  min-width: 300px;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  min-height: 0;\n  h2 {\n    flex: none;\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n    padding: 15px;\n  }\n  h3 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    padding-bottom: 15px;\n    font-size: 15px;\n  }\n}\n.content {\n  flex: auto;\n  padding: 15px;\n  display: flex;\n  flex-flow: column nowrap;\n  gap: 15px;\n}\n.theme {\n  display: flex;\n  flex-flow: row wrap;\n  // padding: 0 15px;\n\n  li {\n    display: flex;\n    flex-flow: column nowrap;\n    align-items: center;\n    cursor: pointer;\n    // color: var(--color-primary);\n    margin-right: 4px;\n    transition: color .3s ease;\n    margin-bottom: 15px;\n    width: 86px;\n\n    &:last-child {\n      margin-right: 0;\n    }\n\n    &.active {\n      color: var(--color-primary-theme);\n      .bg {\n        border-color: var(--color-primary-theme);\n      }\n    }\n\n    .bg {\n      display: block;\n      width: 36px;\n      height: 36px;\n      margin-bottom: 5px;\n      border: 2px solid transparent;\n      padding: 2px;\n      transition: border-color .3s ease;\n      border-radius: 5px;\n      &:after {\n        display: block;\n        content: ' ';\n        width: 100%;\n        height: 100%;\n        border-radius: @radius-border;\n        background-position: center;\n        background-size: cover;\n        background-repeat: no-repeat;\n        background-color: var(--color-primary-theme);\n        background-image: var(--background-image-theme);\n      }\n    }\n\n    label {\n      width: 100%;\n      text-align: center;\n      height: 1.2em;\n      font-size: 14px;\n    }\n  }\n}\n\n.note {\n  padding: 8px 15px;\n  font-size: 13px;\n  line-height: 1.25;\n  color: var(--color-font);\n  // p {\n  //   + p {\n  //     margin-top: 5px;\n  //   }\n  // }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/UserApiModal.vue",
    "content": "<template lang=\"pug\">\nmaterial-modal(:show=\"modelValue\" bg-close teleport=\"#view\" @close=\"handleClose\")\n  main.scroll(:class=\"$style.main\")\n    h2 {{ $t('user_api__title') }}\n    ul.scroll(v-if=\"apiList.length\" :class=\"$style.content\")\n      li(v-for=\"(api, index) in apiList\" :key=\"api.id\" :class=\"[$style.listItem, {[$style.active]: appSetting['common.apiSource'] == api.id}]\")\n        div(:class=\"$style.listLeft\")\n          h3\n            | {{ api.name }}\n            span(v-if=\"api.version\") {{ /^\\d/.test(api.version) ? `v${api.version}` : api.version }}\n            span(v-if=\"api.author\") {{ api.author }}\n          p {{ api.description }}\n          div\n            base-checkbox(:id=\"`user_api_${api.id}`\" v-model=\"api.allowShowUpdateAlert\" :class=\"$style.checkbox\" :label=\"$t('user_api__allow_show_update_alert')\" @change=\"handleChangeAllowUpdateAlert(api, $event)\")\n        base-btn(:class=\"$style.listBtn\" outline :aria-label=\"$t('user_api__btn_remove')\" @click.stop=\"handleRemove(index)\")\n          svg(v-once version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 212.982 212.982\" space=\"preserve\")\n            use(xlink:href=\"#icon-delete\")\n    div(v-else :class=\"$style.content\")\n      div(:class=\"$style.noitem\") {{ $t('user_api__noitem') }}\n    div(:class=\"$style.note\")\n      p(:class=\"[$style.ruleLink]\")\n        | {{ $t('user_api__readme') }}\n        span.hover.underline(aria-label=\"https://lxmusic.toside.cn/desktop/custom-source\" @click=\"handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/custom-source')\") FAQ\n      p {{ $t('user_api__note') }}\n    div(:class=\"$style.footer\")\n      base-btn(:class=\"$style.footerBtn\" @click=\"isShowOnlineImportModal = true\") {{ $t('user_api__btn_import_online') }}\n      base-btn(:class=\"$style.footerBtn\" @click=\"handleImport\") {{ $t('user_api__btn_import') }}\n      //- base-btn(:class=\"$style.footerBtn\" @click=\"handleExport\") {{ $t('user_api__btn_export') }}\n    UserApiOnlineImportModal(v-model:show=\"isShowOnlineImportModal\" @import=\"importUserApi\")\n</template>\n\n<script>\nimport { importUserApi, removeUserApi, showSelectDialog, setAllowShowUserApiUpdateAlert } from '@renderer/utils/ipc'\nimport { readFile } from '@common/utils/nodejs'\nimport { openUrl } from '@common/utils/electron'\nimport apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'\nimport { userApi } from '@renderer/store'\nimport { appSetting, updateSetting } from '@renderer/store/setting'\nimport { computed, ref } from '@common/utils/vueTools'\nimport { dialog } from '@renderer/plugins/Dialog'\n\nimport UserApiOnlineImportModal from './UserApiOnlineImportModal.vue'\n\nexport default {\n  components: {\n    UserApiOnlineImportModal,\n  },\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:modelValue'],\n  setup() {\n    const isShowOnlineImportModal = ref(false)\n    const apiList = computed(() => userApi.list)\n\n    return {\n      userApi,\n      apiList,\n      appSetting,\n      isShowOnlineImportModal,\n    }\n  },\n  methods: {\n    async importUserApi(script) {\n      return importUserApi(script).then(({ apiList }) => {\n        userApi.list = apiList\n      }).catch((err) => {\n        void dialog(this.$t('user_api_import__failed', { message: err.message }))\n      })\n    },\n    handleImport() {\n      if (this.userApi.list.length > 20) {\n        this.$dialog({\n          message: this.$t('user_api__max_tip'),\n          confirmButtonText: this.$t('ok'),\n        })\n        return\n      }\n      void showSelectDialog({\n        title: this.$t('user_api__import_file'),\n        properties: ['openFile'],\n        filters: [\n          { name: 'LX API File', extensions: ['js'] },\n          { name: 'All Files', extensions: ['*'] },\n        ],\n      }).then(async result => {\n        if (result.canceled) return\n        return readFile(result.filePaths[0]).then(async data => {\n          return this.importUserApi(data.toString())\n        })\n      })\n    },\n    handleExport() {\n\n    },\n    async handleRemove(index) {\n      const api = this.apiList[index]\n      if (!api) return\n      if (appSetting['common.apiSource'] == api.id) {\n        let backApi = apiSourceInfo.find(api => !api.disabled)\n        if (!backApi) backApi = userApi.list[0]\n        updateSetting({ 'common.apiSource': backApi?.id ?? '' })\n      }\n      userApi.list = await removeUserApi([api.id])\n    },\n    handleClose() {\n      this.$emit('update:modelValue', false)\n    },\n    handleOpenUrl(url) {\n      void openUrl(url)\n    },\n    handleChangeAllowUpdateAlert(api, enable) {\n      void setAllowShowUserApiUpdateAlert(api.id, enable)\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 15px 8px;\n  max-width: 550px;\n  min-width: 300px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 16px;\n    color: var(--color-font);\n    line-height: 1.3;\n    text-align: center;\n  }\n}\n\n.name {\n  color: var(--color-primary);\n}\n\n.checkbox {\n  margin-top: 3px;\n  font-size: 14px;\n  opacity: .86;\n}\n\n.content {\n  flex: auto;\n  min-height: 80px;\n  max-height: 100%;\n  margin-top: 15px;\n  padding: 0 7px;\n}\n.listItem {\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: center;\n  transition: background-color 0.2s ease;\n  padding: 15px 10px;\n  border-radius: @radius-border;\n  &:hover {\n    background-color: var(--color-primary-background-hover);\n  }\n  &.active {\n    background-color: var(--color-primary-background-active);\n  }\n  h3 {\n    font-size: 15px;\n    color: var(--color-font);\n    word-break: break-all;\n    span {\n      font-size: 12px;\n      color: var(--color-font-label);\n      margin-left: 6px;\n    }\n  }\n  p {\n    margin-top: 5px;\n    font-size: 14px;\n    color: var(--color-font-label);\n    word-break: break-all;\n  }\n}\n.noitem {\n  height: 100px;\n  font-size: 18px;\n  color: var(--color-font-label);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.listLeft {\n  flex: auto;\n  min-width: 0;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n}\n.listBtn {\n  flex: none;\n  height: 30px;\n  width: 30px;\n  padding: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  svg {\n    width: 60%;\n  }\n}\n.note {\n  padding: 0 7px;\n  margin-top: 15px;\n  font-size: 12px;\n  line-height: 1.25;\n  color: var(--color-font);\n  p {\n    + p {\n      margin-top: 5px;\n    }\n  }\n}\n.footer {\n  padding: 0 7px;\n  margin-top: 15px;\n  display: flex;\n  flex-flow: row nowrap;\n}\n.footerBtn {\n  flex: auto;\n  height: 36px;\n  line-height: 36px;\n  padding: 0 10px !important;\n  width: 150px;\n  .mixin-ellipsis-1();\n  + .footerBtn {\n    margin-left: 15px;\n  }\n}\n.ruleLink {\n  .mixin-ellipsis-1();\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/components/UserApiOnlineImportModal.vue",
    "content": "<template>\n  <material-modal :show=\"show\" teleport=\"#view\" @close=\"handleClose\" @after-enter=\"$refs.input.focus()\">\n    <main :class=\"$style.main\">\n      <h2>{{ $t('user_api_import_online__title') }}</h2>\n      <base-input\n        ref=\"input\"\n        v-model=\"url\"\n        :class=\"$style.input\"\n        type=\"url\"\n        :placeholder=\"$t('user_api_import_online__input_tip')\"\n        @submit=\"handleSubmit\" @blur=\"verify\"\n      />\n      <div :class=\"$style.footer\">\n        <base-btn :class=\"$style.btn\" @click=\"handleClose\">{{ $t('btn_close') }}</base-btn>\n        <base-btn :class=\"$style.btn\" :disabled=\"disabled\" @click=\"handleSubmit\">{{ btnText }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script>\nimport { dialog } from '@renderer/plugins/Dialog'\nimport { httpFetch } from '@renderer/utils/request'\n\nexport default {\n  props: {\n    show: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: ['update:show', 'import'],\n  data() {\n    return {\n      url: '',\n      disabled: false,\n      btnText: '',\n    }\n  },\n  watch: {\n    show(n) {\n      if (n) {\n        this.url = ''\n        this.disabled = false\n        this.btnText = this.$t('user_api_import_online__input_confirm')\n      }\n    },\n  },\n  methods: {\n    handleClose() {\n      this.$emit('update:show', false)\n    },\n    verify() {\n      if (!/^https?:\\/\\//.test(this.url)) this.url = ''\n      return this.url\n    },\n    async handleSubmit() {\n      let url = this.verify()\n      if (!url) return\n      this.disabled = true\n      this.btnText = this.$t('user_api_import_online__input_loading')\n      let script\n      try {\n        script = await httpFetch(url, { follow_max: 3 }).promise.then(resp => resp.body)\n      } catch (err) {\n        void dialog(this.$t('user_api_import__failed', { message: err.message }))\n        return\n      } finally {\n        this.disabled = false\n        this.btnText = this.$t('user_api_import_online__input_confirm')\n      }\n      if (script.length > 9_000_000) {\n        void dialog(this.$t('user_api_import__failed', {\n          message: 'Too large script',\n          confirm: this.$t('ok'),\n        }))\n        return\n      }\n      this.$emit('import', script)\n      this.handleClose()\n    },\n  },\n}\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 0 15px;\n  width: 450px;\n  min-width: 280px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 13px;\n    color: var(--color-font);\n    line-height: 1.3;\n    word-break: break-all;\n    // text-align: center;\n    padding: 15px 0 8px;\n  }\n}\n\n.input {\n  // width: 100%;\n  // height: 26px;\n  padding: 8px 8px;\n}\n.footer {\n  margin: 20px 0 15px auto;\n}\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 70px;\n  // .mixin-ellipsis-1();\n\n  +.btn {\n    margin-left: 10px;\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/Setting/index.vue",
    "content": "<template>\n  <div :class=\"$style.main\">\n    <div class=\"scroll\" :class=\"$style.toc\">\n      <ul :class=\"$style.tocList\" role=\"toolbar\">\n        <li v-for=\"h2 in tocList\" :key=\"h2.id\" :class=\"$style.tocListItem\" role=\"presentation\">\n          <h2\n            :class=\"[$style.tocH2, {[$style.active]: avtiveComponentName == h2.id }]\"\n            role=\"tab\" :aria-selected=\"avtiveComponentName == h2.id\"\n            :aria-label=\"h2.title\" ignore-tip @click=\"toggleTab(h2.id)\"\n          >\n            <transition name=\"list-active\">\n              <svg-icon v-if=\"avtiveComponentName == h2.id\" name=\"angle-right-solid\" :class=\"$style.activeIcon\" />\n            </transition>\n            {{ h2.title }}\n          </h2>\n          <!-- <ul v-if=\"h2.children.length\" :class=\"$style.tocList\">\n            <li v-for=\"h3 in h2.children\" :key=\"h3.id\" :class=\"$style.tocSubListItem\">\n              <h3 :class=\"[$style.tocH3, toc.activeId == h3.id ? $style.active : null]\" :aria-label=\"h3.title\">\n                <a :href=\"'#' + h3.id\" @click.stop=\"toc.activeId = h3.id\">{{ h3.title }}</a>\n              </h3>\n            </li>\n          </ul> -->\n        </li>\n      </ul>\n    </div>\n    <div ref=\"dom_content_ref\" class=\"scroll\" :class=\"$style.setting\">\n      <dl>\n        <component :is=\"avtiveComponentName\" />\n        <!-- <SettingBasic />\n        <SettingPlay />\n        <SettingPlayDetail />\n        <SettingDesktopLyric />\n        <SettingSearch />\n        <SettingList />\n        <SettingDownload />\n        <SettingSync />\n        <SettingHotKey />\n        <SettingNetwork />\n        <SettingOdc />\n        <SettingBackup />\n        <SettingOther />\n        <SettingUpdate />\n        <SettingAbout /> -->\n      </dl>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { ref, computed, nextTick } from '@common/utils/vueTools'\n// import { currentStting } from './setting'\nimport { useI18n } from '@renderer/plugins/i18n'\nimport { useRoute } from '@common/utils/vueRouter'\n\nimport SettingBasic from './components/SettingBasic.vue'\nimport SettingPlay from './components/SettingPlay.vue'\nimport SettingPlayDetail from './components/SettingPlayDetail.vue'\nimport SettingDesktopLyric from './components/SettingDesktopLyric.vue'\nimport SettingSearch from './components/SettingSearch.vue'\nimport SettingList from './components/SettingList.vue'\nimport SettingDownload from './components/SettingDownload.vue'\nimport SettingSync from './components/SettingSync/index.vue'\nimport SettingOpenAPI from './components/SettingOpenAPI.vue'\nimport SettingHotKey from './components/SettingHotKey.vue'\nimport SettingNetwork from './components/SettingNetwork.vue'\nimport SettingOdc from './components/SettingOdc.vue'\nimport SettingBackup from './components/SettingBackup.vue'\nimport SettingOther from './components/SettingOther.vue'\nimport SettingUpdate from './components/SettingUpdate.vue'\nimport SettingAbout from './components/SettingAbout.vue'\n\nexport default {\n  name: 'Setting',\n  components: {\n    SettingBasic,\n    SettingPlay,\n    SettingPlayDetail,\n    SettingDesktopLyric,\n    SettingSearch,\n    SettingList,\n    SettingDownload,\n    SettingSync,\n    SettingOpenAPI,\n    SettingHotKey,\n    SettingNetwork,\n    SettingOdc,\n    SettingBackup,\n    SettingOther,\n    SettingUpdate,\n    SettingAbout,\n  },\n  setup() {\n    const t = useI18n()\n    const route = useRoute()\n\n    const dom_content_ref = ref(null)\n\n    const tocList = computed(() => {\n      return [\n        { id: 'SettingBasic', title: t('setting__basic') },\n        { id: 'SettingPlay', title: t('setting__play') },\n        { id: 'SettingPlayDetail', title: t('setting__play_detail') },\n        { id: 'SettingDesktopLyric', title: t('setting__desktop_lyric') },\n        { id: 'SettingSearch', title: t('setting__search') },\n        { id: 'SettingList', title: t('setting__list') },\n        { id: 'SettingDownload', title: t('setting__download') },\n        { id: 'SettingHotKey', title: t('setting__hot_key') },\n        { id: 'SettingSync', title: t('setting__sync') },\n        { id: 'SettingOpenAPI', title: t('setting__open_api') },\n        { id: 'SettingNetwork', title: t('setting__network') },\n        { id: 'SettingOdc', title: t('setting__odc') },\n        { id: 'SettingBackup', title: t('setting__backup') },\n        { id: 'SettingOther', title: t('setting__other') },\n        { id: 'SettingUpdate', title: t('setting__update') },\n        { id: 'SettingAbout', title: t('setting__about') },\n      ]\n    })\n\n    const avtiveComponentName = ref(route.query.name && tocList.value.some(t => t.id == route.query.name)\n      ? route.query.name\n      : tocList.value[0].id)\n\n    const toggleTab = id => {\n      avtiveComponentName.value = id\n      void nextTick(() => {\n        dom_content_ref.value?.scrollTo({\n          top: 0,\n          behavior: 'smooth',\n        })\n      })\n    }\n\n    return {\n      tocList,\n      avtiveComponentName,\n      dom_content_ref,\n      toggleTab,\n    }\n  },\n  // mounted() {\n  //   this.initTOC()\n  // },\n  // methods: {\n  //   initTOC() {\n  //     const list = this.$refs.dom_setting_list.children\n  //     const toc = []\n  //     let prevTitle\n  //     for (const item of list) {\n  //       if (item.tagName == 'DT') {\n  //         prevTitle = {\n  //           title: item.innerText.replace(/[（(].+?[)）]/, ''),\n  //           id: item.getAttribute('id'),\n  //           dom: item,\n  //           children: [],\n  //         }\n  //         toc.push(prevTitle)\n  //         continue\n  //       }\n  //       const h3 = item.querySelector('h3')\n  //       if (h3) {\n  //         prevTitle.children.push({\n  //           title: h3.innerText.replace(/[（(].+?[)）]/, ''),\n  //           id: h3.getAttribute('id'),\n  //           dom: h3,\n  //         })\n  //       }\n  //     }\n  //     console.log(toc)\n  //     this.toc.list = toc\n  //   },\n  //   handleListScroll(event) {\n  //     // console.log(event.target.scrollTop)\n  //   },\n  // },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  display: flex;\n  flex-flow: row nowrap;\n  height: 100%;\n  border-top: var(--color-list-header-border-bottom);\n}\n\n.toc {\n  flex: 0 0 16%;\n  overflow-y: scroll;\n}\n.tocH2 {\n  line-height: 1.5;\n  .mixin-ellipsis-1();\n  font-size: 13px;\n  color: var(--color-font);\n  padding: 8px 10px;\n  transition: @transition-fast;\n  transition-property: background-color, color;\n\n  &:not(.active) {\n    cursor: pointer;\n    &:hover {\n      background-color: var(--color-button-background-hover);\n    }\n  }\n  &.active {\n    color: var(--color-primary);\n  }\n}\n.activeIcon {\n  height: .9em;\n  width: .9em;\n  margin-left: -0.45em;\n  vertical-align: -0.05em;\n}\n// .tocH3 {\n//   font-size: 13px;\n//   opacity: .8;\n// }\n\n// .tocList {\n//   .tocList {\n//     // padding-left: 15px;\n//   }\n// }\n// .tocSubListItem {\n//   padding-top: 10px;\n// }\n\n.setting {\n  padding: 0 15px 15px;\n  font-size: 14px;\n  box-sizing: border-box;\n  overflow-y: auto;\n  height: 100%;\n  position: relative;\n  width: 100%;\n\n  :global {\n    dt {\n      border-left: 5px solid var(--color-primary-alpha-700);\n      padding: 3px 7px;\n      margin: 15px 0;\n\n      + dd h3 {\n        margin-top: 0;\n      }\n    }\n\n    dd {\n      // margin-left: 15px;\n      // font-size: 13px;\n      > div {\n        padding: 0 15px;\n      }\n\n    }\n    h3 {\n      font-size: 12px;\n      margin: 25px 0 15px;\n    }\n    .p {\n      padding: 3px 0;\n      line-height: 1.3;\n      .btn {\n        + .btn {\n          margin-left: 10px;\n        }\n      }\n    }\n\n    .help-btn {\n      padding: 0;\n      margin: 0 0.4em;\n      border: none;\n      background: none;\n      color: var(--color-button-font);\n      cursor: pointer;\n      transition: opacity 0.2s ease;\n      &:hover {\n        opacity: 0.7;\n      }\n    }\n    .help-icon {\n      margin: 0 0.4em;\n    }\n  }\n}\n\n// .btn-content {\n//   display: inline-block;\n//   transition: @transition-theme;\n//   transition-property: opacity, transform;\n//   opacity: 1;\n//   transform: scale(1);\n\n//   &.hide {\n//     opacity: 0;\n//     transform: scale(0);\n//   }\n// }\n\n\n// :global(dt):target, :global(h3):target {\n//   animation: highlight 1s ease;\n// }\n\n// @keyframes highlight {\n//   from { background: yellow; }\n//   to { background: transparent; }\n// }\n\n</style>\n\n"
  },
  {
    "path": "src/renderer/views/songList/Detail/action.ts",
    "content": "import { tempListMeta, userLists } from '@renderer/store/list/state'\nimport { dialog } from '@renderer/plugins/Dialog'\nimport syncSourceList from '@renderer/store/list/syncSourceList'\nimport { getListDetail, getListDetailAll } from '@renderer/store/songList/action'\nimport { createUserList, setTempList } from '@renderer/store/list/action'\nimport { playList } from '@renderer/core/player/action'\nimport { LIST_IDS } from '@common/constants'\nimport { toMD5 } from '@renderer/utils'\n\nconst getListId = (id: string, source: LX.OnlineSource) => `${source}__${id}`\n\nexport const addSongListDetail = async(id: string, source: LX.OnlineSource, name?: string) => {\n  // console.log(this.listDetail.info)\n  // if (!this.listDetail.info.name) return\n  const listId = getListId(id, source)\n  const targetList = userLists.find(l => l.sourceListId == listId)\n  if (targetList) {\n    const confirm = await dialog.confirm({\n      message: window.i18n.t('duplicate_list_tip', { name: targetList.name }),\n      cancelButtonText: window.i18n.t('lists__import_part_button_cancel'),\n      confirmButtonText: window.i18n.t('confirm_button_text'),\n    })\n    if (!confirm) return\n    void syncSourceList(targetList)\n    return\n  }\n\n  const list = await getListDetailAll(id, source)\n  await createUserList({\n    name,\n    id: `${source}_${toMD5(listId)}`,\n    list,\n    source,\n    sourceListId: id,\n  })\n}\n\nexport const playSongListDetail = async(id: string, source: LX.OnlineSource, list?: LX.Music.MusicInfoOnline[], index: number = 0) => {\n  let isPlayingList = false\n  // console.log(list)\n  const listId = getListId(id, source)\n  if (!list?.length) list = (await getListDetail(id, source, 1)).list\n  if (list?.length) {\n    await setTempList(listId, [...list])\n    playList(LIST_IDS.TEMP, index)\n    isPlayingList = true\n  }\n  const fullList = await getListDetailAll(id, source)\n  if (!fullList.length) return\n  if (isPlayingList) {\n    if (tempListMeta.id == listId) {\n      await setTempList(listId, [...fullList])\n    }\n  } else {\n    await setTempList(listId, [...fullList])\n    playList(LIST_IDS.TEMP, index)\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/songList/Detail/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <div :class=\"$style.songListHeader\">\n      <div :class=\"$style.songListHeaderLeft\" :style=\"{ backgroundImage: 'url('+(picUrl || listDetailInfo.info.img)+')' }\">\n        <!-- <span v-if=\"listDetailInfo.info.play_count\" :class=\"$style.playNum\">{{ listDetailInfo.info.play_count }}</span> -->\n      </div>\n      <div :class=\"$style.songListHeaderMiddle\">\n        <h3 :title=\"listDetailInfo.info.name\">{{ listDetailInfo.info.name }}</h3>\n        <p :title=\"listDetailInfo.info.desc\">{{ listDetailInfo.info.desc }}</p>\n      </div>\n      <div :class=\"$style.songListHeaderRight\">\n        <base-btn\n          :class=\"$style.headerRightBtn\"\n          :disabled=\"!!listDetailInfo.noItemLabel\"\n          @click=\"playSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.list)\"\n        >\n          {{ $t('list__play') }}\n        </base-btn>\n        <base-btn\n          :class=\"$style.headerRightBtn\"\n          :disabled=\"!!listDetailInfo.noItemLabel\"\n          @click=\"addSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.info.name)\"\n        >\n          {{ $t('list__collect') }}\n        </base-btn>\n        <base-btn :class=\"$style.headerRightBtn\" @click=\"handleBack\">{{ $t('back') }}</base-btn>\n      </div>\n    </div>\n    <div :class=\"$style.list\">\n      <material-online-list\n        ref=\"listRef\"\n        :page=\"listDetailInfo.page\"\n        :limit=\"listDetailInfo.limit\"\n        :total=\"listDetailInfo.total\"\n        :list=\"listDetailInfo.list\"\n        :no-item=\"listDetailInfo.noItemLabel\"\n        @play-list=\"handlePlayList\"\n        @toggle-page=\"togglePage\"\n      />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { ref, watch } from '@common/utils/vueTools'\nimport { listDetailInfo } from '@renderer/store/songList/state'\nimport { setVisibleListDetail } from '@renderer/store/songList/action'\nimport { useRouter } from '@common/utils/vueRouter'\nimport { addSongListDetail, playSongListDetail } from './action'\nimport useList from './useList'\nimport useKeyBack from './useKeyBack'\n\n\nconst source = ref<LX.OnlineSource>('kw')\nconst id = ref<string>('')\nconst page = ref<number>(1)\nconst picUrl = ref<string>('')\nconst refresh = ref<boolean>(false)\n\n\ninterface Query {\n  source?: string\n  id?: string\n  page?: string\n  picUrl?: string\n  refresh?: 'true'\n  fromName?: string\n}\n\nconst verifyQueryParams = async function(this: any, to: { query: Query, path: string }, from: any, next: (route?: { path: string, query: Query }) => void) {\n  let _source = to.query.source\n  let _id = to.query.id\n  let _page: string | undefined = to.query.page\n  let _picUrl: string | undefined = to.query.picUrl\n  let _refresh: 'true' | undefined = to.query.refresh\n\n  if (_source == null || _id == null) {\n    if (listDetailInfo.key) {\n      _source = listDetailInfo.source\n      _id = listDetailInfo.id\n      _page = listDetailInfo.page.toString()\n      _picUrl = listDetailInfo.info.img\n    } else {\n      setVisibleListDetail(false)\n      next({ path: '/songList/list', query: {} })\n      return\n    }\n\n    next({\n      path: to.path,\n      query: { ...to.query, source: _source, id: _id, page: _page, picUrl: _picUrl, refresh: _refresh },\n    })\n    return\n  }\n  next()\n  setVisibleListDetail(true)\n  source.value = _source as LX.OnlineSource\n  id.value = _id\n  page.value = _page ? parseInt(_page) : 1\n  picUrl.value = _picUrl ?? ''\n  refresh.value = _refresh ? _refresh == 'true' : false\n  if (to.query.fromName) window.lx.songListInfo.fromName = to.query.fromName\n}\n\n\nexport default {\n  beforeRouteEnter: verifyQueryParams,\n  beforeRouteUpdate: verifyQueryParams,\n  setup() {\n    const router = useRouter()\n\n    const {\n      listRef,\n      listDetailInfo,\n      getListData,\n      handlePlayList,\n    } = useList()\n\n\n    const togglePage = (page: number) => {\n      void getListData(source.value, id.value, page, refresh.value)\n    }\n\n    const handleBack = () => {\n      setVisibleListDetail(false)\n      if (window.lx.songListInfo.fromName) void router.replace({ name: window.lx.songListInfo.fromName })\n      else router.back()\n    }\n\n    useKeyBack(handleBack)\n\n    watch([source, id, page, refresh], async([_source, _id, _page, _refresh]) => {\n      if (!_source || !_id) return router.replace({ path: '/songList/list' })\n      // console.log(_source, _id, _page, _refresh, picUrl.value)\n      // source.value = _source\n      // id.value = _id\n      // refresh.value = _refresh\n      // page.value = _page ?? 1\n      void getListData(_source, _id, _page, _refresh)\n    }, {\n      immediate: true,\n    })\n\n    return {\n      source,\n      id,\n      page,\n      picUrl,\n      listDetailInfo,\n      listRef,\n      togglePage,\n      addSongListDetail,\n      playSongListDetail,\n      handlePlayList,\n      handleBack,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  // position: absolute;\n  // left: 0;\n  // top: 0;\n  // width: 100%;\n  // height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n}\n\n.songListHeader {\n  flex: none;\n  display: flex;\n  flex-flow: row nowrap;\n  height: 80px;\n}\n.songListHeaderLeft {\n  flex: none;\n  margin-left: 15px;\n  height: 100%;\n  aspect-ratio: 1 / 1;\n  position: relative;\n  overflow: hidden;\n  border-radius: 4px;\n  background-position: center;\n  background-size: cover;\n  opacity: .9;\n  box-shadow: 0 0 2px 0 rgba(0,0,0,.2);\n}\n.playNum {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  padding: 4px;\n  background-color: rgba(0, 0, 0, 0.4);\n  color: #fff;\n  font-size: 12px;\n  text-align: right;\n  .mixin-ellipsis-1();\n}\n\n.songListHeaderMiddle {\n  flex: auto;\n  padding: 2px 7px;\n  min-width: 0;\n  h3 {\n    .mixin-ellipsis-1();\n    line-height: 1.2;\n    padding-bottom: 5px;\n    color: var(--color-font);\n  }\n  p {\n    .mixin-ellipsis(3);\n    font-size: 12px;\n    line-height: 1.2;\n    color: var(--color-font-label);\n  }\n}\n.songListHeaderRight {\n  flex: none;\n  display: flex;\n  align-items: center;\n  padding-right: 15px;\n\n  .headerRightBtn {\n    border-radius: 0;\n    &:first-child {\n      border-top-left-radius: 4px;\n      border-bottom-left-radius: 4px;\n    }\n    &:last-child {\n      border-top-right-radius: 4px;\n      border-bottom-right-radius: 4px;\n    }\n  }\n}\n\n.list {\n  position: relative;\n  width: 100%;\n  min-height: 0;\n  flex: auto;\n  height: 100%;\n}\n</style>\n\n"
  },
  {
    "path": "src/renderer/views/songList/Detail/useKeyBack.ts",
    "content": "import { onBeforeUnmount, onMounted } from '@common/utils/vueTools'\n\nexport default (handleBack: () => void) => {\n  const handle_key_backspace_down = (event: LX.KeyDownEevent) => {\n    if (event.event && (event.event.repeat || (event.event.target as HTMLElement).classList.contains('key-bind'))) return\n    handleBack()\n  }\n  onMounted(() => {\n    window.key_event.on('key_backspace_down', handle_key_backspace_down)\n  })\n\n  onBeforeUnmount(() => {\n    window.key_event.off('key_backspace_down', handle_key_backspace_down)\n  })\n}\n"
  },
  {
    "path": "src/renderer/views/songList/Detail/useList.ts",
    "content": "import { ref } from '@common/utils/vueTools'\n// import { useI18n } from '@renderer/plugins/i18n'\n// import { } from '@renderer/store/search/state'\nimport { getAndSetListDetail } from '@renderer/store/songList/action'\nimport { listDetailInfo } from '@renderer/store/songList/state'\nimport { playSongListDetail } from './action'\n\nexport default () => {\n  const listRef = ref<any>(null)\n\n  const getListData = async(source: LX.OnlineSource, id: string, page: number, refresh: boolean) => {\n    await getAndSetListDetail(id, source, page, refresh).then(() => {\n      setTimeout(() => {\n        if (listRef.value) listRef.value.scrollToTop()\n      })\n    })\n  }\n\n  const handlePlayList = (index: number) => {\n    void playSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.list, index)\n  }\n\n\n  return {\n    listRef,\n    listDetailInfo,\n    getListData,\n    handlePlayList,\n  }\n}\n"
  },
  {
    "path": "src/renderer/views/songList/List/ListView.vue",
    "content": "<template>\n  <SongList ref=\"list_ref\" :list-info=\"listInfo\" @toggle-page=\"togglePage\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { watch, ref, nextTick } from '@common/utils/vueTools'\nimport { listInfo } from '@renderer/store/songList/state'\nimport { getAndSetList } from '@renderer/store/songList/action'\nimport { useRouter, useRoute, onBeforeRouteLeave } from '@common/utils/vueRouter'\nimport SongList from './components/SongList.vue'\n\n\nconst props = defineProps<{\n  source: LX.OnlineSource\n  tagId: string\n  sortId?: string\n  page: number\n}>()\n\n\nconst list_ref = ref<any>(null)\n\nconst router = useRouter()\nconst route = useRoute()\n\nconst getListData = async(source: LX.OnlineSource, tabId: string, sortId: string, page: number) => {\n  // console.log(source, tabId, sortId, page)\n  await getAndSetList(source, tabId, sortId, page).then(() => {\n    if (listInfo.key == window.lx.songListInfo.songlistKey && window.lx.songListInfo.songlistPosition) {\n      void nextTick(() => {\n        list_ref.value?.scrollTo(window.lx.songListInfo.songlistPosition)\n      })\n    } else if (list_ref.value) {\n      window.lx.songListInfo.songlistKey = null\n      void nextTick(() => {\n        list_ref.value.scrollTo(0)\n      })\n    }\n  })\n}\n\nconst togglePage = (page: number) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      ...route.query,\n      sortId: props.sortId ?? '',\n      page,\n    },\n  })\n  // getListData(props.source, props.tagId, props.sortId ?? '', page)\n}\n\nwatch(() => [props.source, props.tagId, props.sortId, props.page], ([source, tagId, sortId, page]) => {\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  // console.log(source, tagId, sortId)\n  if (!source || !sortId) return\n  // console.log(source, tagId, sortId, page)\n  void getListData(source as LX.OnlineSource, tagId as string, sortId as string, page as number)\n}, {\n  immediate: true,\n})\n\nonBeforeRouteLeave(() => {\n  window.lx.songListInfo.songlistKey = listInfo.key\n  if (list_ref.value) window.lx.songListInfo.songlistPosition = list_ref.value.getScrollTop()\n})\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.container {\n  overflow: hidden;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  position: relative;\n}\n\n.listContent {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  font-size: 14px;\n  box-sizing: border-box;\n  padding: 0 15px;\n\n  ul {\n    display: flex;\n    flex-flow: row wrap;\n    justify-content: space-between;\n  }\n}\n.item {\n  width: 32%;\n  box-sizing: border-box;\n  display: flex;\n  margin-top: 15px;\n  cursor: pointer;\n  transition: opacity @transition-normal;\n  &:hover {\n    opacity: .7;\n  }\n}\n.left {\n  flex: none;\n  width: 88px;\n  height: 88px;\n  display: flex;\n  background-position: center;\n  background-size: cover;\n  border-radius: 4px;\n  overflow: hidden;\n  opacity: .9;\n\n  img {\n    object-fit: cover;\n  }\n  // box-shadow: 0 0 2px 0 rgba(0,0,0,.2);\n}\n.right {\n  flex: auto;\n  padding: 3px 15px 5px 7px;\n  overflow: hidden;\n  h4 {\n    font-size: 14px;\n    height: 2.6em;\n    text-align: justify;\n    line-height: 1.3;\n    .mixin-ellipsis-2();\n  }\n}\n.songlist_info {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 15px;\n  margin-top: 12px;\n  font-size: 12px;\n  .mixin-ellipsis-1();\n  text-align: justify;\n  line-height: 1.2;\n  // text-indent: 24px;\n  color: var(--color-font-label);\n  svg {\n    margin-right: 2px;\n  }\n}\n.author {\n  margin-top: 6px;\n  font-size: 12px;\n  .mixin-ellipsis-1();\n  text-align: justify;\n  line-height: 1.2;\n  // text-indent: 24px;\n  color: var(--color-font-label);\n}\n.pagination {\n  text-align: center;\n  padding: 15px 0;\n  // left: 50%;\n  // transform: translateX(-50%);\n}\n.noitem {\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n  // background-color: var(--color-000);\n\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/songList/List/components/OpenListModal.vue",
    "content": "<template>\n  <material-modal :show=\"props.modelValue\" teleport=\"#view\" width=\"60%\" @close=\"emit('update:model-value', $event)\" @after-enter=\"$refs.input.focus()\">\n    <main class=\"scroll\" :class=\"$style.main\">\n      <h2>{{ $t('songlist__import_input_title') }}</h2>\n      <div :class=\"$style.inputContent\">\n        <base-selection v-model=\"source\" :class=\"$style.select\" :list=\"props.sourceList\" item-key=\"id\" item-name=\"name\" />\n        <base-input\n          ref=\"input\"\n          v-model.trim=\"text\"\n          :class=\"$style.input\"\n          :placeholder=\"$t('songlist__import_input_tip')\"\n          @submit=\"handleSubmit\"\n        />\n      </div>\n      <div :class=\"$style.footer\">\n        <div :class=\"$style.tips\">\n          <ul>\n            <li>{{ $t('songlist__import_input_tip_1') }}</li>\n            <li>{{ $t('songlist__import_input_tip_2') }}</li>\n            <li>{{ $t('songlist__import_input_tip_3') }}</li>\n            <li>\n              {{ $t('songlist__import_input_tip_4') }}\n              <span\n                class=\"hover underline\"\n                aria-label=\"https://lyswhut.github.io/lx-music-doc/desktop/faq/cannot-open-songlist\"\n                @click=\"openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/cannot-open-songlist')\"\n              >FAQ</span>\n            </li>\n          </ul>\n        </div>\n        <base-btn :class=\"$style.btn\" @click=\"handleSubmit\">{{ $t('songlist__import_input_btn_confirm') }}</base-btn>\n      </div>\n    </main>\n  </material-modal>\n</template>\n\n<script setup>\nimport { openSongListInputInfo } from '@renderer/store/songList/state'\nimport { setOpenSongListInputInfo } from '@renderer/store/songList/action'\nimport { ref, watch } from '@common/utils/vueTools'\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\nimport { openUrl } from '@common/utils/electron'\n\nconst props = defineProps({\n  modelValue: Boolean,\n  sourceList: {\n    type: Array,\n    required: true,\n  },\n})\n\nconst emit = defineEmits(['update:model-value'])\n\nconst router = useRouter()\nconst route = useRoute()\nconst text = ref('')\nconst source = ref('')\n\nwatch(() => props.modelValue, (visible) => {\n  if (!visible) return\n  source.value = openSongListInputInfo.source || route.query.source\n  // text.value = openSongListInputInfo.text\n})\n\nconst handleSubmit = () => {\n  if (!text.value.length) return\n  setOpenSongListInputInfo(text.value, source.value)\n  void router.push({\n    path: '/songList/detail',\n    query: {\n      source: source.value,\n      id: text.value,\n      refresh: 'true',\n    },\n  })\n}\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.main {\n  padding: 0 15px;\n  // max-width: 530px;\n  // min-width: 300px;\n  display: flex;\n  flex-flow: column nowrap;\n  min-height: 0;\n  // max-height: 100%;\n  // overflow: hidden;\n  h2 {\n    font-size: 14px;\n    color: var(--color-font);\n    line-height: 1.3;\n    word-break: break-all;\n    // text-align: center;\n    padding: 15px 0 8px;\n  }\n}\n.inputContent {\n  display: flex;\n  flex-flow: row nowrap;\n}\n.select {\n  width: auto;\n  :global {\n    .label-content {\n      height: 100%;\n      border-top-right-radius: 0;\n      border-bottom-right-radius: 0;\n    }\n\n    .selection-list {\n      li {\n        // background-color: var(--color-main-background);\n        text-align: center;\n        line-height: 32px;\n        font-size: 13px;\n        &:hover {\n          background-color: var(--color-button-background-hover);\n        }\n        &:active {\n          background-color: var(--color-button-background-active);\n        }\n      }\n    }\n  }\n}\n.input {\n  flex: auto;\n  border-top-left-radius: 0;\n  border-bottom-left-radius: 0;\n  // width: 100%;\n  // height: 26px;\n  padding: 8px 8px;\n  color: var(--color-font);\n}\n.footer {\n  margin: 50px 0 15px;\n  display: flex;\n  flex-flow: row nowrap;\n  align-items: flex-end;\n}\n\n.tips {\n  flex: auto;\n  font-size: 12px;\n  color: var(--color-font);\n  line-height: 1.5;\n  ul {\n    list-style: decimal;\n    padding-left: 15px;\n  }\n}\n\n.btn {\n  // box-sizing: border-box;\n  // margin-left: 15px;\n  // margin-bottom: 15px;\n  // height: 36px;\n  // line-height: 36px;\n  // padding: 0 10px !important;\n  min-width: 80px;\n  // .mixin-ellipsis-1();\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/songList/List/components/SongList.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <div v-show=\"!props.listInfo.noItemLabel\" ref=\"dom_list_ref\" :class=\"$style.listContent\" class=\"scroll\">\n      <ul>\n        <li v-for=\"item in props.listInfo.list\" :key=\"item.id\" :class=\"$style.item\" @click=\"toDetail(item)\">\n          <div :class=\"$style.image\">\n            <img :class=\"$style.img\" loading=\"lazy\" decoding=\"async\" :src=\"item.img\">\n          </div>\n          <div :class=\"$style.desc\">\n            <h4>{{ item.name }}</h4>\n            <div>\n              <p :class=\"$style.author\">{{ item.author }}</p>\n              <p v-if=\"item.time\" :class=\"$style.time\">{{ item.time }}</p>\n              <div :class=\"$style.songlist_info\">\n                <span v-if=\"item.total != null\"><svg-icon name=\"music\" />{{ item.total }}</span>\n                <span v-if=\"item.play_count != null\"><svg-icon name=\"headphones\" />{{ item.play_count }}</span>\n                <span v-if=\"visibleSource\">{{ item.source }}</span>\n              </div>\n            </div>\n          </div>\n        </li>\n        <li v-for=\"(i, index) in 6\" :key=\"index\" :class=\"$style.item\" style=\"margin-bottom: 0;height: 0;\" />\n      </ul>\n      <div :class=\"$style.pagination\">\n        <material-pagination :count=\"props.listInfo.total\" :limit=\"props.listInfo.limit\" :page=\"props.listInfo.page\" @btn-click=\"togglePage\" />\n      </div>\n    </div>\n    <transition enter-active-class=\"animated fadeIn\" leave-active-class=\"animated fadeOut\">\n      <div v-show=\"props.listInfo.noItemLabel\" :class=\"$style.noitem\">\n        <p v-text=\"props.listInfo.noItemLabel\" />\n      </div>\n    </transition>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from '@common/utils/vueTools'\nimport type { ListInfo, ListInfoItem } from '@renderer/store/songList/state'\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\n\n\nconst props = withDefaults(defineProps<{\n  listInfo: ListInfo\n  visibleSource?: boolean\n}>(), {\n  visibleSource: false,\n})\n\nconst router = useRouter()\nconst route = useRoute()\n\nconst dom_list_ref = ref<HTMLElement | null>(null)\n\nconst emit = defineEmits(['toggle-page'])\n\n\nconst togglePage = (page: number) => {\n  emit('toggle-page', page)\n}\n\nconst toDetail = (info: ListInfoItem) => {\n  void router.push({\n    path: '/songList/detail',\n    query: {\n      source: info.source,\n      id: info.id,\n      picUrl: info.img,\n      fromName: route.name as string,\n    },\n  })\n}\n\ndefineExpose({\n  scrollTo(top: number) {\n    dom_list_ref.value?.scrollTo({\n      top,\n      // behavior: 'smooth',\n    })\n  },\n  getScrollTop() {\n    return dom_list_ref.value?.scrollTop ?? 0\n  },\n})\n\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n.container {\n  overflow: hidden;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  position: relative;\n}\n\n.listContent {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  font-size: 14px;\n  box-sizing: border-box;\n  padding: 15px 15px 0;\n\n  ul {\n    display: flex;\n    flex-flow: row wrap;\n    justify-content: space-between;\n  }\n}\n.item {\n  max-width: 360px;\n  width: 32%;\n  box-sizing: border-box;\n  display: flex;\n  // flex-flow: column nowrap;\n  // padding: 10px;\n  margin-bottom: 20px;\n  cursor: pointer;\n  transition: opacity @transition-normal;\n  &:hover {\n    opacity: .7;\n  }\n}\n.image {\n  flex: none;\n  width: 40%;\n  display: flex;\n  background-position: center;\n  background-size: cover;\n  border-radius: 4px;\n  overflow: hidden;\n  opacity: .9;\n  aspect-ratio: 1 / 1;\n\n  box-shadow: 0 0 2px 0 rgba(0,0,0,.2);\n}\n.img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.desc {\n  flex: auto;\n  padding: 2px 15px 2px 7px;\n  overflow: hidden;\n  h4 {\n    font-size: 14px;\n    // height: 2.6em;\n    text-align: justify;\n    line-height: 1.3;\n    .mixin-ellipsis-2();\n  }\n}\n.songlist_info {\n  display: flex;\n  flex-flow: row nowrap;\n  gap: 15px;\n  margin-top: 8px;\n  font-size: 12px;\n  .mixin-ellipsis-1();\n  text-align: justify;\n  line-height: 1.2;\n  // text-indent: 24px;\n  color: var(--color-font-label);\n  svg {\n    margin-right: 2px;\n  }\n}\n.author {\n  margin-top: 6px;\n  font-size: 12px;\n  .mixin-ellipsis-1();\n  text-align: justify;\n  line-height: 1.3;\n  // text-indent: 24px;\n  color: var(--color-font-label);\n}\n.time {\n  margin-top: 3px;\n  font-size: 12px;\n  .mixin-ellipsis-1();\n  text-align: justify;\n  line-height: 1.3;\n  // text-indent: 24px;\n  color: var(--color-font-label);\n}\n.pagination {\n  text-align: center;\n  padding: 15px 0;\n  // left: 50%;\n  // transform: translateX(-50%);\n}\n.noitem {\n  position: absolute;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  justify-content: center;\n  align-items: center;\n  // background-color: var(--color-000);\n\n  p {\n    font-size: 24px;\n    color: var(--color-font-label);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/songList/List/components/SortTab.vue",
    "content": "<template>\n  <base-tab :model-value=\"sortId\" :class=\"$style.tab\" :list=\"list\" item-label=\"name\" @change=\"handleToggle\" />\n</template>\n\n<script setup>\nimport { watch, shallowReactive } from '@common/utils/vueTools'\nimport { sortList } from '@renderer/store/songList/state'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\n\nconst props = defineProps({\n  source: {\n    type: String,\n    required: true,\n  },\n  tagId: {\n    type: String,\n    required: true,\n  },\n  sortId: {\n    type: String,\n    default: '',\n  },\n})\n\nconst router = useRouter()\nconst route = useRoute()\n\nconst list = shallowReactive([])\n\n\nconst handleToggle = (id) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      source: props.source,\n      tagId: props.tagId,\n      sortId: id,\n    },\n  })\n}\nwatch(() => props.source, async(source) => {\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  if (!source) return\n  let _list = sortList[source] ?? []\n  list.splice(0, list.length, ..._list)\n  if (!props.sortId && list.length) handleToggle(list[0].id)\n  // console.log(list)\n}, {\n  immediate: true,\n})\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.tagList {\n  font-size: 12px;\n  position: relative;\n\n  &.active {\n    .label {\n      .icon {\n        svg{\n          transform: rotate(180deg);\n        }\n      }\n    }\n    .list {\n      opacity: 1;\n      transform: scaleY(1);\n    }\n  }\n}\n\n.label {\n  padding: 8px 15px;\n  // background-color: var(--color-button-background);\n  transition: background-color @transition-normal;\n  // border-top: 2px solid @color-tab-border-bottom;\n  // border-left: 2px solid @color-tab-border-bottom;\n  box-sizing: border-box;\n  text-align: center;\n  // border-top-left-radius: 3px;\n  color: var(--color-button-font);\n  cursor: pointer;\n\n  display: flex;\n\n  span {\n    flex: auto;\n  }\n  .icon {\n    flex: none;\n    margin-left: 7px;\n    line-height: 0;\n    svg {\n      width: .9em;\n      transition: transform .2s ease;\n      transform: rotate(0);\n    }\n  }\n\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n\n.list {\n  position: absolute;\n  top: 100%;\n  width: 645px;\n  left: 0;\n  // border-bottom: 2px solid @color-tab-border-bottom;\n  // border-right: 2px solid @color-tab-border-bottom;\n  border-bottom-right-radius: 5px;\n  background-color: var(--color-main-background);\n  opacity: 0;\n  transform: scaleY(0);\n  overflow-y: auto;\n  transform-origin: 0 0 0;\n  max-height: 250px;\n  transition: .25s ease;\n  transition-property: transform, opacity;\n  z-index: 10;\n  padding: 10px;\n  box-sizing: border-box;\n\n  li {\n    cursor: pointer;\n    padding: 8px 15px;\n    // color: var(--color-button-font);\n    text-align: center;\n    outline: none;\n    transition: background-color @transition-normal;\n    background-color: var(--color-button-background);\n    box-sizing: border-box;\n\n    &:hover {\n      background-color: var(--color-button-background-hover);\n    }\n    &:active {\n      background-color: var(--color-button-background-active);\n    }\n  }\n}\n\n.type {\n  padding-top: 10px;\n  padding-bottom: 3px;\n  color: var(--color-font-label);\n}\n\n.tag {\n  display: inline-block;\n  margin: 5px;\n  background-color: var(--color-button-background);\n  padding: 8px 10px;\n  border-radius: @radius-progress-border;\n  transition: background-color @transition-normal;\n  cursor: pointer;\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/songList/List/components/TagList.vue",
    "content": "<template>\n  <div :class=\"[$style.tagList, {[$style.active]: popupVisible}]\">\n    <div ref=\"dom_btn\" :class=\"$style.label\" @click.stop=\"handleShow\">\n      <span>{{ tagName }}</span>\n      <div :class=\"$style.icon\">\n        <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"100%\" viewBox=\"0 0 451.847 451.847\" space=\"preserve\">\n          <use xlink:href=\"#icon-down\" />\n        </svg>\n      </div>\n    </div>\n    <div :class=\"$style.popup\" :style=\"popupStyle\" :aria-hidden=\"!popupVisible\" @click.stop>\n      <div :class=\"$style.list\" class=\"scroll\">\n        <div :class=\"$style.tag\" @click=\"handleToggleTag('')\">{{ $t('default') }}</div>\n        <dl v-for=\"tagInfo in list\" :key=\"tagInfo.name\">\n          <dt :class=\"$style.type\">{{ tagInfo.name }}</dt>\n          <dd v-for=\"tag in tagInfo.list\" :key=\"tag.id\" :class=\"$style.tag\" @click=\"handleToggleTag(tag.id)\">{{ tag.name }}</dd>\n        </dl>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { watch, shallowReactive, ref, onMounted, onBeforeUnmount, computed, reactive } from '@common/utils/vueTools'\nimport { setTags, getTags } from '@renderer/store/songList/action'\nimport { tags } from '@renderer/store/songList/state'\nimport { useRouter, useRoute } from '@common/utils/vueRouter'\nimport { useI18n } from '@renderer/plugins/i18n'\n\nconst props = defineProps({\n  source: {\n    type: String,\n    required: true,\n  },\n  tagId: {\n    type: String,\n    required: true,\n  },\n  sortId: {\n    type: [String, undefined],\n    default: undefined,\n  },\n})\n\nconst router = useRouter()\nconst route = useRoute()\nconst t = useI18n()\n\nconst list = shallowReactive([])\nconst handleToggleTag = (id) => {\n  void router.replace({\n    path: route.path,\n    query: {\n      source: props.source,\n      tagId: id,\n      sortId: props.sortId,\n    },\n  })\n  handleHide()\n}\nwatch(() => props.source, async(source) => {\n  if (!source) return\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  let tagInfo = tags[source]\n  // console.log(await getTags(source))\n  if (tagInfo == null) setTags(tagInfo = await getTags(source), source)\n\n  list.splice(0, list.length, ...[{ name: window.i18n.t('songlist__tag_info_hot_tag'), list: [...tagInfo.hotTag] }, ...tagInfo.tags])\n}, {\n  immediate: true,\n})\nconst tagName = computed(() => {\n  if (!props.tagId) return t('default')\n  for (const tags of list) {\n    const tag = tags.list.find(t => t.id == props.tagId)\n    if (tag) return tag.name\n  }\n  return props.tagId\n})\n\nconst popupStyle = reactive({\n  width: '645px',\n  maxHeight: '250px',\n})\n\nconst setTagPopupWidth = () => {\n  window.setTimeout(() => {\n    const dom_view = document.getElementById('view')\n    popupStyle.width = dom_view.clientWidth * 0.96 + 'px'\n    popupStyle.maxHeight = dom_view.clientHeight * 0.65 + 'px'\n  }, 50)\n}\n\nconst dom_btn = ref<HTMLElement | null>(null)\nconst popupVisible = ref(false)\nconst handleShow = () => popupVisible.value = !popupVisible.value\nconst handleHide = (evt) => {\n  // if (e && e.target.parentNode != this.$refs.dom_popup && this.show) return this.show = false\n  // console.log(this.$refs)\n  if (evt && (evt.target == dom_btn.value || dom_btn.value?.contains(evt.target))) return\n  popupVisible.value = false\n}\n\n\nonMounted(() => {\n  setTagPopupWidth()\n  document.addEventListener('click', handleHide)\n  window.addEventListener('resize', setTagPopupWidth)\n})\n\nonBeforeUnmount(() => {\n  document.removeEventListener('click', handleHide)\n  window.removeEventListener('resize', setTagPopupWidth)\n})\n\n</script>\n\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.tagList {\n  font-size: 12px;\n  position: relative;\n\n  &.active {\n    .label {\n      .icon {\n        svg{\n          transform: rotate(180deg);\n        }\n      }\n    }\n    .popup {\n      opacity: 1;\n      transform: scale(1);\n      pointer-events: initial;\n    }\n  }\n}\n\n.label {\n  padding: 8px 15px;\n  // background-color: var(--color-button-background);\n  transition: color @transition-normal;\n  // border-top: 2px solid @color-tab-border-bottom;\n  // border-left: 2px solid @color-tab-border-bottom;\n  box-sizing: border-box;\n  text-align: center;\n  // border-top-left-radius: 3px;\n  color: var(--color-font);\n  cursor: pointer;\n\n  display: flex;\n\n  span {\n    flex: auto;\n  }\n  .icon {\n    flex: none;\n    margin-left: 7px;\n    line-height: 0;\n    svg {\n      width: .8em;\n      transition: transform .2s ease;\n      transform: rotate(0);\n    }\n  }\n\n  &:hover {\n    color: var(--color-primary-font-hover);\n  }\n  &:active {\n    color: var(--color-primary-font-active);\n  }\n}\n\n.popup {\n  position: absolute;\n  top: 100%;\n  width: 645px;\n  left: 8px;\n  margin-top: 12px;\n  border-radius: 4px;\n  background-color: var(--color-content-background);\n  opacity: 0;\n  transform: scale(.95, .8);\n  transform-origin: 0 0 0;\n  transition: .25s ease;\n  transition-property: transform, opacity;\n  max-height: 250px;\n  z-index: 10;\n  pointer-events: none;\n  filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, .15));\n  display: flex;\n\n  &:before {\n    content: \" \";\n    position: absolute;\n    top: -6px;\n    left: 20px;\n    width: 0;\n    height: 0;\n    border-left: 8px solid transparent;\n    border-right: 8px solid transparent;\n    border-bottom: 8px solid var(--color-content-background);\n  }\n}\n.list {\n  padding: 10px;\n  box-sizing: border-box;\n  // box-shadow: 0 0 4px rgba(0, 0, 0, .2);\n}\n\n.type {\n  padding-top: 10px;\n  padding-bottom: 3px;\n  color: var(--color-font-label);\n}\n\n.tag {\n  display: inline-block;\n  margin: 5px;\n  background-color: var(--color-button-background);\n  padding: 8px 10px;\n  border-radius: @radius-progress-border;\n  transition: background-color @transition-normal;\n  cursor: pointer;\n  &:hover {\n    background-color: var(--color-button-background-hover);\n  }\n  &:active {\n    background-color: var(--color-button-background-active);\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/views/songList/List/index.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <div :class=\"$style.header\">\n      <div :class=\"$style.left\">\n        <tag-list :source=\"source\" :tag-id=\"tagId\" :sort-id=\"sortId\" />\n        <sort-tab :source=\"source\" :tag-id=\"tagId\" :sort-id=\"sortId\" />\n      </div>\n      <base-btn :class=\"$style.btn\" outline min @click=\"visibleOpenSongListModal = true\">{{ $t('songlist__import_input_show_btn') }}</base-btn>\n      <base-selection :model-value=\"source\" :class=\"$style.select\" :list=\"sourceList\" item-key=\"id\" item-name=\"name\" @update:model-value=\"handleToggleSource\" />\n    </div>\n    <list-view :source=\"source\" :tag-id=\"tagId\" :sort-id=\"sortId\" :page=\"page\" />\n    <open-list-modal v-model=\"visibleOpenSongListModal\" :source-list=\"sourceList\" />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { computed, ref } from '@common/utils/vueTools'\nimport { getSongListSetting, setSongListSetting } from '@renderer/utils/data'\nimport TagList from './components/TagList.vue'\nimport SortTab from './components/SortTab.vue'\nimport OpenListModal from './components/OpenListModal.vue'\nimport ListView from './ListView.vue'\nimport { sources, listInfo, isVisibleListDetail } from '@renderer/store/songList/state'\nimport { sourceNames } from '@renderer/store'\nimport { useRoute, useRouter } from '@common/utils/vueRouter'\n\nconst source = ref<LX.OnlineSource>('kw')\nconst tagId = ref<string>('')\nconst sortId = ref<string>('')\nconst page = ref<number>(1)\n\n\ninterface Query {\n  source?: string\n  tagId?: string\n  sortId?: string\n  page?: string\n}\n\nconst verifyQueryParams = async function(this: any, to: { query: Query, path: string }, from: any, next: (route?: { path: string, query: Query }) => void) {\n  let _source = to.query.source\n  let _tagId = to.query.tagId\n  let _sortId = to.query.sortId\n  let _page: string | undefined = to.query.page\n\n  if (isVisibleListDetail.value) {\n    next({ path: '/songList/detail', query: {} })\n    return\n  } else if (_source == null) {\n    if (listInfo.key) {\n      _source = listInfo.source\n      _tagId = listInfo.tagId\n      _sortId = listInfo.sortId\n      _page = listInfo.page.toString()\n    } else {\n      const setting = await getSongListSetting()\n      _source = setting.source\n      _tagId = setting.tagId\n      _sortId = setting.sortId\n      _page = '1'\n    }\n\n    next({\n      path: to.path,\n      query: { ...to.query, source: _source, tagId: _tagId, sortId: _sortId, page: _page },\n    })\n    return\n  }\n  next()\n  source.value = _source as LX.OnlineSource\n  tagId.value = _tagId ?? ''\n  sortId.value = _sortId ?? ''\n  page.value = _page ? parseInt(_page) : 1\n  void setSongListSetting({ source: _source, tagId: _tagId, sortId: _sortId })\n}\n\n\nexport default {\n  components: {\n    TagList,\n    SortTab,\n    ListView,\n    OpenListModal,\n  },\n  beforeRouteEnter: verifyQueryParams,\n  beforeRouteUpdate: verifyQueryParams,\n  setup() {\n    const visibleOpenSongListModal = ref(false)\n\n    const sourceList = computed(() => {\n      return sources.map(s => ({ id: s, name: sourceNames.value[s] }))\n    })\n    const router = useRouter()\n    const route = useRoute()\n    const handleToggleSource = (id: LX.OnlineSource) => {\n      if (id == source.value) return\n      void router.replace({\n        path: route.path,\n        query: {\n          source: id,\n          tagId: '',\n        },\n      })\n    }\n\n    return {\n      source,\n      tagId,\n      sortId,\n      page,\n      sourceList,\n      handleToggleSource,\n      visibleOpenSongListModal,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@renderer/assets/styles/layout.less';\n\n.container {\n  height: 100%;\n  display: flex;\n  flex-flow: column nowrap;\n  position: relative;\n}\n.header {\n  flex: none;\n  width: 100%;\n  display: flex;\n  flex-flow: row nowrap;\n  // padding-right: 5px;\n  // box-sizing: border-box;\n  padding-bottom: 5px;\n}\n.left {\n  flex: auto;\n  display: flex;\n  flex-flow: row nowrap;\n}\n\n.btn {\n  color: var(--color-font);\n  transition: color @transition-fast;\n  background: none !important;\n  &:hover {\n    color: var(--color-primary-font-hover);\n  }\n}\n\n\n.select {\n  font-size: 12px;\n  width: auto;\n  flex: none;\n  padding: 0 5px;\n\n  &:hover {\n    :global(.icon) {\n      opacity: 1;\n    }\n  }\n\n\n  :global {\n    .label-content {\n      background-color: transparent !important;\n      transition: color @transition-fast;\n      color: var(--color-font);\n      // line-height: 38px;\n      // height: 38px;\n      border-radius: 0;\n      &:hover {\n        // background: none !important;\n        color: var(--color-primary-font-hover);\n        .icon {\n          opacity: 1;\n          // color: var(--color-primary-font-hover);\n        }\n      }\n    }\n    // .label {\n    //   color: var(--color-font) !important;\n    // }\n    .icon {\n      svg {\n        width: .8em;\n      }\n      // opacity: .6;\n      // transition: color @transition-fast;\n      // color: var(--color-font-label);\n    }\n\n    .selection-list {\n      max-height: 500px;\n      box-shadow: 0 1px 4px 0 rgba(0,0,0,.2);\n      li {\n        // background-color: var(--color-main-background);\n        text-align: center;\n        line-height: 38px;\n        font-size: 13px;\n        &:hover {\n          background-color: var(--color-button-background-hover);\n        }\n        &:active {\n          background-color: var(--color-button-background-active);\n        }\n      }\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer/worker/download/common.ts",
    "content": "import { setMeta } from '@common/utils/musicMeta'\nimport { buildLyrics } from './lrcTool'\n\nexport const writeMeta = ({ filePath, isEmbedLyricLx, isEmbedLyricT, isEmbedLyricR, ...meta }: {\n  filePath: string\n  isEmbedLyricLx: boolean\n  isEmbedLyricT: boolean\n  isEmbedLyricR: boolean\n  title: string\n  artist: string\n  album: string\n  APIC: string | null\n}, lyric: LX.Music.LyricInfo, proxy?: { host: string, port: number }) => {\n  setMeta(filePath, { ...meta, lyrics: buildLyrics(lyric, isEmbedLyricLx, isEmbedLyricT, isEmbedLyricR) }, proxy)\n}\n\nexport { saveLrc } from './utils'\n"
  },
  {
    "path": "src/renderer/worker/download/download.ts",
    "content": "import { createDownload, type DownloaderType, type Options as DownloadOptions } from '@common/utils/download'\n// import music from '@renderer/utils/musicSdk'\nimport { createDownloadInfo } from './utils'\n// import {\n//   filterFileName,\n// } from '@common/utils/common'\n// import {\n//   assertApiSupport,\n//   getExt,\n// } from '..'\nimport { checkAndCreateDir, checkPath, getFileStats, removeFile } from '@common/utils/nodejs'\nimport { DOWNLOAD_STATUS } from '@common/constants'\n// import { download as eventDownloadNames } from '@renderer/event/names'\n\n// window.downloadList = []\n// window.downloadListFull = []\n// window.downloadListFullMap = new Map()\n\nconst dls = new Map<string, DownloaderType>()\nconst tryNum = new Map<string, number>()\nconst taskActions = new Map<string, (action: LX.Download.DownloadTaskActions) => void>()\nconst tasks = new Map<string, LX.Download.ListItem>()\n\nexport const checkList = (list: LX.Download.ListItem[], musicInfo: LX.Music.MusicInfo, quality: LX.Quality, ext: string): boolean => {\n  return list.some(s => s.id === musicInfo.id && (s.metadata.quality === quality || s.metadata.ext === ext))\n}\n\n// const removeTask = (id: string) => {\n//   dls.delete(id)\n//   tryNum.delete(id)\n//   taskActions.delete(id)\n//   tasks.delete(id)\n// }\nconst sendAction = (id: string, action: LX.Download.DownloadTaskActions) => {\n  const callback = taskActions.get(id)\n  if (!callback) return\n  callback(action)\n}\n\nexport const createDownloadTasks = (\n  list: LX.Music.MusicInfoOnline[],\n  quality: LX.Quality,\n  fileNameFormat: string,\n  qualityList: LX.QualityList,\n  listId?: string,\n): LX.Download.ListItem[] => {\n  return list.map(musicInfo => {\n    return createDownloadInfo(musicInfo, quality, fileNameFormat, qualityList, listId)\n  }).filter(task => task)\n  // commit('addTasks', { list: taskList, addMusicLocationType: rootState.setting.list.addMusicLocationType })\n  // let result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum)\n  // while (result) {\n  //   dispatch('startTask', result)\n  //   result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum)\n  // }\n}\n\nconst createTask = async(downloadInfo: LX.Download.ListItem, savePath: string, skipExistFile: boolean, proxy?: { host: string, port: number }) => {\n  // console.log('createTask', downloadInfo, savePath)\n  // 开始任务\n  /* commit('onStart', downloadInfo)\n  commit('setStatusText', { downloadInfo, text: '任务初始化中' }) */\n  if (!await checkAndCreateDir(savePath)) {\n    sendAction(downloadInfo.id, {\n      action: 'error',\n      data: {\n        error: 'download_status_error_check_path',\n      },\n    })\n    return\n  }\n  if (!tasks.has(downloadInfo.id)) return\n\n  if (downloadInfo.downloaded == 0) {\n    if (skipExistFile) {\n      const stats = await getFileStats(downloadInfo.metadata.filePath)\n      if (stats && stats.size > 100) {\n        sendAction(downloadInfo.id, {\n          action: 'error',\n          data: {\n            error: 'download_status_error_check_path_exist',\n          },\n        })\n        return\n      }\n    } else if (await checkPath(downloadInfo.metadata.filePath)) {\n      try {\n        await removeFile(downloadInfo.metadata.filePath)\n      } catch (err) {\n        sendAction(downloadInfo.id, {\n          action: 'error',\n          data: {\n            error: 'download_status_error_check_path',\n          },\n        })\n        return\n      }\n    }\n  }\n\n  const downloadOptions: DownloadOptions = {\n    url: downloadInfo.metadata.url ?? '',\n    path: savePath,\n    fileName: downloadInfo.metadata.fileName,\n    method: 'get',\n    proxy,\n    onCompleted() {\n      // if (downloadInfo.progress.progress != '100.00') {\n      //   delete.get(downloadInfo.id)?\n      //   return dispatch('startTask', downloadInfo)\n      // }\n      downloadInfo.isComplate = true\n      downloadInfo.status = DOWNLOAD_STATUS.COMPLETED\n      sendAction(downloadInfo.id, { action: 'complete' })\n      console.log('on complate')\n    },\n    onError(err: any) {\n      console.error(err)\n      if (err.code == 'EPERM') {\n        sendAction(downloadInfo.id, {\n          action: 'error',\n          data: {\n            error: 'download_status_error_write',\n            message: err.message,\n          },\n          // data: `歌曲保存位置被占用或没有写入权限，请尝试更改歌曲保存目录或重启软件或重启电脑，错误详情：${err.message as string}`,\n        })\n        return\n      }\n      // console.log(tryNum[downloadInfo.id])\n      let retryNum = tryNum.get(downloadInfo.id) ?? 0\n      tryNum.set(downloadInfo.id, ++retryNum)\n      if (retryNum > 2) {\n        sendAction(downloadInfo.id, {\n          action: 'error',\n          data: {\n            message: err.message,\n          },\n        })\n        // dispatch('startTask')\n        return\n      }\n      if (err.message?.startsWith('Resume failed')) {\n        removeFile(downloadInfo.metadata.filePath).catch(err => {\n          console.log('删除不匹配的文件失败：', err.message)\n          // commit('onError', { downloadInfo, errorMsg: '删除不匹配的文件失败：' + err.message })\n        }).finally(() => {\n          console.log('正在重试')\n          void dls.get(downloadInfo.id)?.start()\n          // sendAction(downloadInfo.id, {\n          //   action: 'statusText',\n          //   data: 'download_status_error_retrying',\n          // })\n        })\n        return\n      }\n      if (err.code == 'ENOTFOUND') {\n        sendAction(downloadInfo.id, { action: 'refreshUrl' })\n      } else {\n        console.log('Download failed, Attempting Retry')\n        setTimeout(() => {\n          void dls.get(downloadInfo.id)?.start()\n        }, 1000)\n      }\n    },\n    onFail(response) {\n      let retryNum = tryNum.get(downloadInfo.id) ?? 0\n      tryNum.set(downloadInfo.id, ++retryNum)\n      if (retryNum > 2) {\n        if (response.statusCode) {\n          sendAction(downloadInfo.id, {\n            action: 'error',\n            data: {\n              error: 'download_status_error_response',\n              message: String(response.statusCode),\n            },\n          })\n        } else {\n          sendAction(downloadInfo.id, {\n            action: 'error',\n            data: {},\n          })\n        }\n        return\n      }\n      switch (response.statusCode) {\n        case 401:\n        case 403:\n        case 410:\n          sendAction(downloadInfo.id, { action: 'refreshUrl' })\n          // commit('onError', { downloadInfo, errorMsg: '链接失效' })\n          // refreshUrl.call(_this, commit, downloadInfo, rootState.setting.download.isUseOtherSource)\n          break\n        default:\n          void dls.get(downloadInfo.id)?.start()\n          console.log('正在重试')\n          // commit('setStatusText', { downloadInfo, text: '正在重试' })\n          break\n      }\n    },\n    onStart() {\n      sendAction(downloadInfo.id, { action: 'start' })\n      console.log('on start')\n    },\n    onProgress(status) {\n      downloadInfo.total = status.total\n      downloadInfo.downloaded = status.downloaded\n      downloadInfo.progress = status.progress\n      downloadInfo.speed = status.speed\n      downloadInfo.writeQueue = status.writeQueue\n      sendAction(downloadInfo.id, { action: 'progress', data: status })\n      // console.log(status)\n    },\n    onStop() {\n      console.log('on stop')\n      // sendAction(downloadInfo.id, { action: 'pause' })\n      // commit('pauseTask', downloadInfo)\n      // dispatch('startTask')\n    },\n  }\n  // commit('setStatusText', { downloadInfo, text: '获取URL中...' })\n\n  tryNum.set(downloadInfo.id, 0)\n  dls.set(downloadInfo.id, createDownload(downloadOptions))\n}\n\nexport const updateUrl = (id: string, url: string) => {\n  const task = tasks.get(id)\n  if (!task) return\n  task.metadata.url = url\n  // commit('setStatusText', { downloadInfo, text: '链接刷新成功' })\n  const dl = dls.get(id)\n  if (!dl) return\n  dl.refreshUrl(url)\n  dl.start().catch(err => {\n    sendAction(id, {\n      action: 'error',\n      data: {\n        message: err.message,\n      },\n    })\n  })\n}\n\nexport const startTask = async(downloadInfo: LX.Download.ListItem, savePath: string, skipExistFile: boolean, callback: (action: LX.Download.DownloadTaskActions) => void, proxy?: { host: string, port: number }) => {\n  await pauseTask(downloadInfo.id)\n\n  tasks.set(downloadInfo.id, downloadInfo)\n  taskActions.set(downloadInfo.id, callback)\n  // 检查是否可以开始任务\n  // if (!downloadInfo.isComplate && downloadInfo.status != DOWNLOAD_STATUS.RUN) {\n  //   const result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum)\n  //   if (result === false) {\n  //     commit('setStatus', { downloadInfo, status: DOWNLOAD_STATUS.WAITING })\n  //     return\n  //   }\n  // } else {\n  //   const result = getStartTask(downloadList, DOWNLOAD_STATUS, rootState.setting.download.maxDownloadNum)\n  //   if (!result) return\n  //   downloadInfo = result\n  // }\n  // commit('setStatus', { downloadInfo, status: DOWNLOAD_STATUS.RUN })\n\n  let dl = dls.get(downloadInfo.id)\n  if (dl) {\n    // commit('updateFilePath', {\n    //   downloadInfo,\n    //   filePath: path.join(rootState.setting.download.savePath, downloadInfo.metadata.fileName),\n    // })\n    dl.updateSaveInfo(savePath, downloadInfo.metadata.fileName)\n    if (tryNum.has(downloadInfo.id)) tryNum.set(downloadInfo.id, 0)\n    try {\n      await dl.start()\n    } catch (error) {\n      // commit('onError', { downloadInfo, errorMsg: error.message })\n      // commit('setStatusText', error.message)\n      // await dispatch('startTask')\n    }\n  } else {\n    await createTask(downloadInfo, savePath, skipExistFile, proxy)\n    // await dispatch('handleStartTask', downloadInfo)\n  }\n}\n\nexport const pauseTask = async(id: string) => {\n  const dl = dls.get(id)\n  if (dl) {\n    dls.delete(id)\n    tasks.delete(id)\n    taskActions.delete(id)\n    tryNum.delete(id)\n\n    try {\n      await dl.stop()\n    } catch (e) {\n      console.log(e)\n    }\n  }\n  // commit('setStatus', { downloadInfo: downloadInfo, status: DOWNLOAD_STATUS.PAUSE })\n}\n\nexport const removeTask = async(id: string) => {\n  const dl = dls.get(id)\n  const downloadInfo = tasks.get(id)\n  if (dl) {\n    dls.delete(id)\n    tasks.delete(id)\n    taskActions.delete(id)\n    tryNum.delete(id)\n\n    try {\n      await dl.stop()\n    } catch (e) {\n      console.log(e)\n    }\n  }\n\n  if (downloadInfo) {\n    // 没有未完成、已下载大于1k\n    if (!downloadInfo.isComplate && downloadInfo.total && downloadInfo.downloaded > 1024) {\n      try {\n        await removeFile(downloadInfo.metadata.filePath)\n      } catch (_) {}\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/worker/download/index.ts",
    "content": "import { exposeWorker } from '../utils/worker'\n\nimport * as common from './common'\nimport * as download from './download'\n\n\nconsole.log('hello download worker')\n\n\nexposeWorker(Object.assign({}, common, download))\n\nexport type workerDownloadTypes = typeof common &\n  typeof download\n"
  },
  {
    "path": "src/renderer/worker/download/lrcTool.ts",
    "content": "const timeFieldExp = /^(?:\\[[\\d:.]+\\])+/g\nconst timeExp = /\\d{1,3}(:\\d{1,3}){0,2}(?:\\.\\d{1,3})/g\n\nconst t_rxp_1 = /^0+(\\d+)/\nconst t_rxp_2 = /:0+(\\d+)/g\nconst t_rxp_3 = /\\.0+(\\d+)/\nconst formatTimeLabel = (label: string) => {\n  return label.replace(t_rxp_1, '$1')\n    .replace(t_rxp_2, ':$1')\n    .replace(t_rxp_3, '.$1')\n}\n\nconst filterExtendedLyricLabel = (lrcTimeLabels: Set<string>, extendedLyric: string) => {\n  const extendedLines = extendedLyric.split(/\\r\\n|\\n|\\r/)\n  const lines: string[] = []\n  for (let i = 0; i < extendedLines.length; i++) {\n    let line = extendedLines[i].trim()\n    let result = timeFieldExp.exec(line)\n    if (!result) continue\n\n    const timeField = result[0]\n    const text = line.replace(timeFieldExp, '').trim()\n    if (!text) continue\n    let times = timeField.match(timeExp)\n    if (times == null) continue\n\n    const newTimes = times.filter(time => {\n      const timeStr = formatTimeLabel(time)\n      return lrcTimeLabels.has(timeStr)\n    })\n    if (newTimes.length != times.length) {\n      if (!newTimes.length) continue\n      line = `[${newTimes.join('][')}]${text}`\n    }\n    lines.push(line)\n  }\n\n  return lines.join('\\n')\n}\n\nconst parseLrcTimeLabel = (lrc: string) => {\n  const lines = lrc.split(/\\r\\n|\\n|\\r/)\n  const linesSet = new Set<string>()\n  const length = lines.length\n  for (let i = 0; i < length; i++) {\n    const line = lines[i].trim()\n    let result = timeFieldExp.exec(line)\n    if (result) {\n      const timeField = result[0]\n      const text = line.replace(timeFieldExp, '').trim()\n      if (text) {\n        const times = timeField.match(timeExp)\n        if (times == null) continue\n        for (let time of times) {\n          linesSet.add(formatTimeLabel(time))\n        }\n      }\n    }\n  }\n\n  return linesSet\n}\n\nconst buildAwlyric = (lrcData: LX.Music.LyricInfo) => {\n  let lrc: string[] = []\n  if (lrcData.lyric) {\n    lrc.push(`lrc:${Buffer.from(lrcData.lyric.trim(), 'utf-8').toString('base64')}`)\n  }\n  if (lrcData.tlyric) {\n    lrc.push(`tlrc:${Buffer.from(lrcData.tlyric.trim(), 'utf-8').toString('base64')}`)\n  }\n  if (lrcData.rlyric) {\n    lrc.push(`rlrc:${Buffer.from(lrcData.rlyric.trim(), 'utf-8').toString('base64')}`)\n  }\n  if (lrcData.lxlyric) {\n    lrc.push(`awlrc:${Buffer.from(lrcData.lxlyric.trim(), 'utf-8').toString('base64')}`)\n  }\n  return lrc.length ? `[awlrc:${lrc.join(',')}]` : ''\n}\n\nexport const buildLyrics = (lrcData: LX.Music.LyricInfo, downloadAwlrc: boolean, downloadTlrc: boolean, downloadRlrc: boolean) => {\n  if (!lrcData.tlyric && !lrcData.rlyric && !lrcData.lxlyric) return lrcData.lyric\n\n  const lrcTimeLabels = parseLrcTimeLabel(lrcData.lyric)\n\n  let lrc = lrcData.lyric\n  if (downloadTlrc && lrcData.tlyric) {\n    lrc = lrc.trim() + `\\n\\n${filterExtendedLyricLabel(lrcTimeLabels, lrcData.tlyric)}\\n`\n  }\n  if (downloadRlrc && lrcData.rlyric) {\n    lrc = lrc.trim() + `\\n\\n${filterExtendedLyricLabel(lrcTimeLabels, lrcData.rlyric)}\\n`\n  }\n  if (downloadAwlrc) {\n    const awlrc = buildAwlyric(lrcData)\n    if (awlrc) lrc = lrc.trim() + `\\n\\n${awlrc}\\n`\n  }\n  return lrc\n}\n"
  },
  {
    "path": "src/renderer/worker/download/utils.ts",
    "content": "import { DOWNLOAD_STATUS, QUALITYS } from '@common/constants'\nimport { filterFileName } from '@common/utils/common'\nimport { buildLyrics } from './lrcTool'\nimport fs from 'fs'\nimport { clipFileNameLength, clipNameLength } from '@common/utils/tools'\n\n/**\n * 保存歌词文件\n */\nexport const saveLrc = async(lrcData: LX.Music.LyricInfo, info: {\n  filePath: string\n  format: LX.LyricFormat\n  downloadLxlrc: boolean\n  downloadTlrc: boolean\n  downloadRlrc: boolean\n}) => {\n  const iconv = (await import('iconv-lite')).default\n  const lrc = buildLyrics(lrcData, info.downloadLxlrc, info.downloadTlrc, info.downloadRlrc)\n  switch (info.format) {\n    case 'gbk':\n      fs.writeFile(info.filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {\n        if (err) console.log(err)\n      })\n      break\n    case 'utf8':\n    default:\n      fs.writeFile(info.filePath, iconv.encode(lrc, 'utf8', { addBOM: true }), err => {\n        if (err) console.log(err)\n      })\n      break\n  }\n}\n\nexport const getExt = (type: string): LX.Download.FileExt => {\n  switch (type) {\n    case 'ape':\n      return 'ape'\n    case 'flac':\n    case 'flac24bit':\n      return 'flac'\n    case 'wav':\n      return 'wav'\n    case '128k':\n    case '192k':\n    case '320k':\n    default:\n      return 'mp3'\n  }\n}\n\n/**\n * 获取音乐音质\n * @param musicInfo\n * @param type\n * @param qualityList\n */\nexport const getMusicType = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quality, qualityList: LX.QualityList): LX.Quality => {\n  let list = qualityList[musicInfo.source]\n  if (!list) return '128k'\n  if (!list.includes(type)) type = list[list.length - 1]\n  const rangeType = QUALITYS.slice(QUALITYS.indexOf(type))\n  for (const type of rangeType) {\n    if (musicInfo.meta._qualitys[type]) return type\n  }\n  return '128k'\n}\n\n// const checkExistList = (list: LX.Download.ListItem[], musicInfo: LX.Music.MusicInfo, type: LX.Quality, ext: string): boolean => {\n//   return list.some(s => s.id === musicInfo.id && (s.metadata.type === type || s.metadata.ext === ext))\n// }\n\nexport const createDownloadInfo = (musicInfo: LX.Music.MusicInfoOnline, type: LX.Quality, fileName: string, qualityList: LX.QualityList, listId?: string) => {\n  type = getMusicType(musicInfo, type, qualityList)\n  let ext = getExt(type)\n  const key = `${musicInfo.id}_${type}_${ext}`\n  // if (checkExistList(list, musicInfo, type, ext)) return null\n  const downloadInfo: LX.Download.ListItem = {\n    id: key,\n    isComplate: false,\n    status: DOWNLOAD_STATUS.WAITING,\n    statusText: '待下载',\n    downloaded: 0,\n    total: 0,\n    progress: 0,\n    speed: '',\n    writeQueue: 0,\n    metadata: {\n      musicInfo,\n      url: null,\n      quality: type,\n      ext,\n      filePath: '',\n      listId,\n      fileName: filterFileName(`${clipFileNameLength(fileName\n        .replace('歌名', musicInfo.name)\n        .replace('歌手', clipNameLength(musicInfo.singer)))}.${ext}`),\n    },\n  }\n  // downloadInfo.metadata.filePath = joinPath(savePath, downloadInfo.metadata.fileName)\n  // commit('addTask', downloadInfo)\n\n  // 删除同路径下的同名文件\n  // TODO\n  // void removeFile(downloadInfo.metadata.filePath)\n  // .catch(err => {\n  //   if (err.code !== 'ENOENT') {\n  //     return commit('setStatusText', { downloadInfo, text: '文件删除失败' })\n  //   }\n  // })\n\n  return downloadInfo\n}\n"
  },
  {
    "path": "src/renderer/worker/index.ts",
    "content": "import { createMainWorker, createDownloadWorker } from './utils'\n\n\nexport default () => {\n  return {\n    main: createMainWorker(),\n    download: createDownloadWorker(),\n  }\n}\n\n"
  },
  {
    "path": "src/renderer/worker/main/common.ts",
    "content": "import { tranditionalize } from '@renderer/utils/simplify-chinese-main'\n\nexport const langS2t = (textBase64: string): string => {\n  const text = tranditionalize(Buffer.from(textBase64, 'base64').toString())\n  return Buffer.from(text).toString('base64')\n}\n\nexport {\n  saveLxConfigFile,\n  readLxConfigFile,\n  saveStrToFile,\n} from '@common/utils/nodejs'\n"
  },
  {
    "path": "src/renderer/worker/main/index.ts",
    "content": "import { exposeWorker } from '../utils/worker'\n\nimport * as common from './common'\nimport * as list from './list'\nimport * as music from './music'\n\n\nconsole.log('hello main worker')\n\n\nexposeWorker(Object.assign({}, common, list, music))\n\nexport type workerMainTypes = typeof common\n  & typeof list\n  & typeof music\n"
  },
  {
    "path": "src/renderer/worker/main/list.ts",
    "content": "// import { throttle } from '@common/utils'\n\nimport { SPLIT_CHAR } from '@common/constants'\nimport { filterFileName, sortInsert, similar, arrPushByPosition, arrShuffle } from '@common/utils/common'\nimport { joinPath, saveStrToFile } from '@common/utils/nodejs'\nimport { createLocalMusicInfo } from '@renderer/utils/music'\n\n\n/**\n * 过滤列表中已播放的歌曲\n */\nexport const filterMusicList = async({ playedList, listId, list, playerMusicInfo, dislikeInfo, isNext }: {\n  /**\n   * 已播放列表\n   */\n  playedList: LX.Player.PlayMusicInfo[]\n  /**\n   * 列表id\n   */\n  listId: string\n  /**\n   * 播放列表\n   */\n  list: Array<LX.Music.MusicInfo | LX.Download.ListItem>\n  /**\n   * 下载目录\n   */\n  // savePath: string\n  /**\n   * 播放器内当前歌曲（`playInfo.playerPlayIndex`指向的歌曲）\n   */\n  playerMusicInfo?: LX.Music.MusicInfo | LX.Download.ListItem\n  /**\n   * 不喜欢的歌曲名字列表\n   */\n  dislikeInfo: Omit<LX.Dislike.DislikeInfo, 'rules'>\n\n  isNext: boolean\n}) => {\n  let playerIndex = -1\n\n  let canPlayList: Array<LX.Music.MusicInfo | LX.Download.ListItem> = []\n  const filteredPlayedList = playedList.filter(pmInfo => pmInfo.listId == listId && !pmInfo.isTempPlay).map(({ musicInfo }) => musicInfo)\n  const hasDislike = (info: LX.Music.MusicInfo) => {\n    const name = info.name?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? ''\n    const singer = info.singer?.replaceAll(SPLIT_CHAR.DISLIKE_NAME, SPLIT_CHAR.DISLIKE_NAME_ALIAS).toLocaleLowerCase().trim() ?? ''\n\n    return dislikeInfo.musicNames.has(name) || dislikeInfo.singerNames.has(singer) ||\n      dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n  }\n\n  let isDislike = false\n  const filteredList: Array<LX.Music.MusicInfo | LX.Download.ListItem> = list.filter(s => {\n    // if (!assertApiSupport(s.source)) return false\n    if ('progress' in s) {\n      if (!s.isComplate) return false\n    } else if (hasDislike(s)) {\n      if (s.id != playerMusicInfo?.id) return false\n      isDislike = true\n    }\n\n    canPlayList.push(s)\n\n    let index = filteredPlayedList.findIndex(m => m.id == s.id)\n    if (index > -1) {\n      filteredPlayedList.splice(index, 1)\n      return false\n    }\n    return true\n  })\n  if (playerMusicInfo) {\n    if (isDislike) {\n      if (filteredList.length <= 1) {\n        filteredList.splice(0, 1)\n        if (canPlayList.length > 1) {\n          let currentMusicIndex = canPlayList.findIndex(m => m.id == playerMusicInfo.id)\n          if (isNext) {\n            playerIndex = currentMusicIndex - 1\n            if (playerIndex < 0 && canPlayList.length > 1) playerIndex = canPlayList.length - 2\n          } else {\n            playerIndex = currentMusicIndex\n            if (canPlayList.length <= 1) playerIndex = -1\n          }\n          canPlayList.splice(currentMusicIndex, 1)\n        } else canPlayList.splice(0, 1)\n      } else {\n        let currentMusicIndex = filteredList.findIndex(m => m.id == playerMusicInfo.id)\n        if (isNext) {\n          playerIndex = currentMusicIndex - 1\n          if (playerIndex < 0 && filteredList.length > 1) playerIndex = filteredList.length - 2\n        } else {\n          playerIndex = currentMusicIndex\n          if (filteredList.length <= 1) playerIndex = -1\n        }\n        filteredList.splice(currentMusicIndex, 1)\n      }\n    } else {\n      playerIndex = (filteredList.length ? filteredList : canPlayList).findIndex(m => m.id == playerMusicInfo.id)\n    }\n  }\n  return {\n    filteredList,\n    canPlayList,\n    playerIndex,\n  }\n}\n\nconst getIntv = (musicInfo: LX.Music.MusicInfo) => {\n  if (!musicInfo.interval) return 0\n  // if (musicInfo._interval) return musicInfo._interval\n  let intvArr = musicInfo.interval.split(':')\n  let intv = 0\n  let unit = 1\n  while (intvArr.length) {\n    intv += parseInt(intvArr.pop()!) * unit\n    unit *= 60\n  }\n  return intv\n}\n\nexport type SortFieldName = 'name' | 'singer' | 'albumName' | 'interval' | 'source'\nexport type SortFieldType = 'up' | 'down' | 'random'\n/**\n * 排序歌曲\n * @param list 歌曲列表\n * @param sortType 排序类型\n * @param fieldName 排序字段\n * @param localeId 排序语言\n * @returns\n */\nexport const sortListMusicInfo = async(list: LX.Music.MusicInfo[], sortType: SortFieldType, fieldName: SortFieldName, localeId: string) => {\n  // console.log(sortType, fieldName, localeId)\n  // const locale = new Intl.Locale(localeId)\n  switch (sortType) {\n    case 'random':\n      arrShuffle(list)\n      break\n    case 'up':\n      if (fieldName == 'interval') {\n        list.sort((a, b) => {\n          if (a.interval == null) {\n            return b.interval == null ? 0 : -1\n          } else return b.interval == null ? 1 : getIntv(a) - getIntv(b)\n        })\n      } else {\n        switch (fieldName) {\n          case 'name':\n          case 'singer':\n          case 'source':\n            list.sort((a, b) => {\n              if (a[fieldName] == null) {\n                return b[fieldName] == null ? 0 : -1\n              } else return b[fieldName] == null ? 1 : a[fieldName].localeCompare(b[fieldName], localeId)\n            })\n            break\n          case 'albumName':\n            list.sort((a, b) => {\n              if (a.meta.albumName == null) {\n                return b.meta.albumName == null ? 0 : -1\n              } else return b.meta.albumName == null ? 1 : a.meta.albumName.localeCompare(b.meta.albumName, localeId)\n            })\n            break\n        }\n      }\n      break\n    case 'down':\n      if (fieldName == 'interval') {\n        list.sort((a, b) => {\n          if (a.interval == null) {\n            return b.interval == null ? 0 : 1\n          } else return b.interval == null ? -1 : getIntv(b) - getIntv(a)\n        })\n      } else {\n        switch (fieldName) {\n          case 'name':\n          case 'singer':\n          case 'source':\n            list.sort((a, b) => {\n              if (a[fieldName] == null) {\n                return b[fieldName] == null ? 0 : 1\n              } else return b[fieldName] == null ? -1 : b[fieldName].localeCompare(a[fieldName], localeId)\n            })\n            break\n          case 'albumName':\n            list.sort((a, b) => {\n              if (a.meta.albumName == null) {\n                return b.meta.albumName == null ? 0 : 1\n              } else return b.meta.albumName == null ? -1 : b.meta.albumName.localeCompare(a.meta.albumName, localeId)\n            })\n            break\n        }\n      }\n      break\n  }\n  return list\n}\n\nconst variantRxp = /(\\(|（).+(\\)|）)/g\nconst variantRxp2 = /\\s|'|\\.|,|，|&|\"|、|\\(|\\)|（|）|`|~|-|<|>|\\||\\/|\\]|\\[/g\n/**\n * 过滤列表内重复的歌曲\n * @param list 歌曲列表\n * @param isFilterVariant 是否过滤 Live Explicit 等歌曲名\n * @returns\n */\nexport const filterDuplicateMusic = async(list: LX.Music.MusicInfo[], isFilterVariant: boolean = true) => {\n  type ListMapValue = Array<{ id: string, index: number, musicInfo: LX.Music.MusicInfo }>\n  const listMap = new Map<string, ListMapValue>()\n  const duplicateList = new Set<string>()\n  const handleFilter = (name: string, index: number, musicInfo: LX.Music.MusicInfo) => {\n    if (listMap.has(name)) {\n      const targetMusicInfo = listMap.get(name)\n      targetMusicInfo!.push({\n        id: musicInfo.id,\n        index,\n        musicInfo,\n      })\n      duplicateList.add(name)\n    } else {\n      listMap.set(name, [{\n        id: musicInfo.id,\n        index,\n        musicInfo,\n      }])\n    }\n  }\n  if (isFilterVariant) {\n    list.forEach((musicInfo, index) => {\n      let musicInfoName = musicInfo.name.toLowerCase().replace(variantRxp, '').replace(variantRxp2, '')\n      musicInfoName ||= musicInfo.name.toLowerCase().replace(/\\s+/g, '')\n      handleFilter(musicInfoName, index, musicInfo)\n    })\n  } else {\n    list.forEach((musicInfo, index) => {\n      const musicInfoName = musicInfo.name.toLowerCase().trim()\n      handleFilter(musicInfoName, index, musicInfo)\n    })\n  }\n  // console.log(duplicateList)\n  const duplicateNames = Array.from(duplicateList)\n  duplicateNames.sort((a, b) => a.localeCompare(b))\n  return duplicateNames.map(name => listMap.get(name)!).flat()\n}\n\nexport const searchListMusic = (list: LX.Music.MusicInfo[], text: string) => {\n  let result: LX.Music.MusicInfo[] = []\n  let rxp = new RegExp(text.split('').map(s => s.replace(/[.*+?^${}()|[\\]\\\\]/, '\\\\$&')).join('.*') + '.*', 'i')\n  for (const mInfo of list) {\n    const str = `${mInfo.name}${mInfo.singer}${mInfo.meta.albumName ? mInfo.meta.albumName : ''}`\n    if (rxp.test(str)) result.push(mInfo)\n  }\n\n  const sortedList: Array<{ num: number, data: LX.Music.MusicInfo }> = []\n\n  for (const mInfo of result) {\n    sortInsert(sortedList, {\n      num: similar(text, `${mInfo.name}${mInfo.singer}${mInfo.meta.albumName ? mInfo.meta.albumName : ''}`),\n      data: mInfo,\n    })\n  }\n  return sortedList.map(item => item.data).reverse()\n}\n\n/**\n * 创建排序后的列表\n * @param list 原始列表\n * @param position 新位置\n * @param ids 要调整顺序的歌曲id\n * @returns\n */\nexport const createSortedList = (list: LX.Music.MusicInfo[], position: number, ids: string[]) => {\n  const infos: LX.Music.MusicInfo[] = []\n  const map = new Map<string, LX.Music.MusicInfo>()\n  for (const item of list) map.set(item.id, item)\n  for (const id of ids) {\n    infos.push(map.get(id)!)\n    map.delete(id)\n  }\n  list = list.filter(mInfo => map.has(mInfo.id))\n  arrPushByPosition(list, infos, Math.min(position, list.length))\n  return list\n}\n\n\n/**\n * 创建本地列表音乐信息\n * @param filePaths 文件路径\n */\nexport const createLocalMusicInfos = async(filePaths: string[]): Promise<LX.Music.MusicInfoLocal[]> => {\n  const list: LX.Music.MusicInfoLocal[] = []\n  for await (const path of filePaths) {\n    const musicInfo = await createLocalMusicInfo(path)\n    if (!musicInfo) continue\n    list.push(musicInfo)\n  }\n\n  return list\n}\n\n/**\n * 导出列表到txt文件\n * @param savePath 保存路径\n * @param lists 列表数据\n * @param isMerge 是否合并\n */\nexport const exportPlayListToText = async(savePath: string, lists: Array<LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull>, isMerge: boolean) => {\n  const iconv = (await import('iconv-lite')).default\n\n  if (isMerge) {\n    await saveStrToFile(savePath,\n      iconv.encode(lists.map(l => l.list.map(m => `${m.name}  ${m.singer}  ${m.meta.albumName ?? ''}`).join('\\n')).join('\\n\\n'), 'utf8', { addBOM: true }))\n  } else {\n    for await (const list of lists) {\n      await saveStrToFile(joinPath(savePath, `lx_list_${filterFileName(list.name)}.txt`),\n        iconv.encode(list.list.map(m => `${m.name}  ${m.singer}  ${m.meta.albumName ?? ''}`).join('\\n'), 'utf8', { addBOM: true }))\n    }\n  }\n}\n\n/**\n * 导出列表到csv文件\n * @param savePath 保存路径\n * @param lists 列表数据\n * @param isMerge 是否合并\n * @param header 表头名称\n */\nexport const exportPlayListToCSV = async(savePath: string,\n  lists: Array<LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull>,\n  isMerge: boolean,\n  header: string) => {\n  const iconv = (await import('iconv-lite')).default\n\n  const filterStr = (str: string) => {\n    if (!str) return ''\n    str = str.replace(/\"/g, '\"\"')\n    if (str.includes(',')) str = `\"${str}\"`\n    return str\n  }\n\n  if (isMerge) {\n    await saveStrToFile(savePath, iconv.encode(header + lists.map(l => l.list.map(m => `${filterStr(m.name)},${filterStr(m.singer)},${filterStr(m.meta.albumName ?? '')}`).join('\\n')).join('\\n'), 'utf8', { addBOM: true }))\n  } else {\n    for await (const list of lists) {\n      await saveStrToFile(joinPath(savePath, `lx_list_${filterFileName(list.name)}.csv`), iconv.encode(header + list.list.map(m => `${filterStr(m.name)},${filterStr(m.singer)},${filterStr(m.meta.albumName ?? '')}`).join('\\n'), 'utf8', { addBOM: true }))\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/worker/main/music.ts",
    "content": "import { getLocalMusicFileLyric, getLocalMusicFilePic } from '@renderer/utils/music'\nimport path from 'node:path'\nimport os from 'node:os'\nimport fs from 'node:fs/promises'\nimport { checkPath } from '@common/utils/nodejs'\n\nconst getTempDir = async() => {\n  const tempDir = path.join(os.tmpdir(), 'lxmusic_temp')\n  if (!await checkPath(tempDir)) {\n    await fs.mkdir(tempDir, { recursive: true })\n  }\n  return tempDir\n}\n\nexport const getMusicFilePic = async(filePath: string) => {\n  const picture = await getLocalMusicFilePic(filePath)\n  if (!picture) return ''\n  if (typeof picture == 'string') return picture\n  if (picture.data.length > 400_000) {\n    try {\n      const tempDir = await getTempDir()\n      const tempFile = path.join(tempDir, path.basename(filePath) + '.' + picture.format.split('/')[1])\n      await fs.writeFile(tempFile, picture.data)\n      return tempFile\n    } catch (err) {\n      console.log(err)\n    }\n  }\n  return `data:${picture.format};base64,${Buffer.from(picture.data).toString('base64')}`\n}\n\nexport const parseLyric = (lrc: string): LX.Music.LyricInfo => {\n  const verifyAwlrc = (lrc: string) => {\n    return /(?:^|\\s*)\\[\\d+:\\d+(?:\\.\\d+)]<\\d+,\\d+>.+$/m.test(lrc)\n  }\n  const verifylrc = (lrc: string) => {\n    return /(?:^|\\s*)\\[\\d+:\\d+(?:\\.\\d+)].+$/m.test(lrc)\n  }\n  const lrcTags = {\n    awlrc: {\n      name: 'lxlyric',\n      verify: verifyAwlrc,\n    },\n    lrc: {\n      name: 'lyric',\n      verify: verifylrc,\n    },\n    tlrc: {\n      name: 'tlyric',\n      verify: verifylrc,\n    },\n    rlrc: {\n      name: 'rlyric',\n      verify: verifylrc,\n    },\n  } as const\n  const tagRxp = /(?:^|\\n\\s*)\\[awlrc:([^\\]]+)]/i\n  const lrcRxp = /^(lrc|awlrc|tlrc|rlrc):([^,]+)$/i\n  const parse = (content: string) => {\n    const lyricInfo: Partial<LX.Music.LyricInfo> = {}\n    const lrcs = content.trim().split(',')\n    for (const lrc of lrcs) {\n      const result = lrcRxp.exec(lrc.trim())\n      if (!result) continue\n      const target = lrcTags[result[1].toLowerCase() as 'tlrc' | 'rlrc' | 'lrc' | 'awlrc']\n      if (!target) continue\n      const data = Buffer.from(result[2], 'base64').toString('utf-8').trim()\n      if (target.verify(data)) lyricInfo[target.name] = data\n    }\n    return lyricInfo\n  }\n  let parsedInfo: Partial<LX.Music.LyricInfo> = {}\n  let lyric = lrc.replace(tagRxp, (_: string, p1: string) => {\n    parsedInfo = parse(p1)\n    return ''\n  }).trim()\n  return { lyric, ...parsedInfo }\n}\n\n\nexport const getMusicFileLyric = async(filePath: string): Promise<LX.Music.LyricInfo | null> => {\n  const lyric = await getLocalMusicFileLyric(filePath)\n  if (!lyric) return null\n  return parseLyric(lyric.lyric)\n}\n"
  },
  {
    "path": "src/renderer/worker/utils/index.ts",
    "content": "import * as Comlink from 'comlink'\n\nexport type MainTypes = Comlink.Remote<LX.WorkerMainTypes>\n\nexport const createMainWorker = () => {\n  const worker: Worker = new Worker(new URL(\n    /* webpackChunkName: 'renderer.main.worker' */\n    '../main',\n    import.meta.url,\n  ))\n  return Comlink.wrap<LX.WorkerMainTypes>(worker)\n}\n\n// export const createWorker = <T>(url: string): Comlink.Remote<T> => {\n//   // @ts-expect-error\n//   const worker: Worker = new Worker(new URL(url, import.meta.url))\n//   return Comlink.wrap<T>(worker)\n//   // worker.addEventListener('message', (event: MessageEvent) => {\n\n//   // })\n// }\n\nexport type DownloadTypes = Comlink.Remote<LX.WorkerDownloadTypes>\nexport const createDownloadWorker = () => {\n  const worker: Worker = new Worker(new URL(\n    /* webpackChunkName: 'renderer.download.worker' */\n    '../download',\n    import.meta.url,\n  ))\n  return Comlink.wrap<LX.WorkerDownloadTypes>(worker)\n}\n\nexport const proxyCallback = <Args extends any[]>(callback: (...T: Args) => void) => {\n  return Comlink.proxy(callback)\n}\n"
  },
  {
    "path": "src/renderer/worker/utils/worker.ts",
    "content": "import * as Comlink from 'comlink'\n\n\nexport const exposeWorker = (obj: any) => {\n  Comlink.expose(obj)\n}\n"
  },
  {
    "path": "src/renderer-lyric/.eslintrc.cjs",
    "content": "/* eslint-env node */\nconst { base, html, typescript, vue } = require('../../.eslintrc.base.cjs')\n\nmodule.exports = {\n  root: true,\n  ...base,\n  overrides: [\n    html,\n    vue,\n    {\n      ...typescript,\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": "src/renderer-lyric/App.vue",
    "content": "<template>\n  <div id=\"container\" :class=\"[{ lock: setting['desktopLyric.isLock'] }, { hide: isHide || (isHoverHide && isMouseEnter) }]\">\n    <div id=\"main\" @mouseenter=\"handleMouseEnter\" @mouseleave=\"handleMouseLeave\" @mousemove=\"handleMouseMoveMain\">\n      <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated-fast fadeOut\">\n        <div v-show=\"!setting['desktopLyric.isLock']\" class=\"control-bar\">\n          <layout-control-bar />\n        </div>\n      </transition>\n      <layout-lyric-vertical v-if=\"setting['desktopLyric.direction'] == 'vertical'\" />\n      <layout-lyric-horizontal v-else />\n      <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated-fast fadeOut\">\n        <common-audio-visualizer v-if=\"setting['desktopLyric.audioVisualization']\" />\n      </transition>\n    </div>\n    <template v-if=\"isShowResize\">\n      <div class=\"resize resize-left\" @mousedown.self=\"handleMouseDown('left', $event)\" @touchstart.self=\"handleTouchDown('left', $event)\" />\n      <div class=\"resize resize-top\" @mousedown.self=\"handleMouseDown('top', $event)\" @touchstart.self=\"handleTouchDown('top', $event)\" />\n      <div class=\"resize resize-right\" @mousedown.self=\"handleMouseDown('right', $event)\" @touchstart.self=\"handleTouchDown('right', $event)\" />\n      <div class=\"resize resize-bottom\" @mousedown.self=\"handleMouseDown('bottom', $event)\" @touchstart.self=\"handleTouchDown('bottom', $event)\" />\n      <div class=\"resize resize-top-left\" @mousedown.self=\"handleMouseDown('top-left', $event)\" @touchstart.self=\"handleTouchDown('top-left', $event)\" />\n      <div class=\"resize resize-top-right\" @mousedown.self=\"handleMouseDown('top-right', $event)\" @touchstart.self=\"handleTouchDown('top-right', $event)\" />\n      <div class=\"resize resize-bottom-left\" @mousedown.self=\"handleMouseDown('bottom-left', $event)\" @touchstart.self=\"handleTouchDown('bottom-left', $event)\" />\n      <div class=\"resize resize-bottom-right\" @mousedown.self=\"handleMouseDown('bottom-right', $event)\" @touchstart.self=\"handleTouchDown('bottom-right', $event)\" />\n    </template>\n    <layout-icons />\n  </div>\n</template>\n\n<script setup>\nimport useWindowSize from '@lyric/useApp/useWindowSize'\nimport useHoverHide from '@lyric/useApp/useHoverHide'\nimport { onMounted } from '@common/utils/vueTools'\nimport { setting } from '@lyric/store/state'\nimport { sendConnectMainWindowEvent } from '@lyric/utils/ipc'\nimport useCommon from '@lyric/useApp/useCommon'\nimport useLyric from '@lyric/useApp/useLyric'\nimport useTheme from '@lyric/useApp/useTheme'\nimport { init as initLyricPlayer } from '@lyric/core/lyric'\nimport usePauseHide from '@lyric/useApp/usePauseHide'\n\nconst isShowResize = window.os != 'windows'\nuseCommon()\nconst { handleMouseDown, handleTouchDown } = useWindowSize()\nconst { handleMouseMoveMain, isHoverHide, isMouseEnter } = useHoverHide()\nuseLyric()\nuseTheme()\nconst isHide = usePauseHide()\n\n\nonMounted(() => {\n  initLyricPlayer()\n  sendConnectMainWindowEvent()\n})\n\n</script>\n\n<style lang=\"less\">\n@import './assets/styles/index.less';\n@import './assets/styles/layout.less';\n\nbody {\n  user-select: none;\n  height: 100vh;\n  box-sizing: border-box;\n  color: #fff;\n  opacity: .8;\n}\n\nbody {\n  user-select: none;\n  height: 100vh;\n  box-sizing: border-box;\n}\n#root {\n  height: 100%;\n}\n\n#container {\n  box-sizing: border-box;\n  height: 100%;\n  transition: opacity .3s ease;\n  opacity: 1;\n  &.lock {\n    #main {\n      background-color: transparent;\n    }\n  }\n  &.hide {\n    opacity: .05;\n    &:hover {\n      opacity: 1;\n    }\n  }\n}\n\n@resize-width: 6px;\n.resize {\n  z-index: 2;\n}\n.resize-left {\n  position: absolute;\n  left: 0;\n  top: 0;\n  height: 100%;\n  width: @resize-width;\n  cursor: ew-resize;\n  // background-color: rgba(0, 0, 0, 1);\n}\n.resize-right {\n  position: absolute;\n  right: 0;\n  top: 0;\n  height: 100%;\n  width: @resize-width;\n  cursor: ew-resize;\n}\n.resize-top {\n  position: absolute;\n  left: 0;\n  top: 0;\n  height: 4px;\n  width: 100%;\n  cursor: ns-resize;\n}\n.resize-bottom {\n  position: absolute;\n  left: 0;\n  bottom: 0;\n  height: @resize-width;\n  width: 100%;\n  cursor: ns-resize;\n}\n.resize-top-left {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: @resize-width;\n  height: @resize-width;\n  cursor: nwse-resize;\n  // background-color: rgba(0, 0, 0, 1);\n}\n.resize-top-right {\n  position: absolute;\n  right: 0;\n  top: 0;\n  width: @resize-width;\n  height: @resize-width;\n  cursor: nesw-resize;\n  // background-color: rgba(0, 0, 0, 1);\n}\n.resize-bottom-left {\n  position: absolute;\n  left: 0;\n  bottom: 0;\n  width: @resize-width;\n  height: @resize-width;\n  cursor: nesw-resize;\n  // background-color: rgba(0, 0, 0, 1);\n}\n.resize-bottom-right {\n  position: absolute;\n  right: 0;\n  bottom: 0;\n  width: @resize-width;\n  height: @resize-width;\n  cursor: nwse-resize;\n  // background-color: rgba(0, 0, 0, 1);\n}\n\n#main {\n  position: relative;\n  box-sizing: border-box;\n  height: 100%;\n  transition: background-color @transition-theme;\n  min-height: 0;\n  border-radius: @radius-border;\n  overflow: hidden;\n  background-color: rgba(0, 0, 0, .2);\n\n  &:hover {\n    .control-bar {\n      opacity: 1;\n    }\n  }\n}\n\n.control-bar {\n  position: absolute;\n  border-top-left-radius: @radius-border;\n  border-top-right-radius: @radius-border;\n  overflow: hidden;\n  top: 0;\n  left: 0;\n  width: 100%;\n  opacity: 0;\n  transition: opacity @transition-theme;\n  z-index: 1;\n}\n</style>\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/animate.less",
    "content": "// https://daneden.github.io/animate.css/\n\n@keyframes bounce {\n  from,\n  20%,\n  53%,\n  80%,\n  to {\n    -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  40%,\n  43% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -30px, 0);\n    transform: translate3d(0, -30px, 0);\n  }\n\n  70% {\n    -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);\n    -webkit-transform: translate3d(0, -15px, 0);\n    transform: translate3d(0, -15px, 0);\n  }\n\n  90% {\n    -webkit-transform: translate3d(0, -4px, 0);\n    transform: translate3d(0, -4px, 0);\n  }\n}\n@keyframes flash {\n  from,\n  50%,\n  to {\n    opacity: 1;\n  }\n\n  25%,\n  75% {\n    opacity: 0;\n  }\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@keyframes pulse {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.05, 1.05, 1.05);\n    transform: scale3d(1.05, 1.05, 1.05);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes rubberBand {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  30% {\n    -webkit-transform: scale3d(1.25, 0.75, 1);\n    transform: scale3d(1.25, 0.75, 1);\n  }\n\n  40% {\n    -webkit-transform: scale3d(0.75, 1.25, 1);\n    transform: scale3d(0.75, 1.25, 1);\n  }\n\n  50% {\n    -webkit-transform: scale3d(1.15, 0.85, 1);\n    transform: scale3d(1.15, 0.85, 1);\n  }\n\n  65% {\n    -webkit-transform: scale3d(0.95, 1.05, 1);\n    transform: scale3d(0.95, 1.05, 1);\n  }\n\n  75% {\n    -webkit-transform: scale3d(1.05, 0.95, 1);\n    transform: scale3d(1.05, 0.95, 1);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes shake {\n  from,\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  10%,\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: translate3d(-10px, 0, 0);\n    transform: translate3d(-10px, 0, 0);\n  }\n\n  20%,\n  40%,\n  60%,\n  80% {\n    -webkit-transform: translate3d(10px, 0, 0);\n    transform: translate3d(10px, 0, 0);\n  }\n}\n@keyframes headShake {\n  0% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n\n  6.5% {\n    -webkit-transform: translateX(-6px) rotateY(-9deg);\n    transform: translateX(-6px) rotateY(-9deg);\n  }\n\n  18.5% {\n    -webkit-transform: translateX(5px) rotateY(7deg);\n    transform: translateX(5px) rotateY(7deg);\n  }\n\n  31.5% {\n    -webkit-transform: translateX(-3px) rotateY(-5deg);\n    transform: translateX(-3px) rotateY(-5deg);\n  }\n\n  43.5% {\n    -webkit-transform: translateX(2px) rotateY(3deg);\n    transform: translateX(2px) rotateY(3deg);\n  }\n\n  50% {\n    -webkit-transform: translateX(0);\n    transform: translateX(0);\n  }\n}\n@keyframes swing {\n  20% {\n    -webkit-transform: rotate3d(0, 0, 1, 15deg);\n    transform: rotate3d(0, 0, 1, 15deg);\n  }\n\n  40% {\n    -webkit-transform: rotate3d(0, 0, 1, -10deg);\n    transform: rotate3d(0, 0, 1, -10deg);\n  }\n\n  60% {\n    -webkit-transform: rotate3d(0, 0, 1, 5deg);\n    transform: rotate3d(0, 0, 1, 5deg);\n  }\n\n  80% {\n    -webkit-transform: rotate3d(0, 0, 1, -5deg);\n    transform: rotate3d(0, 0, 1, -5deg);\n  }\n\n  to {\n    -webkit-transform: rotate3d(0, 0, 1, 0deg);\n    transform: rotate3d(0, 0, 1, 0deg);\n  }\n}\n@keyframes tada {\n  from {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n\n  10%,\n  20% {\n    -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);\n  }\n\n  30%,\n  50%,\n  70%,\n  90% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);\n  }\n\n  40%,\n  60%,\n  80% {\n    -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n    transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);\n  }\n\n  to {\n    -webkit-transform: scale3d(1, 1, 1);\n    transform: scale3d(1, 1, 1);\n  }\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n@keyframes wobble {\n  from {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  15% {\n    -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n    transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg);\n  }\n\n  30% {\n    -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n    transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg);\n  }\n\n  45% {\n    -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n    transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg);\n  }\n\n  60% {\n    -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n    transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg);\n  }\n\n  75% {\n    -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n    transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg);\n  }\n\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes jello {\n  from,\n  11.1%,\n  to {\n    -webkit-transform: translate3d(0, 0, 0);\n    transform: translate3d(0, 0, 0);\n  }\n\n  22.2% {\n    -webkit-transform: skewX(-12.5deg) skewY(-12.5deg);\n    transform: skewX(-12.5deg) skewY(-12.5deg);\n  }\n\n  33.3% {\n    -webkit-transform: skewX(6.25deg) skewY(6.25deg);\n    transform: skewX(6.25deg) skewY(6.25deg);\n  }\n\n  44.4% {\n    -webkit-transform: skewX(-3.125deg) skewY(-3.125deg);\n    transform: skewX(-3.125deg) skewY(-3.125deg);\n  }\n\n  55.5% {\n    -webkit-transform: skewX(1.5625deg) skewY(1.5625deg);\n    transform: skewX(1.5625deg) skewY(1.5625deg);\n  }\n\n  66.6% {\n    -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg);\n    transform: skewX(-0.78125deg) skewY(-0.78125deg);\n  }\n\n  77.7% {\n    -webkit-transform: skewX(0.390625deg) skewY(0.390625deg);\n    transform: skewX(0.390625deg) skewY(0.390625deg);\n  }\n\n  88.8% {\n    -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg);\n    transform: skewX(-0.1953125deg) skewY(-0.1953125deg);\n  }\n}\n@keyframes heartBeat {\n  0% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n\n  14% {\n    -webkit-transform: scale(1.3);\n    transform: scale(1.3);\n  }\n\n  28% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n\n  42% {\n    -webkit-transform: scale(1.3);\n    transform: scale(1.3);\n  }\n\n  70% {\n    -webkit-transform: scale(1);\n    transform: scale(1);\n  }\n}\n\n@keyframes flipInX {\n  from {\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n\n  to {\n    transform: perspective(400px);\n  }\n}\n@keyframes flipOutX {\n  from {\n    transform: perspective(400px);\n  }\n\n  30% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    opacity: 1;\n  }\n\n  to {\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes flipInY {\n  from {\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    animation-timing-function: ease-in;\n    opacity: 0;\n  }\n\n  40% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -20deg);\n    animation-timing-function: ease-in;\n  }\n\n  60% {\n    transform: perspective(400px) rotate3d(0, 1, 0, 10deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -5deg);\n  }\n\n  to {\n    transform: perspective(400px);\n  }\n}\n@keyframes flipOutY {\n  from {\n    transform: perspective(400px);\n  }\n\n  30% {\n    transform: perspective(400px) rotate3d(0, 1, 0, -15deg);\n    opacity: 1;\n  }\n\n  to {\n    transform: perspective(400px) rotate3d(0, 1, 0, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n@keyframes fadeOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n@keyframes bounceIn {\n  from,\n  20%,\n  40%,\n  60%,\n  80%,\n  to {\n    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);\n  }\n\n  0% {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  20% {\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  40% {\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(1.03, 1.03, 1.03);\n  }\n\n  80% {\n    transform: scale3d(0.97, 0.97, 0.97);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale3d(1, 1, 1);\n  }\n}\n@keyframes bounceOut {\n  20% {\n    transform: scale3d(0.9, 0.9, 0.9);\n  }\n\n  50%,\n  55% {\n    opacity: 1;\n    transform: scale3d(1.1, 1.1, 1.1);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n}\n@keyframes lightSpeedIn {\n  from {\n    transform: translate3d(100%, 0, 0) skewX(-30deg);\n    opacity: 0;\n  }\n\n  60% {\n    transform: skewX(20deg);\n    opacity: 1;\n  }\n\n  80% {\n    transform: skewX(-5deg);\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes lightSpeedOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    transform: translate3d(100%, 0, 0) skewX(30deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateIn {\n  from {\n    transform-origin: center;\n    transform: rotate3d(0, 0, 1, -200deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: center;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownLeft {\n  from {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInDownRight {\n  from {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpLeft {\n  from {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateInUpRight {\n  from {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, -90deg);\n    opacity: 0;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: translate3d(0, 0, 0);\n    opacity: 1;\n  }\n}\n@keyframes rotateOut {\n  from {\n    transform-origin: center;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: center;\n    transform: rotate3d(0, 0, 1, 200deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownLeft {\n  from {\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, 45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutDownRight {\n  from {\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpLeft {\n  from {\n    transform-origin: left bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: left bottom;\n    transform: rotate3d(0, 0, 1, -45deg);\n    opacity: 0;\n  }\n}\n@keyframes rotateOutUpRight {\n  from {\n    transform-origin: right bottom;\n    opacity: 1;\n  }\n\n  to {\n    transform-origin: right bottom;\n    transform: rotate3d(0, 0, 1, 90deg);\n    opacity: 0;\n  }\n}\n@keyframes rollIn {\n  from {\n    opacity: 0;\n    transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg);\n  }\n\n  to {\n    opacity: 1;\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes rollOut {\n  from {\n    opacity: 1;\n  }\n\n  to {\n    opacity: 0;\n    transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg);\n  }\n}\n@keyframes zoomIn {\n  from {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  50% {\n    opacity: 1;\n  }\n}\n@keyframes zoomInDown {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInLeft {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInRight {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomInUp {\n  from {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  60% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomOut {\n  from {\n    opacity: 1;\n  }\n\n  50% {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n\n  to {\n    opacity: 0;\n  }\n}\n@keyframes zoomOutDown {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0);\n    transform-origin: center bottom;\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes zoomOutLeft {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale(0.1) translate3d(-2000px, 0, 0);\n    transform-origin: left center;\n  }\n}\n@keyframes zoomOutRight {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale(0.1) translate3d(2000px, 0, 0);\n    transform-origin: right center;\n  }\n}\n@keyframes zoomOutUp {\n  40% {\n    opacity: 1;\n    transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0);\n    animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);\n  }\n\n  to {\n    opacity: 0;\n    transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0);\n    transform-origin: center bottom;\n    animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1);\n  }\n}\n@keyframes slideInDown {\n  from {\n    transform: translate3d(0, -100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInLeft {\n  from {\n    transform: translate3d(-100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInRight {\n  from {\n    transform: translate3d(100%, 0, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideInUp {\n  from {\n    transform: translate3d(0, 100%, 0);\n    visibility: visible;\n  }\n\n  to {\n    transform: translate3d(0, 0, 0);\n  }\n}\n@keyframes slideOutDown {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(0, 100%, 0);\n  }\n}\n@keyframes slideOutLeft {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(-100%, 0, 0);\n  }\n}\n@keyframes slideOutRight {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(100%, 0, 0);\n  }\n}\n@keyframes slideOutUp {\n  from {\n    transform: translate3d(0, 0, 0);\n  }\n\n  to {\n    visibility: hidden;\n    transform: translate3d(0, -100%, 0);\n  }\n}\n@keyframes jackInTheBox {\n  from {\n    opacity: 0;\n    transform: scale(0.1) rotate(30deg);\n    transform-origin: center bottom;\n  }\n\n  50% {\n    transform: rotate(-10deg);\n  }\n\n  70% {\n    transform: rotate(3deg);\n  }\n\n  to {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n.flipInX {\n  backface-visibility: visible !important;\n  animation-name: flipInX;\n}\n.flipInY {\n  backface-visibility: visible !important;\n  animation-name: flipInY;\n}\n.fadeIn {\n  animation-name: fadeIn;\n}\n.bounceIn {\n  animation-duration: 0.75s;\n  animation-name: bounceIn;\n}\n.lightSpeedIn {\n  animation-name: lightSpeedIn;\n  animation-timing-function: ease-out;\n}\n.rotateIn {\n  animation-name: rotateIn;\n}\n.rotateInDownLeft {\n  animation-name: rotateInDownLeft;\n}\n.rotateInDownRight {\n  animation-name: rotateInDownRight;\n}\n.rotateInUpLeft {\n  animation-name: rotateInUpLeft;\n}\n.rotateInUpRight {\n  animation-name: rotateInUpRight;\n}\n.rollIn {\n  animation-name: rollIn;\n}\n.zoomIn {\n  animation-name: zoomIn;\n}\n.zoomInDown {\n  animation-name: zoomInDown;\n}\n.zoomInLeft {\n  animation-name: zoomInLeft;\n}\n.zoomInRight {\n  animation-name: zoomInRight;\n}\n.zoomInUp {\n  animation-name: zoomInUp;\n}\n.slideInDown {\n  animation-name: slideInDown;\n}\n.slideInLeft {\n  animation-name: slideInLeft;\n}\n.slideInRight {\n  animation-name: slideInRight;\n}\n.slideInUp {\n  animation-name: slideInUp;\n}\n.jackInTheBox {\n  -webkit-animation-name: jackInTheBox;\n  animation-name: jackInTheBox;\n}\n\n\n\n.flipOutX {\n  animation-duration: 0.75s;\n  animation-name: flipOutX;\n  backface-visibility: visible !important;\n}\n.flipOutY {\n  animation-duration: 0.75s;\n  backface-visibility: visible !important;\n  animation-name: flipOutY;\n}\n.fadeOut {\n  animation-name: fadeOut;\n}\n.bounceOut {\n  animation-duration: 0.75s;\n  animation-name: bounceOut;\n}\n.lightSpeedOut {\n  animation-name: lightSpeedOut;\n  animation-timing-function: ease-in;\n}\n.rotateOut {\n  animation-name: rotateOut;\n}\n.rotateOutDownLeft {\n  animation-name: rotateOutDownLeft;\n}\n.rotateOutDownRight {\n  animation-name: rotateOutDownRight;\n}\n.rotateOutUpLeft {\n  animation-name: rotateOutUpLeft;\n}\n.rotateOutUpRight {\n  animation-name: rotateOutUpRight;\n}\n.hinge {\n  animation-duration: 2s;\n  animation-name: hinge;\n}\n.rollOut {\n  animation-name: rollOut;\n}\n.zoomOut {\n  animation-name: zoomOut;\n}\n.zoomOutDown {\n  animation-name: zoomOutDown;\n}\n.zoomOutLeft {\n  animation-name: zoomOutLeft;\n}\n.zoomOutRight {\n  animation-name: zoomOutRight;\n}\n.zoomOutUp {\n  animation-name: zoomOutUp;\n}\n.slideOutDown {\n  animation-name: slideOutDown;\n}\n.slideOutLeft {\n  animation-name: slideOutLeft;\n}\n.slideOutRight {\n  animation-name: slideOutRight;\n}\n.slideOutUp {\n  animation-name: slideOutUp;\n}\n\n\n.bounce {\n  animation-name: bounce;\n  transform-origin: center bottom;\n}\n.flash {\n  animation-name: flash;\n}\n.pulse {\n  animation-name: pulse;\n}\n.rubberBand {\n  animation-name: rubberBand;\n}\n.shake {\n  animation-name: shake;\n}\n.headShake {\n  animation-timing-function: ease-in-out;\n  animation-name: headShake;\n}\n.swing {\n  transform-origin: top center;\n  animation-name: swing;\n}\n.tada {\n  animation-name: tada;\n}\n.wobble {\n  animation-name: wobble;\n}\n.jello {\n  animation-name: jello;\n  transform-origin: center;\n}\n.heartBeat {\n  animation-name: heartBeat;\n  animation-duration: 1.3s;\n  animation-timing-function: ease-in-out;\n}\n\n.animated {\n  animation-duration: 0.5s;\n  animation-fill-mode: both;\n}\n\n.animated-slow {\n  animation-duration: 0.8s;\n  animation-fill-mode: both;\n}\n\n.animated-fast {\n  animation-duration: 0.3s;\n  animation-fill-mode: both;\n}\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/colors.less",
    "content": "@red-50: #ffebee;\n@red-100: #ffcdd2;\n@red-200: #ef9a9a;\n@red-300: #e57373;\n@red-400: #ef5350;\n@red-500: #f44336;\n@red-600: #e53935;\n@red-700: #d32f2f;\n@red-800: #c62828;\n@red-900: #b71c1c;\n@red-A100: #ff8a80;\n@red-A200: #ff5252;\n@red-A400: #ff1744;\n@red-A700: #d50000;\n@red: @red-500;\n\n\n@pink-50: #fce4ec;\n@pink-100: #f8bbd0;\n@pink-200: #f48fb1;\n@pink-300: #f06292;\n@pink-400: #ec407a;\n@pink-500: #e91e63;\n@pink-600: #d81b60;\n@pink-700: #c2185b;\n@pink-800: #ad1457;\n@pink-900: #880e4f;\n@pink-A100: #ff80ab;\n@pink-A200: #ff4081;\n@pink-A400: #f50057;\n@pink-A700: #c51162;\n@pink: @pink-500;\n\n\n@purple-50: #f3e5f5;\n@purple-100: #e1bee7;\n@purple-200: #ce93d8;\n@purple-300: #ba68c8;\n@purple-400: #ab47bc;\n@purple-500: #9c27b0;\n@purple-600: #8e24aa;\n@purple-700: #7b1fa2;\n@purple-800: #6a1b9a;\n@purple-900: #4a148c;\n@purple-A100: #ea80fc;\n@purple-A200: #e040fb;\n@purple-A400: #d500f9;\n@purple-A700: #aa00ff;\n@purple: @purple-500;\n\n\n@deep-purple-50: #ede7f6;\n@deep-purple-100: #d1c4e9;\n@deep-purple-200: #b39ddb;\n@deep-purple-300: #9575cd;\n@deep-purple-400: #7e57c2;\n@deep-purple-500: #673ab7;\n@deep-purple-600: #5e35b1;\n@deep-purple-700: #512da8;\n@deep-purple-800: #4527a0;\n@deep-purple-900: #311b92;\n@deep-purple-A100: #b388ff;\n@deep-purple-A200: #7c4dff;\n@deep-purple-A400: #651fff;\n@deep-purple-A700: #6200ea;\n@deep-purple: @deep-purple-500;\n\n\n@indigo-50: #e8eaf6;\n@indigo-100: #c5cae9;\n@indigo-200: #9fa8da;\n@indigo-300: #7986cb;\n@indigo-400: #5c6bc0;\n@indigo-500: #3f51b5;\n@indigo-600: #3949ab;\n@indigo-700: #303f9f;\n@indigo-800: #283593;\n@indigo-900: #1a237e;\n@indigo-A100: #8c9eff;\n@indigo-A200: #536dfe;\n@indigo-A400: #3d5afe;\n@indigo-A700: #304ffe;\n@indigo: @indigo-500;\n\n\n@blue-50: #e3f2fd;\n@blue-100: #bbdefb;\n@blue-200: #90caf9;\n@blue-300: #64b5f6;\n@blue-400: #42a5f5;\n@blue-500: #2196f3;\n@blue-600: #1e88e5;\n@blue-700: #1976d2;\n@blue-800: #1565c0;\n@blue-900: #0d47a1;\n@blue-A100: #82b1ff;\n@blue-A200: #448aff;\n@blue-A400: #2979ff;\n@blue-A700: #2962ff;\n@blue: @blue-500;\n\n\n@light-blue-50: #e1f5fe;\n@light-blue-100: #b3e5fc;\n@light-blue-200: #81d4fa;\n@light-blue-300: #4fc3f7;\n@light-blue-400: #29b6f6;\n@light-blue-500: #03a9f4;\n@light-blue-600: #039be5;\n@light-blue-700: #0288d1;\n@light-blue-800: #0277bd;\n@light-blue-900: #01579b;\n@light-blue-A100: #80d8ff;\n@light-blue-A200: #40c4ff;\n@light-blue-A400: #00b0ff;\n@light-blue-A700: #0091ea;\n@light-blue: @light-blue-500;\n\n\n@cyan-50: #e0f7fa;\n@cyan-100: #b2ebf2;\n@cyan-200: #80deea;\n@cyan-300: #4dd0e1;\n@cyan-400: #26c6da;\n@cyan-500: #00bcd4;\n@cyan-600: #00acc1;\n@cyan-700: #0097a7;\n@cyan-800: #00838f;\n@cyan-900: #006064;\n@cyan-A100: #84ffff;\n@cyan-A200: #18ffff;\n@cyan-A400: #00e5ff;\n@cyan-A700: #00b8d4;\n@cyan: @cyan-500;\n\n\n@teal-50: #e0f2f1;\n@teal-100: #b2dfdb;\n@teal-200: #80cbc4;\n@teal-300: #4db6ac;\n@teal-400: #26a69a;\n@teal-500: #009688;\n@teal-600: #00897b;\n@teal-700: #00796b;\n@teal-800: #00695c;\n@teal-900: #004d40;\n@teal-A100: #a7ffeb;\n@teal-A200: #64ffda;\n@teal-A400: #1de9b6;\n@teal-A700: #00bfa5;\n@teal: @teal-500;\n\n\n@green-50: #e8f5e9;\n@green-100: #c8e6c9;\n@green-200: #a5d6a7;\n@green-300: #81c784;\n@green-400: #66bb6a;\n@green-500: #4caf50;\n@green-600: #43a047;\n@green-700: #388e3c;\n@green-800: #2e7d32;\n@green-900: #1b5e20;\n@green-A100: #b9f6ca;\n@green-A200: #69f0ae;\n@green-A400: #00e676;\n@green-A700: #00c853;\n@green: @green-500;\n\n\n@light-green-50: #f1f8e9;\n@light-green-100: #dcedc8;\n@light-green-200: #c5e1a5;\n@light-green-300: #aed581;\n@light-green-400: #9ccc65;\n@light-green-500: #8bc34a;\n@light-green-600: #7cb342;\n@light-green-700: #689f38;\n@light-green-800: #558b2f;\n@light-green-900: #33691e;\n@light-green-A100: #ccff90;\n@light-green-A200: #b2ff59;\n@light-green-A400: #76ff03;\n@light-green-A700: #64dd17;\n@light-green: @light-green-500;\n\n\n@lime-50: #f9fbe7;\n@lime-100: #f0f4c3;\n@lime-200: #e6ee9c;\n@lime-300: #dce775;\n@lime-400: #d4e157;\n@lime-500: #cddc39;\n@lime-600: #c0ca33;\n@lime-700: #afb42b;\n@lime-800: #9e9d24;\n@lime-900: #827717;\n@lime-A100: #f4ff81;\n@lime-A200: #eeff41;\n@lime-A400: #c6ff00;\n@lime-A700: #aeea00;\n@lime: @lime-500;\n\n\n@yellow-50: #fffde7;\n@yellow-100: #fff9c4;\n@yellow-200: #fff59d;\n@yellow-300: #fff176;\n@yellow-400: #ffee58;\n@yellow-500: #fec60a;\n@yellow-600: #fdd835;\n@yellow-700: #fbc02d;\n@yellow-800: #f9a825;\n@yellow-900: #f57f17;\n@yellow-A100: #ffff8d;\n@yellow-A200: #ffff00;\n@yellow-A400: #ffea00;\n@yellow-A700: #ffd600;\n@yellow: @yellow-700;\n\n\n@amber-50: #fff8e1;\n@amber-100: #ffecb3;\n@amber-200: #ffe082;\n@amber-300: #ffd54f;\n@amber-400: #ffca28;\n@amber-500: #ffc107;\n@amber-600: #ffb300;\n@amber-700: #ffa000;\n@amber-800: #ff8f00;\n@amber-900: #ff6f00;\n@amber-A100: #ffe57f;\n@amber-A200: #ffd740;\n@amber-A400: #ffc400;\n@amber-A700: #ffab00;\n@amber: @amber-500;\n\n\n@orange-50: #fff3e0;\n@orange-100: #ffe0b2;\n@orange-200: #ffcc80;\n@orange-300: #ffb74d;\n@orange-400: #ffa726;\n@orange-500: #ff9800;\n@orange-600: #fb8c00;\n@orange-700: #f57c00;\n@orange-800: #ef6c00;\n@orange-900: #e65100;\n@orange-A100: #ffd180;\n@orange-A200: #ffab40;\n@orange-A400: #ff9100;\n@orange-A700: #ff6d00;\n@orange: @orange-500;\n\n\n@deep-orange-50: #fbe9e7;\n@deep-orange-100: #ffccbc;\n@deep-orange-200: #ffab91;\n@deep-orange-300: #ff8a65;\n@deep-orange-400: #ff7043;\n@deep-orange-500: #ff5722;\n@deep-orange-600: #f4511e;\n@deep-orange-700: #e64a19;\n@deep-orange-800: #d84315;\n@deep-orange-900: #bf360c;\n@deep-orange-A100: #ff9e80;\n@deep-orange-A200: #ff6e40;\n@deep-orange-A400: #ff3d00;\n@deep-orange-A700: #dd2c00;\n@deep-orange: @deep-orange-500;\n\n\n@brown-50: #efebe9;\n@brown-100: #d7ccc8;\n@brown-200: #bcaaa4;\n@brown-300: #a1887f;\n@brown-400: #8d6e63;\n@brown-500: #795548;\n@brown-600: #6d4c41;\n@brown-700: #5d4037;\n@brown-800: #4e342e;\n@brown-900: #3e2723;\n@brown-A100: #d7ccc8;\n@brown-A200: #bcaaa4;\n@brown-A400: #8d6e63;\n@brown-A700: #5d4037;\n@brown: @brown-500;\n\n\n@grey-50: #fafafa;\n@grey-100: #f5f5f5;\n@grey-200: #eeeeee;\n@grey-300: #e0e0e0;\n@grey-400: #bdbdbd;\n@grey-500: #9e9e9e;  @rgb-grey-500: \"158, 158, 158\";\n@grey-600: #757575;\n@grey-700: #616161;\n@grey-800: #424242;\n@grey-900: #212121;\n@grey-A100: #f5f5f5;\n@grey-A200: #eeeeee;\n@grey-A400: #bdbdbd;\n@grey-A700: #616161;\n@grey: @grey-500;\n\n\n@blue-grey-50: #eceff1;\n@blue-grey-100: #cfd8dc;\n@blue-grey-200: #b0bec5;\n@blue-grey-300: #90a4ae;\n@blue-grey-400: #78909c;\n@blue-grey-500: #607d8b;\n@blue-grey-600: #546e7a;\n@blue-grey-700: #455a64;\n@blue-grey-800: #37474f;\n@blue-grey-900: #263238;\n@blue-grey-A100: #cfd8dc;\n@blue-grey-A200: #b0bec5;\n@blue-grey-A400: #78909c;\n@blue-grey-A700: #455a64;\n@blue-grey: @blue-grey-500;\n\n\n@black: #000000; @rgb-black: \"0,0,0\";\n@white: #ffffff; @rgb-white: \"255,255,255\";\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/index.less",
    "content": "@import './reset.less';\n@import './animate.less';\n*, *::after, *::before {\n\t-webkit-user-drag: none;\n}\n\nhtml {\n  font-size: 16px;\n}\n\n:root {\n  --color-lyric-unplay: rgba(255, 255, 255, 1);\n  --color-lyric-played: rgba(7, 197, 86, 1);\n  --color-lyric-shadow: rgba(0, 0, 0, 0.14);\n  --color-lyric-shadow-font-mode: rgba(0, 0, 0, 0.07);\n}\n\n.nobreak {\n  white-space: nowrap;\n}\n\n.auto-hidden {\n  .mixin-ellipsis-1();\n}\n\n.center {\n  text-align: center;\n}\n\n.break {\n  word-break: break-all;\n}\n\n.select {\n  user-select: text;\n}\n.no-select {\n  user-select: none;\n}\n\na {\n  color: #000;\n}\n\n\nsmall {\n  font-size: .8em;\n}\n.small {\n  font-size: .9em;\n}\nstrong {\n  font-weight: bold;\n}\n\n.underline {\n  text-decoration: underline;\n}\n\nsvg {\n  transition: @transition-theme;\n  transition-property: fill;\n}\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/layout.less",
    "content": "@import './variables.less';\n\n\n/*自动隐藏文字*/\n.mixin-ellipsis-1() {\n\toverflow: hidden;\n\twhite-space: nowrap;\n\ttext-overflow: ellipsis;\n}\n.mixin-ellipsis(@n: 1) {\n\tdisplay: -webkit-box;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tword-wrap: break-word;\n\tword-break: break-all;\n\twhite-space: normal !important;\n\t-webkit-line-clamp: @n;\n\t-webkit-box-orient: vertical;\n}\n.mixin-ellipsis-2() {\n\tdisplay: -webkit-box;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\tword-wrap: break-word;\n\tword-break: break-all;\n\twhite-space: normal !important;\n\t-webkit-line-clamp: 2;\n\t-webkit-box-orient: vertical;\n}\n\n.stroke(@stroke, @color) {\n  @maxi: @stroke + 1;\n  .i-loop (@i) when (@i > 0) {\n    @maxj: @stroke + 1;\n    .j-loop (@j) when (@j > 0) {\n      text-shadow+: (@i - 1)*(1px)  (@j - 1)*(1px) 0 @color;\n      text-shadow+: (@i - 1)*(1px)  (@j - 1)*(-1px) 0 @color;\n      text-shadow+: (@i - 1)*(-1px)  (@j - 1)*(-1px) 0 @color;\n      text-shadow+: (@i - 1)*(-1px)  (@j - 1)*(1px) 0 @color;\n      .j-loop(@j - 1);\n    }\n    .j-loop (0) {}\n    .j-loop(@maxj);\n    .i-loop(@i - 1);\n  }\n  .i-loop (0) {}\n  .i-loop(@maxi);\n  text-shadow+: 0 0 0 @color;\n}\n\n.stroke4(@color) {\n  text-shadow:\n  0.02em -0.02em 0 var(--color-lyric-shadow),\n  // -0.02em 0.02em 0 var(--color-lyric-shadow),\n  // 0.02em 0.02em 0 var(--color-lyric-shadow),\n  // 0.02em -0.02em 0 var(--color-lyric-shadow),\n  -0.02em -1px 0 var(--color-lyric-shadow),\n  -0.02em 1px 0 var(--color-lyric-shadow),\n  0.02em 1px 0 var(--color-lyric-shadow),\n  0.02em -1px 0 var(--color-lyric-shadow),\n  -0.02em 0px 0 var(--color-lyric-shadow),\n  -0.02em 0px 0 var(--color-lyric-shadow),\n  0.02em 0px 0 var(--color-lyric-shadow),\n  0.02em 0px 0 var(--color-lyric-shadow),\n  // -1px -0.02em 0 var(--color-lyric-shadow),\n  // -1px 0.02em 0 var(--color-lyric-shadow),\n  // 1px 0.02em 0 var(--color-lyric-shadow),\n  // 1px -0.02em 0 var(--color-lyric-shadow),\n  -1px -1px 0 var(--color-lyric-shadow),\n  -1px 1px 0 var(--color-lyric-shadow),\n  1px 1px 0 var(--color-lyric-shadow),\n  1px -1px 0 var(--color-lyric-shadow),\n  -1px 0px 0 var(--color-lyric-shadow),\n  -1px 0px 0 var(--color-lyric-shadow),\n  1px 0px 0 var(--color-lyric-shadow),\n  1px 0px 0 var(--color-lyric-shadow)\n  // 0px -0.02em 0 var(--color-lyric-shadow),\n  // 0px 0.02em 0 var(--color-lyric-shadow),\n  // 0px 0.02em 0 var(--color-lyric-shadow),\n  // 0px -0.02em 0 var(--color-lyric-shadow),\n  // 0px -1px 0 var(--color-lyric-shadow),\n  // 0px 1px 0 var(--color-lyric-shadow),\n  // 0px 1px 0 var(--color-lyric-shadow),\n  // 0px -1px 0 var(--color-lyric-shadow)\n  ;\n  // text-shadow:\n  // -0.04em -0.04em 0 var(--color-lyric-shadow),\n  // // -0.04em 0.04em 0 var(--color-lyric-shadow),\n  // 0.04em 0.04em 0 var(--color-lyric-shadow),\n  // 0.04em -0.04em 0 var(--color-lyric-shadow),\n  // -0.04em 0px 0 var(--color-lyric-shadow),\n  // -0.04em 0px 0 var(--color-lyric-shadow),\n  // 0.04em 0px 0 var(--color-lyric-shadow),\n  // 0.04em 0px 0 var(--color-lyric-shadow),\n  // // 0px -0.03em 0 var(--color-lyric-shadow),\n  // 0px 0.04em 0 var(--color-lyric-shadow),\n  // // 0px 0.03em 0 var(--color-lyric-shadow),\n  // 0px -0.04em 0 var(--color-lyric-shadow);\n\n  // @maxi: 2 + 1;\n  // .i-loop (@i) when (@i > 0) {\n  //   @maxj: 2 + 1;\n  //   .j-loop (@j) when (@j > 0) {\n  //     text-shadow+: (@i - 1)*(-1px)  (@j - 1)*(-1px) 0 @color;\n  //     text-shadow+: (@i - 1)*(-1px)  (@j - 1)*(1px) 0 @color;\n  //     text-shadow+: (@i - 1)*(1px)  (@j - 1)*(1px) 0 @color;\n  //     text-shadow+: (@i - 1)*(1px)  (@j - 1)*(-1px) 0 @color;\n  //     .j-loop(@j - 1);\n  //   }\n  //   .j-loop (0) {}\n  //   .j-loop(@maxj);\n  //   .i-loop(@i - 1);\n  // }\n  // .i-loop (0) {}\n  // .i-loop(@maxi);\n  // text-shadow+: 0 0 0 @color;\n}\n\n.stroke3(@color) {\n  text-shadow: 0.04em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0.04em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  -0.04em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  -0.04em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0.04em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  0.04em -0.01em 0 var(--color-lyric-shadow-font-mode),\n  -0.04em -0.01em 0 var(--color-lyric-shadow-font-mode),\n  -0.04em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  -0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  -0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  0.01em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0.01em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  -0.01em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  -0.01em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0.01em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  0.01em -0.01em 0 var(--color-lyric-shadow-font-mode),\n  -0.01em -0.01em 0 var(--color-lyric-shadow-font-mode),\n  -0.01em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  -0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  -0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  0px 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0px -0.03em 0 var(--color-lyric-shadow-font-mode),\n  0px -0.03em 0 var(--color-lyric-shadow-font-mode),\n  0px 0.04em 0 var(--color-lyric-shadow-font-mode),\n  0px 0.01em 0 var(--color-lyric-shadow-font-mode),\n  0px -0.01em 0 var(--color-lyric-shadow-font-mode),\n  0px -0.01em 0 var(--color-lyric-shadow-font-mode),\n  0px 0.01em 0 var(--color-lyric-shadow-font-mode);\n  // text-shadow: 0.04em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0.04em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0.04em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  // 0.04em -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  // 0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  // 0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  // -0.04em 0px 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em 0.01em 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  // 0.01em 0px 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em 0px 0 var(--color-lyric-shadow-font-mode),\n  // -0.02em 0px 0 var(--color-lyric-shadow-font-mode),\n  // 0px 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0px -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // 0px -0.03em 0 var(--color-lyric-shadow-font-mode),\n  // 0px 0.04em 0 var(--color-lyric-shadow-font-mode),\n  // 0px 0.01em 0 var(--color-lyric-shadow-font-mode),\n  // 0px -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // 0px -0.02em 0 var(--color-lyric-shadow-font-mode),\n  // 0px 0.01em 0 var(--color-lyric-shadow-font-mode);\n}\n\n.stroke2(@color) {\n  text-shadow: 2px 0 @color, -2px 0 @color, 0 2px @color, 0 -2px @color,\n             1px 1px @color, -1px -1px @color, 1px -1px @color, -1px 1px @color;\n}\n// .stroke2(@color) {\n//   // text-shadow: 1px 1px 2px @color;\n//   // text-shadow:\n//   //   -1px -1px 0px @color,\n//   //   0px -1px 0px @color,\n//   //   1px -1px 0px @color,\n//   //   -1px  0px 0px @color,\n//   //   1px  0px 0px @color,\n//   //   -1px  1px 0px @color,\n//   //   0px  1px 0px @color,\n//   //   1px  1px 0px @color;\n//   // -webkit-text-stroke: 0.03em black;\n// \t// -webkit-text-fill-color: @color;\n// }\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/reset.less",
    "content": "// https://github.com/microsoft/vscode/blob/2dd0bca3954d4c03c427d6b447205b68817bd000/src/vs/workbench/browser/media/style.css\n/* Font Families (with CJK support) */\n\n.windows { font-family: \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(zh-Hans) { font-family:\"Microsoft YaHei\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(zh-Hant) { font-family:\"Microsoft Jhenghei\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(ja) { font-family:\"Yu Gothic UI\", \"Meiryo UI\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n.windows:lang(ko) { font-family:\"Malgun Gothic\", \"Dotom\", \"Segoe WPC\", \"Segoe UI\", sans-serif; }\n\n.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }\n.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Hiragino Sans GB\", sans-serif; }\n.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, \"PingFang TC\", sans-serif; }\n.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, \"Hiragino Kaku Gothic Pro\", sans-serif; }\n.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Nanum Gothic\", \"AppleGothic\", sans-serif; }\n\n/* Linux: add `system-ui` as first font and not `Ubuntu` to allow other distribution pick their standard OS font */\n.linux { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", sans-serif; }\n.linux:lang(zh-Hans) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans SC\", \"Source Han Sans CN\", \"Source Han Sans\", sans-serif; }\n.linux:lang(zh-Hant) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans TC\", \"Source Han Sans TW\", \"Source Han Sans\", sans-serif; }\n.linux:lang(ja) { font-family: system-ui, \"Ubuntu\", \"Droid Sans\", \"Source Han Sans J\", \"Source Han Sans JP\", \"Source Han Sans\", sans-serif; }\n.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; }\n\n\nhtml, body, div, span, applet, object, iframe,\nh1, h2, h3, h4, h5, h6, p, blockquote, pre,\na, abbr, acronym, address, big, cite, code,\ndel, dfn, em, img, ins, kbd, q, s, samp,\nsmall, strike, strong, sub, sup, tt, var,\nb, u, i, center,\ndl, dt, dd, ol, ul, li,\nfieldset, form, label, legend,\ntable, caption, tbody, tfoot, thead, tr, th, td,\narticle, aside, canvas, details, embed,\nfigure, figcaption, footer, header, hgroup,\nmenu, nav, output, ruby, section, summary,\ntime, mark, audio, video {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tfont-size: 100%;\n\tfont: inherit;\n  font-size: inherit;\n\tvertical-align: baseline;\n}\ninput, button, textarea {\n\tfont-family: inherit;\n}\n\n/* HTML5 display-role reset for older browsers */\narticle, aside, details, figcaption, figure,\nfooter, header, hgroup, menu, nav, section {\n  display: block;\n}\n// html {\n// }\nbody {\n\tline-height: 1.2;\n}\nol, ul {\n\tlist-style: none;\n}\nblockquote, q {\n\tquotes: none;\n}\nblockquote:before, blockquote:after,\nq:before, q:after {\n\tcontent: '';\n\tcontent: none;\n}\ntable {\n\tborder-collapse: collapse;\n\tborder-spacing: 0;\n}\n"
  },
  {
    "path": "src/renderer-lyric/assets/styles/variables.less",
    "content": "@import './colors.less';\n\n// Width\n@width-app-left: 6.6%;\n\n// Height\n@height-toolbar: 54px;\n@height-player: 60px;\n\n\n// Shadow\n@shadow-app: 8px;\n\n\n// Radius\n@radius-progress-border: 5px;\n@radius-border: 4px;\n\n@transition-theme: .4s ease;\n@transition-slow: .6s ease;\n@transition-normal: .4s ease;\n@transition-fast: .2s ease;\n\n@form-radius: 3px;\n"
  },
  {
    "path": "src/renderer-lyric/components/common/AudioVisualizer.vue",
    "content": "<template>\n  <div :class=\"$style.content\">\n    <canvas ref=\"dom_canvas\" :class=\"$style.canvas\" />\n  </div>\n</template>\n\n<script>\nimport { ref, onBeforeUnmount, onMounted, watch } from '@common/utils/vueTools'\nimport { useEvent, getAnalyserDataArray } from '@lyric/core/mainWindowChannel'\n// import { getAnalyser } from '@renderer/plugins/player'\nimport { isPlay, setting } from '@lyric/store/state'\n\n// const themes = {\n//   green: 'rgba(77,175,124,.16)',\n//   blue: 'rgba(52,152,219,.16)',\n//   yellow: 'rgba(233,212,96,.22)',\n//   orange: 'rgba(245,171,53,.16)',\n//   red: 'rgba(214,69,65,.12)',\n//   pink: 'rgba(241,130,141,.16)',\n//   purple: 'rgba(155,89,182,.14)',\n//   grey: 'rgba(108,122,137,.16)',\n//   ming: 'rgba(51,110,123,.14)',\n//   blue2: 'rgba(79,98,208,.14)',\n//   black: 'rgba(39,39,39,.4)',\n//   mid_autumn: 'rgba(74,55,82,.1)',\n//   naruto: 'rgba(87,144,167,.15)',\n//   happy_new_year: 'rgba(192,57,43,.1)',\n// }\n\nconst getBarWidth = canvasWidth => {\n  let barWidth = (canvasWidth / 128) * 2.5\n  const width = canvasWidth / 86\n  const diffWidth = barWidth - width\n  // console.log(barWidth - width)\n  // if (barWidth - width > 20) newBarWidth = 20\n  // barWidth = newBarWidth\n  return diffWidth > 32\n    ? canvasWidth / 128 // 4k屏、超宽屏直接显示所有频谱条\n    : diffWidth > 12 ? width : barWidth\n}\nexport default {\n  setup() {\n    const dom_canvas = ref(null)\n\n    let ctx\n    // let bufferLength = 0\n    // let dataArray\n    let WIDTH\n    let HEIGHT\n    let MAX_HEIGHT\n    let barWidth\n    let barHeight\n    let x = 0\n    let isPlaying = false\n    let animationFrameId\n\n    let num\n    let mult\n    const maxNum = 255\n    let frequencyAvg = 0\n\n    // const theme = useRefGetter('theme')\n    // const setting = useRefGetter('setting')\n    // let themeColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary-light-200-alpha-800')\n    // watch(theme, theme => {\n    let themeColor = 'rgba(255, 255, 255, .12)'\n    // })\n\n    useEvent((event) => {\n      if (event.action == 'send_analyser_data_array') {\n        // console.log(event.action)\n        renderFrame(event.data)\n      }\n    })\n\n    // https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode/smoothingTimeConstant\n    const renderFrame = (dataArray) => {\n      x = 0\n\n      // console.log(dataArray)\n      // analyser.getByteFrequencyData(dataArray)\n\n      ctx.clearRect(0, 0, WIDTH, HEIGHT)\n      // ctx.fillRect(0, 0, WIDTH, HEIGHT)\n      ctx.fillStyle = themeColor\n\n      for (let i = 0; i < dataArray.length; i++) {\n        mult = Math.floor(i / maxNum)\n        num = mult % 2 === 0 ? (i - maxNum * mult) : (maxNum - (i - maxNum * mult))\n        let spectrum = num > 90 ? 0 : dataArray[num + 20]\n        frequencyAvg += spectrum * 1.4\n      }\n      frequencyAvg /= dataArray.length\n      frequencyAvg *= 1.6\n\n      frequencyAvg = frequencyAvg / maxNum\n      // ctx.scale(1, 1 + frequencyAvg)\n\n      for (let i = 0; i < dataArray.length; i++) {\n        if (x > WIDTH) break\n\n        barHeight = dataArray[i]\n\n        // let r = barHeight + (25 * (i / bufferLength))\n        // let g = 250 * (i / bufferLength)\n        // let b = 50\n\n        // ctx.fillStyle = 'rgb(' + r + ',' + g + ',' + b + ')'\n        barHeight = (barHeight * frequencyAvg + barHeight * 0.42) * MAX_HEIGHT\n        ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight)\n\n        x += barWidth\n      }\n\n      animationFrameId = null\n      if (isPlaying) animationFrameId = window.requestAnimationFrame(getAnalyserDataArray)\n    }\n\n    const handlePlay = () => {\n      isPlaying = true\n      // analyser.fftSize = 256\n      // bufferLength = analyser.frequencyBinCount\n      // console.log(bufferLength)\n      barWidth = getBarWidth(WIDTH)\n      // dataArray = new Uint8Array(bufferLength)\n      // renderFrame()\n      getAnalyserDataArray()\n    }\n\n\n    const handlePause = () => {\n      if (animationFrameId) window.cancelAnimationFrame(animationFrameId)\n      isPlaying = false\n    }\n\n    const handleResize = () => {\n      const canvas = dom_canvas.value\n      canvas.width = canvas.clientWidth\n      canvas.height = canvas.clientHeight\n      WIDTH = canvas.width\n      HEIGHT = canvas.height\n      MAX_HEIGHT = Math.round(HEIGHT * 0.46 / 255 * 10000) / 10000\n      // console.log(MAX_HEIGHT)\n      barWidth = getBarWidth(WIDTH)\n    }\n\n    watch(isPlay, (isPlay) => {\n      if (isPlay) handlePlay()\n      else handlePause()\n    })\n    watch(() => setting['desktopLyric.audioVisualization'], (enable) => {\n      if (!enable) handlePause()\n    })\n    window.addEventListener('resize', handleResize)\n    onBeforeUnmount(() => {\n      handlePause()\n      window.removeEventListener('resize', handleResize)\n    })\n\n    onMounted(() => {\n      const canvas = dom_canvas.value\n      ctx = canvas.getContext('2d')\n      canvas.width = canvas.clientWidth\n      canvas.height = canvas.clientHeight\n      WIDTH = canvas.width\n      HEIGHT = canvas.height\n      MAX_HEIGHT = Math.round(HEIGHT * 0.46 / 255 * 10000) / 10000\n\n      // console.log(MAX_HEIGHT)\n      if (isPlay.value) handlePlay()\n    })\n\n    return {\n      dom_canvas,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n.content {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  z-index: -1;\n}\n.canvas {\n  width: 100%;\n  height: 100%;\n  // opacity: 0.1;\n}\n</style>\n"
  },
  {
    "path": "src/renderer-lyric/components/index.js",
    "content": "import upperFirst from 'lodash/upperFirst'\nimport camelCase from 'lodash/camelCase'\n\nconst requireComponent = require.context('./', true, /\\.vue$/)\n\nconst vueFileRxp = /\\.vue$/\n\nexport default app => {\n  requireComponent.keys().forEach(fileName => {\n    const filePath = fileName.replace(/^\\.\\//, '')\n\n    if (!filePath.split('/').every((path, index, arr) => {\n      const char = path.charAt(0)\n      return vueFileRxp.test(path) || char.toUpperCase() !== char || arr[index + 1] == 'index.vue'\n    })) return\n\n    const componentConfig = requireComponent(fileName)\n\n    let componentName = upperFirst(camelCase(filePath.replace(/\\.\\w+$/, '')))\n\n    if (componentName.endsWith('Index')) componentName = componentName.replace(/Index$/, '')\n\n    app.component(componentName, componentConfig.default || componentConfig)\n  })\n}\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/ControlBar.vue",
    "content": "<template>\n  <div :class=\"$style.container\">\n    <transition enter-active-class=\"animated-fast fadeIn\" leave-active-class=\"animated fadeOut\">\n      <div v-show=\"!isShowThemeList\" :class=\"$style.btns\" @mousedown=\"handleLyricMouseDown\" @touchstart=\"handleLyricTouchStart\">\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__close')\" @click=\"handleClose\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-close\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__' + (setting['desktopLyric.isLock'] ? 'unlock' : 'lock'))\" @click=\"handleLock\">\n          <svg v-if=\"setting['desktopLyric.isLock']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-unlock\" />\n          </svg>\n          <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-lock\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__font_increase')\" @click=\"handleFontChange('increase', 1)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-font-increase\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__font_decrease')\" @click=\"handleFontChange('decrease', 1)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-font-decrease\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__opacity_increase')\" @click=\"handleOpactiyChange('increase', 10)\" @contextmenu=\"handleOpactiyChange('increase', 2)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-opactiy-increase\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__opacity_decrease')\" @click=\"handleOpactiyChange('decrease', 10)\" @contextmenu=\"handleOpactiyChange('decrease', 2)\">\n          <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-opactiy-decrease\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__' + (setting['desktopLyric.style.isZoomActiveLrc'] ? 'lrc_active_zoom_off' : 'lrc_active_zoom_on'))\" @click=\"handleZoomLrc\">\n          <svg v-if=\"setting['desktopLyric.style.isZoomActiveLrc']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-vibrate-off\" />\n          </svg>\n          <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-vibrate\" />\n          </svg>\n        </button>\n        <button :class=\"$style.btn\" :title=\"$t('desktop_lyric__' + (setting['desktopLyric.isAlwaysOnTop'] ? 'win_top_off' : 'win_top_on'))\" @click=\"handleAlwaysOnTop\">\n          <svg v-if=\"setting['desktopLyric.isAlwaysOnTop']\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-top-off\" />\n          </svg>\n          <svg v-else version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" height=\"20px\" viewBox=\"0 0 24 24\" space=\"preserve\">\n            <use xlink:href=\"#icon-top-on\" />\n          </svg>\n        </button>\n      </div>\n    </transition>\n  </div>\n</template>\n\n<script>\nimport { ref } from '@common/utils/vueTools'\nimport { setting } from '@lyric/store/state'\nimport { updateSetting } from '@lyric/store/action'\nimport useDrag from './useDrag'\n\nexport default {\n  setup() {\n    const isShowThemeList = ref(false)\n    const { handleLyricMouseDown, handleLyricTouchStart } = useDrag()\n\n    const handleClose = () => {\n      updateSetting({ 'desktopLyric.enable': false })\n    }\n    const handleLock = () => {\n      updateSetting({ 'desktopLyric.isLock': true })\n    }\n    const handleAlwaysOnTop = () => {\n      updateSetting({ 'desktopLyric.isAlwaysOnTop': !setting['desktopLyric.isAlwaysOnTop'] })\n    }\n    const handleZoomLrc = () => {\n      updateSetting({ 'desktopLyric.style.isZoomActiveLrc': !setting['desktopLyric.style.isZoomActiveLrc'] })\n    }\n    const handleFontChange = (action, step) => {\n      let num\n      switch (action) {\n        case 'increase':\n          num = Math.min(setting['desktopLyric.style.fontSize'] + step, 80)\n          break\n        case 'decrease':\n          num = Math.max(setting['desktopLyric.style.fontSize'] - step, 10)\n          break\n      }\n      if (setting['desktopLyric.style.fontSize'] == num) return\n      updateSetting({ 'desktopLyric.style.fontSize': num })\n    }\n    const handleOpactiyChange = (action, step) => {\n      let num\n      switch (action) {\n        case 'increase':\n          num = Math.min(setting['desktopLyric.style.opacity'] + step, 100)\n          break\n        case 'decrease':\n          num = Math.max(setting['desktopLyric.style.opacity'] - step, 6)\n          break\n      }\n      if (setting['desktopLyric.style.opacity'] == num) return\n      updateSetting({ 'desktopLyric.style.opacity': num })\n    }\n    return {\n      setting,\n      isShowThemeList,\n\n      handleClose,\n      handleLock,\n      handleAlwaysOnTop,\n      handleZoomLrc,\n      handleFontChange,\n      handleOpactiyChange,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '../../assets/styles/layout.less';\n\n@bar-height: 38px;\n@bar-height-padding: 7px;\n\n.container {\n  position: relative;\n  // height: 50px;\n  transition: opacity @transition-theme;\n  // opacity: 0;\n  // &:hover {\n  //   opacity: 1;\n  // }\n}\n\n.btns {\n  display: flex;\n  flex-flow: row wrap;\n  align-items: center;\n  background-color: rgba(0, 0, 0, 0.7);\n}\n\n.btn {\n  min-height: @bar-height;\n  padding: 0 10px;\n  cursor: pointer;\n  border: none;\n  outline: none;\n  background: none;\n  color: #fff;\n  transition: opacity @transition-theme;\n  &:hover {\n    opacity: .7;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/Icons.vue",
    "content": "<template>\n  <svg version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xlink=\"http://www.w3.org/1999/xlink\" style=\"display: none;\">\n    <defs>\n      <g id=\"icon-close\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z\" />\n      </g>\n      <g id=\"icon-unlock\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10A2,2 0 0,1 6,8H15V6A3,3 0 0,0 12,3A3,3 0 0,0 9,6H7A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,17A2,2 0 0,1 10,15A2,2 0 0,1 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17Z\" />\n      </g>\n      <g id=\"icon-lock\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z\" />\n      </g>\n      <g id=\"icon-font-decrease\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M5.12,14L7.5,7.67L9.87,14M6.5,5L1,19H3.25L4.37,16H10.62L11.75,19H14L8.5,5H6.5M18,17L23,11.93L21.59,10.5L19,13.1V7H17V13.1L14.41,10.5L13,11.93L18,17Z\" />\n      </g>\n      <g id=\"icon-font-increase\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M5.12,14L7.5,7.67L9.87,14M6.5,5L1,19H3.25L4.37,16H10.62L11.75,19H14L8.5,5H6.5M18,7L13,12.07L14.41,13.5L17,10.9V17H19V10.9L21.59,13.5L23,12.07L18,7Z\" />\n      </g>\n      <g id=\"icon-opactiy-increase\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63Z\" />\n      </g>\n      <g id=\"icon-opactiy-decrease\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z\" />\n      </g>\n      <g id=\"icon-top-on\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z\" />\n      </g>\n      <g id=\"icon-top-off\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M2,5.27L3.28,4L20,20.72L18.73,22L12.8,16.07V22H11.2V16H6V14L8,12V11.27L2,5.27M16,12L18,14V16H17.82L8,6.18V4H7V2H17V4H16V12Z\" />\n      </g>\n      <g id=\"icon-theme\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M17.5,12A1.5,1.5 0 0,1 16,10.5A1.5,1.5 0 0,1 17.5,9A1.5,1.5 0 0,1 19,10.5A1.5,1.5 0 0,1 17.5,12M14.5,8A1.5,1.5 0 0,1 13,6.5A1.5,1.5 0 0,1 14.5,5A1.5,1.5 0 0,1 16,6.5A1.5,1.5 0 0,1 14.5,8M9.5,8A1.5,1.5 0 0,1 8,6.5A1.5,1.5 0 0,1 9.5,5A1.5,1.5 0 0,1 11,6.5A1.5,1.5 0 0,1 9.5,8M6.5,12A1.5,1.5 0 0,1 5,10.5A1.5,1.5 0 0,1 6.5,9A1.5,1.5 0 0,1 8,10.5A1.5,1.5 0 0,1 6.5,12M12,3A9,9 0 0,0 3,12A9,9 0 0,0 12,21A1.5,1.5 0 0,0 13.5,19.5C13.5,19.11 13.35,18.76 13.11,18.5C12.88,18.23 12.73,17.88 12.73,17.5A1.5,1.5 0 0,1 14.23,16H16A5,5 0 0,0 21,11C21,6.58 16.97,3 12,3Z\" />\n      </g>\n      <g id=\"icon-vibrate\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M16,19H8V5H16M16.5,3H7.5A1.5,1.5 0 0,0 6,4.5V19.5A1.5,1.5 0 0,0 7.5,21H16.5A1.5,1.5 0 0,0 18,19.5V4.5A1.5,1.5 0 0,0 16.5,3M19,17H21V7H19M22,9V15H24V9M3,17H5V7H3M0,15H2V9H0V15Z\" />\n      </g>\n      <g id=\"icon-vibrate-off\" fill=\"currentColor\">\n        <!-- 0 0 24 24-->\n        <path d=\"M8.2,5L6.55,3.35C6.81,3.12 7.15,3 7.5,3H16.5A1.5,1.5 0 0,1 18,4.5V14.8L16,12.8V5H8.2M0,15H2V9H0V15M21,17V7H19V15.8L20.2,17H21M3,17H5V7H3V17M18,17.35L22.11,21.46L20.84,22.73L18,19.85C17.83,20.54 17.21,21 16.5,21H7.5A1.5,1.5 0 0,1 6,19.5V7.89L1.11,3L2.39,1.73L6.09,5.44L8,7.34L16,15.34L18,17.34V17.35M16,17.89L8,9.89V19H16V17.89M22,9V15H24V9H22Z\" />\n      </g>\n      <g id=\"icon-back\" fill=\"currentColor\">\n        <!-- 0 0 512 512-->\n        <path d=\"M511.563,434.259c-1.728-142.329-124.42-258.242-277.087-263.419V95.999c0-17.645-14.342-31.999-31.974-31.999 c-7.931,0-15.591,3.042-21.524,8.562c0,0-134.828,124.829-173.609,163.755C2.623,241.109,0,248.088,0,255.994 c0,7.906,2.623,14.885,7.369,19.687c38.781,38.915,173.609,163.745,173.609,163.745c5.933,5.521,13.593,8.562,21.524,8.562 c17.631,0,31.974-14.354,31.974-31.999v-74.591c153.479,2.156,255.792,50.603,255.792,95.924c0,5.896,4.767,10.666,10.658,10.666 c0.167,0.021,0.333,0.01,0.416,0c5.891,0,10.658-4.771,10.658-10.666C512,436.259,511.854,435.228,511.563,434.259z\" />\n      </g>\n    </defs>\n  </svg>\n</template>\n\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/LyricHorizontal/index.vue",
    "content": "<template>\n  <div\n    ref=\"dom_lyric\"\n    :class=\"classNames\"\n    :style=\"lrcStyles\" @wheel=\"handleWheel\" @mousedown=\"handleLyricMouseDown\" @touchstart=\"handleLyricTouchStart\"\n  >\n    <div :class=\"$style.lyricSpace\" />\n    <div ref=\"dom_lyric_text\" />\n    <div :class=\"$style.lyricSpace\" />\n  </div>\n</template>\n\n<script>\nimport { setting } from '@lyric/store/state'\nimport { computed, useCssModule } from '@common/utils/vueTools'\nimport useLyric from './useLyric'\n\nexport default {\n  setup() {\n    const styles = useCssModule()\n    // const isZoomActiveLrc = computed(() => setting['desktopLyric.style.isZoomActiveLrc'])\n    // const ellipsis = computed(() => setting['desktopLyric.style.ellipsis'])\n    // const isFontWeightFont = computed(() => setting['desktopLyric.style.isFontWeightFont'])\n    // const isFontWeightLine = computed(() => setting['desktopLyric.style.isFontWeightLine'])\n    // const isFontWeightExtended = computed(() => setting['desktopLyric.style.isFontWeightExtended'])\n    const classNames = computed(() => {\n      const name = [styles.lyric]\n      if (isMsDown.value) name.push(styles.draging)\n      if (setting['desktopLyric.style.isZoomActiveLrc']) name.push(styles.lrcActiveZoom)\n      if (setting['desktopLyric.style.ellipsis']) name.push(styles.ellipsis)\n      if (setting['desktopLyric.style.isFontWeightFont']) name.push(styles.fontWeightFont)\n      if (setting['desktopLyric.style.isFontWeightLine']) name.push(styles.fontWeightLine)\n      if (setting['desktopLyric.style.isFontWeightExtended']) name.push(styles.fontWeightExtended)\n      return name\n    })\n    const lrcStyles = computed(() => ({\n      fontFamily: setting['desktopLyric.style.font'],\n      fontSize: Math.trunc(setting['desktopLyric.style.fontSize']) + 'px',\n      opacity: setting['desktopLyric.style.opacity'] / 100,\n      textAlign: setting['desktopLyric.style.align'],\n      '--line-gap': setting['desktopLyric.style.lineGap'] + 'px',\n      '--line-extended-gap': (setting['desktopLyric.style.lineGap'] / 3).toFixed(2) + 'px',\n    }))\n    const isComputeHeight = computed(() => {\n      return setting['desktopLyric.style.isZoomActiveLrc'] && !setting['desktopLyric.isDelayScroll']\n    })\n    const {\n      dom_lyric,\n      dom_lyric_text,\n      isMsDown,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n    } = useLyric(isComputeHeight)\n\n    return {\n      classNames,\n      lrcStyles,\n\n      dom_lyric,\n      dom_lyric_text,\n      isMsDown,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@lyric/assets/styles/layout.less';\n\n.lyric {\n  position: relative;\n  text-align: center;\n  height: 100%;\n  overflow: hidden;\n  font-size: 16px;\n  contain: strict;\n  cursor: move;\n  // font-weight: bold;\n\n  :global {\n    .font-lrc, .shadow {\n      padding: 0.08em 0.14em;\n      margin: -0.08em 0;\n    }\n    .font-lrc {\n      color: var(--color-lyric-unplay);\n    }\n    .shadow {\n      color: transparent;\n      // margin-left: -0.14em;\n    }\n    .line-content {\n      line-height: 1.2;\n      margin: var(--line-gap) 0;\n      overflow-wrap: break-word;\n\n      .font-lrc {\n        cursor: grab;\n      }\n\n      .extended {\n        font-size: 0.8em;\n        margin-top: var(--line-extended-gap);\n      }\n      &.line-mode {\n        .font-lrc {\n          transition: @transition-slow;\n          transition-property: font-size, color;\n        }\n      }\n      &.line-mode.active .font-lrc, &.font-mode.played .font-lrc {\n        color: var(--color-lyric-played);\n      }\n      &.font-mode .extended .font-lrc {\n        transition: @transition-slow;\n        transition-property: font-size, color;\n      }\n      // &.font-mode > .line {\n      //   font-weight: bold;\n      // }\n\n      &.font-mode > .line > .font-lrc {\n        > span {\n          transition: @transition-slow;\n          transition-property: font-size;\n          font-size: 1em;\n          background-repeat: no-repeat;\n          background-color: var(--color-lyric-unplay);\n          background-image: -webkit-linear-gradient(left, var(--color-lyric-played), var(--color-lyric-played));\n          -webkit-text-fill-color: transparent;\n          -webkit-background-clip: text;\n          background-size: 0 100%;\n          padding-left: 0.12em;\n          padding-right: 0.12em;\n          padding-bottom: 0.12em;\n          margin-left: -0.11em;\n          margin-right: -0.11em;\n          margin-bottom: -0.12em;\n        }\n      }\n     .line .shadow span {\n        padding-left: 0.12em;\n        padding-right: 0.12em;\n        padding-bottom: 0.12em;\n        margin-left: -0.11em;\n        margin-right: -0.11em;\n        margin-bottom: -0.12em;\n      }\n      // &.line-mode {\n      //   .shadow {\n      //     text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.40);\n      //   }\n      // }\n\n      // &.font-mode {\n      // }\n    }\n    .line-mode .font-lrc, .extended .font-lrc {\n      // text-shadow: 0 0 2px rgba(0, 0, 0, 0.7), 0 0 2px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.3);\n      .stroke3(var(--color-lyric-shadow));\n      // .stroke2(rgba(0, 0, 0, 0.18));\n      // .stroke(1px, rgba(0, 0, 0, 0.08));\n      // .stroke(2px, rgba(0, 0, 0, 0.025));\n      transition: font-size @transition-slow;\n    }\n    .font-mode .line .shadow span {\n      .stroke(1px, var(--color-lyric-shadow-font-mode));\n      transition: font-size @transition-slow;\n      // text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3),  1px 1px 1px rgba(0, 0, 0, 0.3);\n    }\n\n  }\n  // p {\n  //   padding: 8px 0;\n  //   line-height: 1.2;\n  //   overflow-wrap: break-word;\n  //   transition: @transition-normal !important;\n  //   transition-property: color, font-size;\n  // }\n}\n\n// .lrc-line {\n//   display: inline-block;\n//   padding: 8px 0;\n//   line-height: 1.2;\n//   overflow-wrap: break-word;\n//   transition: @transition-normal;\n//   transition-property: color, font-size, text-shadow;\n//   cursor: grab;\n//   // font-weight: bold;\n//   // background-clip: text;\n//   color: @color-theme-lyric;\n//   text-shadow: 1px 1px 2px #000;\n//   // background: linear-gradient(@color-theme-lyric, @color-theme-lyric);\n//   // background-clip: text;\n//   // -webkit-background-clip: text;\n//   // -webkit-text-fill-color: #fff;\n//   // -webkit-text-stroke: thin #124628;\n// }\n.lyricSpace {\n  height: 80%;\n}\n// .lyric-text {\n\n// }\n// .lrc-active {\n\n//   .lrc-line {\n//     color: @color-theme-lyric_2;\n//     // background: linear-gradient(@color-theme-lyric, @color-theme-lyric_2);\n//     // background-clip: text;\n//     // -webkit-background-clip: text;\n//     // -webkit-text-fill-color: @color-theme-lyric_2;\n//     // -webkit-text-stroke: thin #124628;\n//   }\n// }\n.draging {\n  :global {\n    .line-content {\n      .font-lrc {\n        cursor: grabbing;\n      }\n    }\n  }\n}\n.lrcActiveZoom {\n  :global {\n    .line-content {\n      &.active {\n        .extended {\n          font-size: .94em;\n        }\n        .line {\n          font-size: 1.2em;\n        }\n      }\n    }\n  }\n}\n.ellipsis {\n  :global {\n    .font-lrc, .shadow {\n      display: -webkit-box !important;\n      .mixin-ellipsis(1);\n    }\n  }\n}\n.fontWeightFont {\n  :global {\n    .font-mode > .line {\n      font-weight: bold;\n    }\n  }\n}\n.fontWeightLine {\n  :global {\n    .line-mode > .line {\n      font-weight: bold;\n    }\n  }\n}\n.fontWeightExtended {\n  :global {\n    .extended {\n      font-weight: bold;\n    }\n  }\n}\n// .footer {\n//   flex: 0 0 100px;\n//   overflow: hidden;\n//   display: flex;\n//   align-items: center;\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/LyricHorizontal/useLyric.js",
    "content": "import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools'\nimport { scrollTo } from '@common/utils/renderer'\nimport { lyric } from '@lyric/store/lyric'\nimport { isPlay, setting } from '@lyric/store/state'\nimport { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc'\nimport { isWin } from '@common/utils'\n\nconst getOffsetTop = (contentHeight, lineHeight) => {\n  switch (setting['desktopLyric.scrollAlign']) {\n    case 'top': return 0\n    default: return contentHeight * 0.5 - lineHeight / 2\n  }\n}\n\nexport default (isComputeHeight) => {\n  const dom_lyric = ref(null)\n  const dom_lyric_text = ref(null)\n  const isMsDown = ref(false)\n  let isStopScroll = false\n\n  const winEvent = {\n    isMsDown: false,\n    msDownX: 0,\n    msDownY: 0,\n    windowW: 0,\n    windowH: 0,\n  }\n\n  let msDownY = 0\n  let msDownScrollY = 0\n  let timeout = null\n  let cancelScrollFn\n  let dom_lines\n  let line_heights\n  let isSetedLines = false\n  let prevActiveLine = 0\n\n\n  const handleScrollLrc = (duration = 300) => {\n    if (!dom_lines?.length || !dom_lyric.value) return\n    if (isStopScroll) return\n    let dom_p = dom_lines[lyric.line]\n\n    if (dom_p) {\n      let offset = 0\n      if (isComputeHeight.value) {\n        let prevLineHeight = line_heights[prevActiveLine] ?? 0\n        offset = prevActiveLine < lyric.line ? ((dom_lines[prevActiveLine]?.clientHeight ?? 0) - prevLineHeight) : 0\n        // console.log(prevActiveLine, dom_lines[prevActiveLine]?.clientHeight ?? 0, prevLineHeight, offset)\n      }\n      cancelScrollFn = scrollTo(dom_lyric.value, dom_p ? (dom_p.offsetTop - offset - getOffsetTop(dom_lyric.value.clientHeight, dom_p.clientHeight)) : 0, duration)\n    } else {\n      cancelScrollFn = scrollTo(dom_lyric.value, 0, duration)\n    }\n  }\n  const clearLyricScrollTimeout = () => {\n    if (!timeout) return\n    clearTimeout(timeout)\n    timeout = null\n  }\n  const startLyricScrollTimeout = () => {\n    clearLyricScrollTimeout()\n    timeout = setTimeout(() => {\n      timeout = null\n      isStopScroll = false\n      if (!isPlay.value) return\n      handleScrollLrc()\n    }, 3000)\n  }\n\n  const handleLyricDown = (target, x, y) => {\n    if (target.classList.contains('font-lrc') ||\n        target.parentNode.classList.contains('font-lrc') ||\n        target.classList.contains('extended') ||\n        target.parentNode.classList.contains('extended')\n    ) {\n      if (delayScrollTimeout) {\n        clearTimeout(delayScrollTimeout)\n        delayScrollTimeout = null\n      }\n      isMsDown.value = true\n      msDownY = y\n      msDownScrollY = dom_lyric.value.scrollTop\n    } else {\n      winEvent.isMsDown = true\n      winEvent.msDownX = x\n      winEvent.msDownY = y\n      winEvent.windowW = window.innerWidth\n      winEvent.windowH = window.innerHeight\n      // https://github.com/lyswhut/lx-music-desktop/issues/2244\n      if (isWin) setWindowResizeable(false)\n    }\n  }\n  const handleLyricMouseDown = event => {\n    handleLyricDown(event.target, event.clientX, event.clientY)\n  }\n  const handleLyricTouchStart = event => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      handleLyricDown(event.target, touch.clientX, touch.clientY)\n    }\n  }\n  const handleMouseMsUp = () => {\n    isMsDown.value = false\n    winEvent.isMsDown = false\n    if (isWin) setWindowResizeable(true)\n  }\n\n  const handleMove = (x, y) => {\n    if (isMsDown.value) {\n      isStopScroll ||= true\n      if (cancelScrollFn) {\n        cancelScrollFn()\n        cancelScrollFn = null\n      }\n      dom_lyric.value.scrollTop = msDownScrollY + msDownY - y\n      startLyricScrollTimeout()\n    } else if (winEvent.isMsDown) {\n      // https://github.com/lyswhut/lx-music-desktop/issues/2244\n      if (isWin) {\n        setWindowBounds({\n          x: x - winEvent.msDownX,\n          y: y - winEvent.msDownY,\n          w: winEvent.windowW,\n          h: winEvent.windowH,\n        })\n      } else {\n        setWindowBounds({\n          x: x - winEvent.msDownX,\n          y: y - winEvent.msDownY,\n          w: window.innerWidth,\n          h: window.innerHeight,\n        })\n      }\n    }\n  }\n  const handleMouseMsMove = event => {\n    handleMove(event.clientX, event.clientY)\n  }\n  const handleTouchMove = (e) => {\n    if (e.changedTouches.length) {\n      const touch = e.changedTouches[0]\n      handleMove(touch.clientX, touch.clientY)\n    }\n  }\n\n  const handleWheel = (event) => {\n    console.log(event.deltaY)\n    if (cancelScrollFn) {\n      cancelScrollFn()\n      cancelScrollFn = null\n    }\n    dom_lyric.value.scrollTop = dom_lyric.value.scrollTop + event.deltaY\n    startLyricScrollTimeout()\n  }\n\n  const setLyric = (lines) => {\n    const dom_line_content = document.createDocumentFragment()\n    for (const line of lines) {\n      dom_line_content.appendChild(line.dom_line)\n    }\n    dom_lyric_text.value.textContent = ''\n    dom_lyric_text.value.appendChild(dom_line_content)\n    nextTick(() => {\n      dom_lines = dom_lyric.value.querySelectorAll('.line-content')\n      line_heights = Array.from(dom_lines).map(l => l.clientHeight)\n      handleScrollLrc()\n    })\n  }\n\n  const initLrc = (lines, oLines) => {\n    prevActiveLine = 0\n    isSetedLines = true\n    if (oLines) {\n      if (lines.length) {\n        setLyric(lines)\n      } else {\n        cancelScrollFn = scrollTo(dom_lyric.value, 0, 300, () => {\n          if (lyric.lines !== lines) return\n          setLyric(lines)\n        }, 50)\n      }\n    } else {\n      setLyric(lines)\n    }\n  }\n\n  let delayScrollTimeout\n  const scrollLine = (line, oldLine) => {\n    setImmediate(() => {\n      prevActiveLine = line\n    })\n    if (line < 0 || !lyric.lines.length) return\n    if (line == 0 && isSetedLines) return isSetedLines = false\n    isSetedLines &&= false\n    if (oldLine == null || line - oldLine != 1) return handleScrollLrc()\n\n    if (setting['desktopLyric.isDelayScroll']) {\n      delayScrollTimeout = setTimeout(() => {\n        delayScrollTimeout = null\n        handleScrollLrc(600)\n      }, 600)\n    } else {\n      handleScrollLrc()\n    }\n  }\n\n  watch(() => lyric.lines, initLrc)\n  watch(() => lyric.line, scrollLine)\n\n  onMounted(() => {\n    document.addEventListener('mousemove', handleMouseMsMove)\n    document.addEventListener('mouseup', handleMouseMsUp)\n    document.addEventListener('touchmove', handleTouchMove)\n    document.addEventListener('touchend', handleMouseMsUp)\n\n    initLrc(lyric.lines, null)\n  })\n\n  onBeforeUnmount(() => {\n    document.removeEventListener('mousemove', handleMouseMsMove)\n    document.removeEventListener('mouseup', handleMouseMsUp)\n    document.removeEventListener('touchmove', handleTouchMove)\n    document.removeEventListener('touchend', handleMouseMsUp)\n  })\n\n  return {\n    dom_lyric,\n    dom_lyric_text,\n    isMsDown,\n    handleLyricMouseDown,\n    handleLyricTouchStart,\n    handleWheel,\n  }\n}\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/LyricVertical/index.vue",
    "content": "<template>\n  <div\n    ref=\"dom_lyric\"\n    :class=\"classNames\"\n    :style=\"lrcStyles\" @wheel=\"handleWheel\" @mousedown=\"handleLyricMouseDown\" @touchstart=\"handleLyricTouchStart\"\n  >\n    <div :class=\"$style.lyricSpace\" />\n    <div ref=\"dom_lyric_text\" />\n    <div :class=\"$style.lyricSpace\" />\n  </div>\n</template>\n\n<script>\nimport { setting } from '@lyric/store/state'\nimport { computed, useCssModule } from '@common/utils/vueTools'\nimport useLyric from './useLyric'\n\nexport default {\n  setup() {\n    const styles = useCssModule()\n    // const isZoomActiveLrc = computed(() => setting['desktopLyric.style.isZoomActiveLrc'])\n    // const ellipsis = computed(() => setting['desktopLyric.style.ellipsis'])\n    // const isFontWeightFont = computed(() => setting['desktopLyric.style.isFontWeightFont'])\n    // const isFontWeightLine = computed(() => setting['desktopLyric.style.isFontWeightLine'])\n    const classNames = computed(() => {\n      const name = [styles.lyric]\n      if (isMsDown.value) name.push(styles.draging)\n      if (setting['desktopLyric.style.isZoomActiveLrc']) name.push(styles.lrcActiveZoom)\n      if (setting['desktopLyric.style.ellipsis']) name.push(styles.ellipsis)\n      if (setting['desktopLyric.style.isFontWeightFont']) name.push(styles.fontWeightFont)\n      if (setting['desktopLyric.style.isFontWeightLine']) name.push(styles.fontWeightLine)\n      if (setting['desktopLyric.style.isFontWeightExtended']) name.push(styles.fontWeightExtended)\n      return name\n    })\n    const lrcStyles = computed(() => ({\n      fontFamily: setting['desktopLyric.style.font'],\n      fontSize: Math.trunc(setting['desktopLyric.style.fontSize']) + 'px',\n      opacity: setting['desktopLyric.style.opacity'] / 100,\n      textAlign: setting['desktopLyric.style.align'],\n      '--line-gap': Math.ceil(setting['desktopLyric.style.lineGap'] * 1.06) + 'px',\n      '--line-extended-gap': Math.ceil(setting['desktopLyric.style.lineGap'] * 1.06 / 8).toFixed(2) + 'px',\n    }))\n    const isComputeWidth = computed(() => {\n      return setting['desktopLyric.style.isZoomActiveLrc'] && !setting['desktopLyric.isDelayScroll']\n    })\n    const {\n      dom_lyric,\n      dom_lyric_text,\n      isMsDown,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n    } = useLyric(isComputeWidth)\n\n    return {\n      classNames,\n      lrcStyles,\n\n      dom_lyric,\n      dom_lyric_text,\n      isMsDown,\n      handleLyricMouseDown,\n      handleLyricTouchStart,\n      handleWheel,\n    }\n  },\n}\n</script>\n\n<style lang=\"less\" module>\n@import '@lyric/assets/styles/layout.less';\n\n.lyric {\n  position: relative;\n  text-align: center;\n  height: 100%;\n  overflow-x: scroll;\n  font-size: 16px;\n  contain: strict;\n  cursor: move;\n  writing-mode: vertical-rl;\n  width: 100%;\n  &::-webkit-scrollbar {\n    height: 0;\n  }\n\n  :global {\n    .font-lrc, .shadow {\n      padding: 0.14em 0.07em;\n      margin: 0 -0.07em;\n    }\n    .font-lrc {\n      color: var(--color-lyric-unplay);\n    }\n    .shadow {\n      color: transparent;\n      // margin-left: -0.14em;\n    }\n    .line-content {\n      line-height: 1.2;\n      margin: 0 var(--line-gap);\n      overflow-wrap: break-word;\n\n      .font-lrc {\n        cursor: grab;\n      }\n\n      .extended {\n        font-size: 0.8em;\n        margin-right: var(--line-extended-gap);\n      }\n      &.line-mode {\n        letter-spacing: 5px;\n        .font-lrc {\n          transition: @transition-slow;\n          transition-property: font-size, color;\n        }\n      }\n      &.line-mode.active .font-lrc, &.font-mode.played .font-lrc {\n        color: var(--color-lyric-played);\n      }\n      &.font-mode .extended .font-lrc {\n        transition: @transition-slow;\n        transition-property: font-size, color;\n      }\n      // &.font-mode > .line {\n      //   font-weight: bold;\n      // }\n\n      &.font-mode > .line > .font-lrc {\n        > span {\n          transition: @transition-slow;\n          transition-property: font-size;\n          font-size: 1em;\n          background-repeat: no-repeat;\n          background-color: var(--color-lyric-unplay);\n          background-image: -webkit-linear-gradient(top, var(--color-lyric-played), var(--color-lyric-played));\n          -webkit-text-fill-color: transparent;\n          -webkit-background-clip: text;\n          background-size: 0 100%;\n          padding: 0.14em;\n          margin: -0.08em;\n        }\n      }\n\n      &.font-mode .line .shadow span {\n        padding: 0.14em;\n        margin: -0.08em;\n      }\n    }\n    // .shadow {\n    //   .stroke2(rgba(0, 0, 0, 0.05));\n    //   transition: font-size @transition-normal;\n    // }\n    // .font-mode .line .shadow {\n    //   .stroke(1px, rgba(0, 0, 0, 0.05));\n    //   // text-shadow: 1px 0 2px rgba(0, 0, 0, 0.30), 1px 0 1px rgba(0, 0, 0, 0.20);\n    // }\n    .line-mode .font-lrc, .extended .font-lrc {\n      // text-shadow: 0 0 2px rgba(0, 0, 0, 0.7), 0 0 2px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.3);\n      // .stroke2(rgba(0, 0, 0, 0.14));\n      .stroke4(var(--color-lyric-shadow));\n      // .stroke(1px, rgba(0, 0, 0, 0.08));\n      // .stroke(2px, rgba(0, 0, 0, 0.025));\n      transition: font-size @transition-slow;\n    }\n    .font-mode .line .shadow span {\n      .stroke(1px, var(--color-lyric-shadow-font-mode));\n      // .stroke(1px, rgba(0, 0, 0, 0.07));\n      transition: font-size @transition-slow;\n      // text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3),  1px 1px 1px rgba(0, 0, 0, 0.3);\n    }\n\n\n  }\n  // p {\n  //   padding: 8px 0;\n  //   line-height: 1.2;\n  //   overflow-wrap: break-word;\n  //   transition: @transition-normal !important;\n  //   transition-property: color, font-size;\n  // }\n}\n// .lrc-line {\n//   display: inline-block;\n//   padding: 8px 0;\n//   line-height: 1.2;\n//   overflow-wrap: break-word;\n//   transition: @transition-normal;\n//   transition-property: color, font-size, text-shadow;\n//   cursor: grab;\n//   // font-weight: bold;\n//   // background-clip: text;\n//   color: @color-theme-lyric;\n//   text-shadow: 1px 1px 2px #000;\n//   // background: linear-gradient(@color-theme-lyric, @color-theme-lyric);\n//   // background-clip: text;\n//   // -webkit-background-clip: text;\n//   // -webkit-text-fill-color: #fff;\n//   // -webkit-text-stroke: thin #124628;\n// }\n.lyricSpace {\n  width: 80%;\n  height: 100%;\n}\n// .lyric-text {\n\n// }\n// .lrc-active {\n\n//   .lrc-line {\n//     color: @color-theme-lyric_2;\n//     // background: linear-gradient(@color-theme-lyric, @color-theme-lyric_2);\n//     // background-clip: text;\n//     // -webkit-background-clip: text;\n//     // -webkit-text-fill-color: @color-theme-lyric_2;\n//     // -webkit-text-stroke: thin #124628;\n//   }\n// }\n.draging {\n  :global {\n    .line-content {\n      .font-lrc {\n        cursor: grabbing;\n      }\n    }\n  }\n}\n.lrcActiveZoom {\n  :global {\n    .line-content {\n      &.active {\n        .extended {\n          font-size: .94em;\n        }\n        .line {\n          font-size: 1.2em;\n        }\n      }\n    }\n  }\n}\n.ellipsis {\n  :global {\n    .font-lrc, .shadow {\n      display: -webkit-box !important;\n      .mixin-ellipsis(1);\n    }\n  }\n}\n.fontWeightFont {\n  :global {\n    .font-mode > .line {\n      font-weight: bold;\n    }\n  }\n}\n.fontWeightLine {\n  :global {\n    .line-mode > .line {\n      font-weight: bold;\n    }\n  }\n}\n.fontWeightExtended {\n  :global {\n    .extended {\n      font-weight: bold;\n    }\n  }\n}\n// .footer {\n//   flex: 0 0 100px;\n//   overflow: hidden;\n//   display: flex;\n//   align-items: center;\n// }\n\n</style>\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/LyricVertical/useLyric.js",
    "content": "import { ref, onMounted, onBeforeUnmount, watch, nextTick } from '@common/utils/vueTools'\nimport { scrollXRTo } from '@common/utils/renderer'\nimport { lyric } from '@lyric/store/lyric'\nimport { isPlay, setting } from '@lyric/store/state'\nimport { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc'\nimport { isWin } from '@common/utils'\n\nconst getOffsetTop = (contentWidth, lineWidth) => {\n  switch (setting['desktopLyric.scrollAlign']) {\n    case 'top': return contentWidth - lineWidth - 2\n    default: return contentWidth * 0.5 - lineWidth / 2\n  }\n}\n\nexport default (isComputeWidth) => {\n  const dom_lyric = ref(null)\n  const dom_lyric_text = ref(null)\n  const isMsDown = ref(false)\n  let isStopScroll = false\n\n  const winEvent = {\n    isMsDown: false,\n    msDownX: 0,\n    msDownY: 0,\n    windowW: 0,\n    windowH: 0,\n  }\n\n  let msDownX = 0\n  let msDownScrollX = 0\n  let timeout = null\n  let cancelScrollFn\n  let dom_lines\n  let line_widths\n  let isSetedLines = false\n  let prevActiveLine = 0\n\n\n  const handleScrollLrc = (duration = 300) => {\n    if (!dom_lines?.length || !dom_lyric.value) return\n    if (isStopScroll) return\n    let dom_p = dom_lines[lyric.line]\n\n    if (dom_p) {\n      let offset = 0\n      if (isComputeWidth.value) {\n        let prevLineWidth = line_widths[prevActiveLine] ?? 0\n        offset = prevActiveLine < lyric.line ? ((dom_lines[prevActiveLine]?.clientWidth ?? 0) - prevLineWidth) : 0\n        // console.log(prevActiveLine, dom_lines[prevActiveLine]?.clientHeight ?? 0, prevLineWidth, offset)\n      }\n      cancelScrollFn = scrollXRTo(dom_lyric.value, dom_p ? (dom_p.offsetLeft + offset - getOffsetTop(dom_lyric.value.clientWidth, dom_p.clientWidth)) : 0, duration)\n    } else {\n      cancelScrollFn = scrollXRTo(dom_lyric.value, 0, duration)\n    }\n  }\n  const clearLyricScrollTimeout = () => {\n    if (!timeout) return\n    clearTimeout(timeout)\n    timeout = null\n  }\n  const startLyricScrollTimeout = () => {\n    clearLyricScrollTimeout()\n    timeout = setTimeout(() => {\n      timeout = null\n      isStopScroll = false\n      if (!isPlay.value) return\n      handleScrollLrc()\n    }, 3000)\n  }\n\n  const handleLyricDown = (target, x, y) => {\n    if (target.classList.contains('font-lrc') ||\n        target.parentNode.classList.contains('font-lrc') ||\n        target.classList.contains('extended') ||\n        target.parentNode.classList.contains('extended')\n    ) {\n      if (delayScrollTimeout) {\n        clearTimeout(delayScrollTimeout)\n        delayScrollTimeout = null\n      }\n      isMsDown.value = true\n      msDownX = x\n      msDownScrollX = dom_lyric.value.scrollLeft\n    } else {\n      winEvent.isMsDown = true\n      winEvent.msDownX = x\n      winEvent.msDownY = y\n      winEvent.windowW = window.innerWidth\n      winEvent.windowH = window.innerHeight\n      // https://github.com/lyswhut/lx-music-desktop/issues/2244\n      if (isWin) setWindowResizeable(false)\n    }\n  }\n  const handleLyricMouseDown = event => {\n    handleLyricDown(event.target, event.clientX, event.clientY)\n  }\n  const handleLyricTouchStart = event => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      handleLyricDown(event.target, touch.clientX, touch.clientY)\n    }\n  }\n  const handleMouseMsUp = () => {\n    isMsDown.value = false\n    winEvent.isMsDown = false\n    if (isWin) setWindowResizeable(true)\n  }\n\n  const handleMove = (x, y) => {\n    if (isMsDown.value) {\n      isStopScroll ||= true\n      if (cancelScrollFn) {\n        cancelScrollFn()\n        cancelScrollFn = null\n      }\n      dom_lyric.value.scrollLeft = msDownScrollX + msDownX - x\n      startLyricScrollTimeout()\n    } else if (winEvent.isMsDown) {\n      // https://github.com/lyswhut/lx-music-desktop/issues/2244\n      if (isWin) {\n        setWindowBounds({\n          x: x - winEvent.msDownX,\n          y: y - winEvent.msDownY,\n          w: winEvent.windowW,\n          h: winEvent.windowH,\n        })\n      } else {\n        setWindowBounds({\n          x: x - winEvent.msDownX,\n          y: y - winEvent.msDownY,\n          w: window.innerWidth,\n          h: window.innerHeight,\n        })\n      }\n    }\n  }\n  const handleMouseMsMove = event => {\n    handleMove(event.clientX, event.clientY)\n  }\n  const handleTouchMove = (e) => {\n    if (e.changedTouches.length) {\n      const touch = e.changedTouches[0]\n      handleMove(touch.clientX, touch.clientY)\n    }\n  }\n\n  const handleWheel = (event) => {\n    console.log(event.deltaY)\n    if (cancelScrollFn) {\n      cancelScrollFn()\n      cancelScrollFn = null\n    }\n    dom_lyric.value.scrollLeft = dom_lyric.value.scrollLeft - event.deltaY\n    startLyricScrollTimeout()\n  }\n\n  const setLyric = (lines) => {\n    const dom_line_content = document.createDocumentFragment()\n    for (const line of lines) {\n      dom_line_content.appendChild(line.dom_line)\n    }\n    dom_lyric_text.value.textContent = ''\n    dom_lyric_text.value.appendChild(dom_line_content)\n    nextTick(() => {\n      dom_lines = dom_lyric.value.querySelectorAll('.line-content')\n      line_widths = Array.from(dom_lines).map(l => l.clientWidth)\n      handleScrollLrc()\n    })\n  }\n\n  const initLrc = (lines, oLines) => {\n    prevActiveLine = 0\n    isSetedLines = true\n    if (oLines) {\n      if (lines.length) {\n        setLyric(lines)\n      } else {\n        cancelScrollFn = scrollXRTo(dom_lyric.value, 0, 300, () => {\n          if (lyric.lines !== lines) return\n          setLyric(lines)\n        }, 50)\n      }\n    } else {\n      setLyric(lines)\n    }\n  }\n\n  let delayScrollTimeout\n  const scrollLine = (line, oldLine) => {\n    setImmediate(() => {\n      prevActiveLine = line\n    })\n    if (line < 0) return\n    if (line == 0 && isSetedLines) return isSetedLines = false\n    isSetedLines &&= false\n    if (oldLine == null || line - oldLine != 1) return handleScrollLrc()\n\n    if (setting['desktopLyric.isDelayScroll']) {\n      delayScrollTimeout = setTimeout(() => {\n        delayScrollTimeout = null\n        handleScrollLrc(600)\n      }, 600)\n    } else {\n      handleScrollLrc()\n    }\n  }\n\n  watch(() => lyric.lines, initLrc)\n  watch(() => lyric.line, scrollLine)\n\n  onMounted(() => {\n    document.addEventListener('mousemove', handleMouseMsMove)\n    document.addEventListener('mouseup', handleMouseMsUp)\n    document.addEventListener('touchmove', handleTouchMove)\n    document.addEventListener('touchend', handleMouseMsUp)\n\n    initLrc(lyric.lines, null)\n  })\n\n  onBeforeUnmount(() => {\n    document.removeEventListener('mousemove', handleMouseMsMove)\n    document.removeEventListener('mouseup', handleMouseMsUp)\n    document.removeEventListener('touchmove', handleTouchMove)\n    document.removeEventListener('touchend', handleMouseMsUp)\n  })\n\n  return {\n    dom_lyric,\n    dom_lyric_text,\n    isMsDown,\n    handleLyricMouseDown,\n    handleLyricTouchStart,\n    handleWheel,\n  }\n}\n"
  },
  {
    "path": "src/renderer-lyric/components/layout/useDrag.js",
    "content": "import { onMounted, onBeforeUnmount } from '@common/utils/vueTools'\nimport { setWindowBounds, setWindowResizeable } from '@lyric/utils/ipc'\nimport { isWin } from '@common/utils'\n\nexport default () => {\n  const winEvent = {\n    isMsDown: false,\n    msDownX: 0,\n    msDownY: 0,\n    windowW: 0,\n    windowH: 0,\n  }\n\n  const handleLyricDown = (target, x, y) => {\n    winEvent.isMsDown = true\n    winEvent.msDownX = x\n    winEvent.msDownY = y\n    winEvent.windowW = window.innerWidth\n    winEvent.windowH = window.innerHeight\n    // https://github.com/lyswhut/lx-music-desktop/issues/2244\n    if (isWin) setWindowResizeable(false)\n  }\n  const handleLyricMouseDown = event => {\n    console.log(event.target, event.currentTarget)\n    if (event.target !== event.currentTarget) return\n    handleLyricDown(event.target, event.clientX, event.clientY)\n  }\n  const handleLyricTouchStart = event => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      if (touch.target !== touch.currentTarget) return\n      handleLyricDown(event.target, touch.clientX, touch.clientY)\n    }\n  }\n  const handleMouseMsUp = () => {\n    winEvent.isMsDown = false\n    if (isWin) setWindowResizeable(true)\n  }\n\n  const handleMove = (x, y) => {\n    if (!winEvent.isMsDown) return\n    // https://github.com/lyswhut/lx-music-desktop/issues/2244\n    if (isWin) {\n      setWindowBounds({\n        x: x - winEvent.msDownX,\n        y: y - winEvent.msDownY,\n        w: winEvent.windowW,\n        h: winEvent.windowH,\n      })\n    } else {\n      setWindowBounds({\n        x: x - winEvent.msDownX,\n        y: y - winEvent.msDownY,\n        w: window.innerWidth,\n        h: window.innerHeight,\n      })\n    }\n  }\n  const handleMouseMsMove = event => {\n    handleMove(event.clientX, event.clientY)\n  }\n  const handleTouchMove = (e) => {\n    if (e.changedTouches.length) {\n      const touch = e.changedTouches[0]\n      handleMove(touch.clientX, touch.clientY)\n    }\n  }\n\n  onMounted(() => {\n    document.addEventListener('mousemove', handleMouseMsMove)\n    document.addEventListener('mouseup', handleMouseMsUp)\n    document.addEventListener('touchmove', handleTouchMove)\n    document.addEventListener('touchend', handleMouseMsUp)\n  })\n\n  onBeforeUnmount(() => {\n    document.removeEventListener('mousemove', handleMouseMsMove)\n    document.removeEventListener('mouseup', handleMouseMsUp)\n    document.removeEventListener('touchmove', handleTouchMove)\n    document.removeEventListener('touchend', handleMouseMsUp)\n  })\n\n  return {\n    handleLyricMouseDown,\n    handleLyricTouchStart,\n  }\n}\n"
  },
  {
    "path": "src/renderer-lyric/core/lyric.ts",
    "content": "import Lyric from '@common/utils/lyric-font-player'\nimport { markRawList } from '@common/utils/vueTools'\nimport { setLines, setOffset, setTempOffset, setText, lyrics } from '@lyric/store/lyric'\nimport { musicInfo, setting } from '@lyric/store/state'\n\nlet lrc: Lyric\n\nexport const init = () => {\n  lrc = new Lyric({\n    shadowContent: true,\n    activeLineClassName: 'active',\n    rate: setting['player.playbackRate'],\n    isVertical: setting['desktopLyric.direction'] == 'vertical',\n    onPlay(line, text) {\n      setText(text, Math.max(line, 0))\n      // console.log(line, text)\n    },\n    onSetLyric(lines, offset) { // listening lyrics seting event\n      // console.log(lines) // lines is array of all lyric text\n      setLines(markRawList([...lines]))\n      setText(lines[0] ?? '', 0)\n      setOffset(offset) // 歌词延迟\n      setTempOffset(0) // 重置临时延迟\n    },\n    onUpdateLyric(lines) {\n      setLines(markRawList([...lines]))\n      setText(lines[0] ?? '', 0)\n    },\n  })\n}\n\nexport const setLyricOffset = (offset: number) => {\n  setTempOffset(offset)\n  lrc.setOffset(offset)\n}\n\nexport const setPlaybackRate = (rate: number) => {\n  lrc.setPlaybackRate(rate)\n}\n\nexport const setLyric = () => {\n  if (!musicInfo.id) return\n  const extendedLyrics = []\n  if (setting['player.isShowLyricRoma'] && lyrics.rlyric) extendedLyrics.push(lyrics.rlyric)\n  if (setting['player.isShowLyricTranslation'] && lyrics.tlyric) extendedLyrics.push(lyrics.tlyric)\n  if (setting['player.isSwapLyricTranslationAndRoma']) extendedLyrics.reverse()\n  lrc.setLyric(\n    setting['player.isPlayLxlrc'] && lyrics.lxlyric ? lyrics.lxlyric : lyrics.lyric,\n    extendedLyrics,\n  )\n}\n\n\nexport const play = (time: number) => {\n  if (!lyrics.lyric) return\n  lrc.play(time)\n}\n\nexport const pause = () => {\n  lrc.pause()\n}\n\nexport const stop = () => {\n  lrc.setLyric('')\n  // setLines([])\n  setText('', 0)\n}\n\nexport const setVertical = (isVertical: boolean) => {\n  lrc.setVertical(isVertical)\n}\n"
  },
  {
    "path": "src/renderer-lyric/core/mainWindowChannel.ts",
    "content": "import { onProvideMainWindowChannel } from '@lyric/utils/ipc'\nimport { onBeforeUnmount } from '@common/utils/vueTools'\nimport { setMusicInfo, setIsPlay } from '../store/action'\nimport { pause, play, setLyric, setLyricOffset, setPlaybackRate, stop } from './lyric'\nimport { lyrics } from '@lyric/store/lyric'\n\nlet mainWindowPort: Electron.IpcRendererEvent['ports'][0] | null = null\nexport const sendDesktopLyricInfo = (info: LX.DesktopLyric.WinMainActions) => {\n  if (mainWindowPort == null) return\n  mainWindowPort.postMessage({ action: info })\n}\n\nconst listeners: Array<(event: LX.DesktopLyric.LyricActions) => void> = []\n\nconst handleDesktopLyricMessage = (event: LX.DesktopLyric.LyricActions) => {\n  switch (event.action) {\n    case 'set_info':\n      setMusicInfo({\n        id: event.data.id,\n        singer: event.data.singer,\n        name: event.data.name,\n        album: event.data.album,\n      })\n      lyrics.lyric = event.data.lrc ?? ''\n      lyrics.tlyric = event.data.tlrc\n      lyrics.rlyric = event.data.rlrc\n      lyrics.lxlyric = event.data.lxlrc\n      setLyric()\n      if (event.data.isPlay) {\n        setImmediate(() => {\n          getStatus()\n        })\n      }\n      break\n    case 'set_lyric':\n      lyrics.lyric = event.data.lrc ?? ''\n      lyrics.tlyric = event.data.tlrc\n      lyrics.rlyric = event.data.rlrc\n      lyrics.lxlyric = event.data.lxlrc\n      setLyric()\n      break\n    case 'set_status':\n      setIsPlay(event.data.isPlay)\n      if (event.data.isPlay) play(event.data.played_time)\n      else pause()\n      break\n    case 'set_offset':\n      setLyricOffset(event.data)\n      break\n    case 'set_playbackRate':\n      setPlaybackRate(event.data)\n      break\n    case 'set_pause':\n      setIsPlay(false)\n      pause()\n      break\n    case 'set_play':\n      setIsPlay(true)\n      play(event.data)\n      break\n    case 'set_stop':\n      setIsPlay(false)\n      stop()\n      break\n    default:\n      for (const listener of listeners) {\n        listener(event)\n      }\n      break\n  }\n}\n\nexport const init = () => {\n  onProvideMainWindowChannel(({ event }) => {\n    const [port] = event.ports\n    mainWindowPort = port\n\n    // ... register a handler to receive results ...\n    port.onmessage = ({ data }) => {\n      handleDesktopLyricMessage(data)\n      // console.log('received result:', data)\n    }\n    // ... and start sending it work!\n\n    port.onmessageerror = (event) => {\n      console.log('onmessageerror', event)\n    }\n\n    getInfo()\n  })\n}\n\nexport const useEvent = (listener: (event: LX.DesktopLyric.LyricActions) => void) => {\n  listeners.push(listener)\n\n  onBeforeUnmount(() => {\n    listeners.splice(listeners.indexOf(listener), 1)\n  })\n}\n\nexport const getInfo = () => {\n  sendDesktopLyricInfo('get_info')\n}\n\nexport const getAnalyserDataArray = () => {\n  sendDesktopLyricInfo('get_analyser_data_array')\n}\n\nexport const getStatus = () => {\n  sendDesktopLyricInfo('get_status')\n}\n"
  },
  {
    "path": "src/renderer-lyric/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" style=\"background-color: transparent;\">\n  <head>\n    <meta charset=\"UTF-8\"/>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n    <title>Lyric - LX Music</title>\n  </head>\n  <body id=\"body\" style=\"background-color: transparent;\">\n    <div id=\"root\"></div>\n    <script>\n      const formatLang = (lang = 'en') => {\n        if (lang === 'zh-cn') return 'zh-Hans'\n        if (lang === 'zh-tw') return 'zh-Hant'\n        return lang.split('-')[0]\n      }\n      window.setLang = (lang = navigator.language.toLocaleLowerCase()) => {\n        document.documentElement.setAttribute('lang', formatLang(lang))\n      }\n      window.setLang()\n      window.os = /os=(\\w+)/.exec(window.location.search)[1]\n      document.documentElement.classList.add(window.os)\n      window.dom_style_theme = document.createElement('style')\n      window.dom_style_lyric = document.createElement('style')\n\n      window.setTheme = colors => {\n        window.dom_style_theme.innerText = `:root {${(Object.entries(colors)).map(([key, value]) => `${key}:${value};`).join('')}}`\n      }\n      window.setLyricColor = colors => {\n        console.log(colors)\n        window.dom_style_lyric.innerText = `:root {${(Object.entries(colors)).map(([key, value]) => `${key}:${value};`).join('')}}`\n      }\n      document.body.appendChild(window.dom_style_lyric)\n\n      const applyThemeColor = (theme) => {\n        theme = JSON.parse(decodeURIComponent(theme))\n        window.setTheme(theme.colors)\n        document.body.appendChild(window.dom_style_theme)\n      }\n      if (/theme=(.+)(#|$)/.test(window.location.search)) applyThemeColor(RegExp.$1)\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer-lyric/main.ts",
    "content": "import { createApp } from 'vue'\n\nimport { i18nPlugin } from './plugins/i18n'\n\nimport mountComponents from './components'\n\nimport App from './App.vue'\n\nimport '@root/common/error'\nimport { getSetting, onMainWindowInited, onSettingChanged, sendConnectMainWindowEvent } from './utils/ipc'\nimport { initSetting, mergeSetting } from './store/action'\nimport { init as initMainWindowChannel } from './core/mainWindowChannel'\n\nwindow.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS\n\nvoid getSetting().then((setting) => {\n  // window.lx.appSetting = setting\n  // Set language automatically\n  const languageId = setting['common.langId']\n  if (window.i18n.locale !== languageId && languageId != null) {\n    window.i18n.setLanguage(languageId)\n    window.setLang(languageId)\n  }\n\n  // store.commit('setSetting', setting)\n  initSetting(setting)\n\n  onSettingChanged(({ params: setting }) => {\n    // console.log('onSettingChanged', setting)\n    mergeSetting(setting)\n  })\n  onMainWindowInited(() => {\n    sendConnectMainWindowEvent()\n  })\n  initMainWindowChannel()\n\n  const app = createApp(App)\n  app.use(i18nPlugin)\n  mountComponents(app)\n  app.mount('#root')\n})\n"
  },
  {
    "path": "src/renderer-lyric/plugins/i18n.ts",
    "content": "import type { I18n } from '@root/lang'\nimport { createI18n, i18nPlugin, useI18n } from '@root/lang'\n\nwindow.i18n = createI18n()\n\nexport {\n  i18nPlugin,\n  useI18n,\n}\n\nexport type { I18n }\n"
  },
  {
    "path": "src/renderer-lyric/store/action.ts",
    "content": "import { setting, musicInfo, isPlay } from './state'\nimport { updateSetting as saveSetting } from '@lyric/utils/ipc'\n\nexport const initSetting = (newSetting: LX.DesktopLyric.Config) => {\n  mergeSetting(newSetting)\n}\n\nexport const mergeSetting = (newSetting: Partial<LX.DesktopLyric.Config>) => {\n  for (const [key, value] of Object.entries(newSetting)) {\n    // @ts-expect-error\n    setting[key] = value\n  }\n}\n\nexport const updateSetting = (setting: Partial<LX.DesktopLyric.Config>) => {\n  void saveSetting(setting)\n}\n\ntype MusicInfoKeys = keyof typeof musicInfo\nconst musicInfoKeys: MusicInfoKeys[] = Object.keys(musicInfo) as MusicInfoKeys[]\n\nexport const setMusicInfo = (_musicInfo: Partial<typeof musicInfo>) => {\n  for (const key of musicInfoKeys) {\n    const val = _musicInfo[key]\n    if (val !== undefined) {\n      // @ts-expect-error\n      musicInfo[key] = val\n    }\n  }\n}\n\nexport const setIsPlay = (_status: boolean) => {\n  isPlay.value = _status\n}\n"
  },
  {
    "path": "src/renderer-lyric/store/lyric.ts",
    "content": "import { markRaw, reactive } from '@common/utils/vueTools'\n\n\nexport const lyrics = markRaw<{\n  lyric: string\n  tlyric: string | null\n  rlyric: string | null\n  lxlyric: string | null\n}>({\n  lyric: '',\n  tlyric: '',\n  rlyric: '',\n  lxlyric: '',\n})\n\ninterface Line {\n  text: string\n  time: number\n  extendedLyrics: string[]\n  dom_line: HTMLDivElement\n}\n\nexport const lyric = reactive<{\n  lines: Line[]\n  text: string\n  line: number\n  offset: number // 歌词延迟\n  tempOffset: number // 歌词临时延迟\n}>({\n  lines: [],\n  text: '',\n  line: 0,\n  offset: 0, // 歌词延迟\n  tempOffset: 0, // 歌词临时延迟\n})\n\nexport const setLines = (lines: Line[]) => {\n  if (!lines.length && !lyric.lines.length) return\n  lyric.lines = lines\n}\nexport const setText = (text: string, line: number) => {\n  lyric.text = text\n  lyric.line = line\n}\nexport const setOffset = (offset: number) => {\n  lyric.offset = offset\n}\nexport const setTempOffset = (offset: number) => {\n  lyric.tempOffset = offset\n}\n"
  },
  {
    "path": "src/renderer-lyric/store/state.ts",
    "content": "import { ref, shallowReactive } from '@common/utils/vueTools'\n\nexport const setting = shallowReactive<LX.DesktopLyric.Config>({\n  'desktopLyric.enable': false,\n  'desktopLyric.isLock': false,\n  'desktopLyric.isAlwaysOnTop': false,\n  'desktopLyric.isAlwaysOnTopLoop': false,\n  'desktopLyric.isShowTaskbar': true,\n  'desktopLyric.pauseHide': false,\n  'desktopLyric.audioVisualization': false,\n  'desktopLyric.width': 450,\n  'desktopLyric.height': 300,\n  'desktopLyric.x': null,\n  'desktopLyric.y': null,\n  'desktopLyric.isLockScreen': true,\n  'desktopLyric.isDelayScroll': true,\n  'desktopLyric.scrollAlign': 'center',\n  'desktopLyric.isHoverHide': false,\n  'desktopLyric.direction': 'horizontal',\n  'desktopLyric.style.align': 'center',\n  'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',\n  'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',\n  'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.14)',\n  'desktopLyric.style.font': '',\n  'desktopLyric.style.fontSize': 20,\n  'desktopLyric.style.lineGap': 15,\n  // 'desktopLyric.style.fontWeight': true,\n  'desktopLyric.style.opacity': 95,\n  'desktopLyric.style.ellipsis': false,\n  'desktopLyric.style.isFontWeightFont': false,\n  'desktopLyric.style.isFontWeightLine': false,\n  'desktopLyric.style.isFontWeightExtended': false,\n  'desktopLyric.style.isZoomActiveLrc': true,\n  'common.langId': 'zh-cn',\n  'player.isShowLyricTranslation': false,\n  'player.isShowLyricRoma': false,\n  'player.isSwapLyricTranslationAndRoma': false,\n  'player.isPlayLxlrc': false,\n  'player.playbackRate': 1,\n})\n\n// export const themeList = markRaw([\n//   {\n//     id: 0,\n//     className: 'green',\n//   },\n//   {\n//     id: 1,\n//     className: 'yellow',\n//   },\n//   {\n//     id: 2,\n//     className: 'blue',\n//   },\n//   {\n//     id: 3,\n//     className: 'red',\n//   },\n//   {\n//     id: 4,\n//     className: 'pink',\n//   },\n//   {\n//     id: 5,\n//     className: 'purple',\n//   },\n//   {\n//     id: 6,\n//     className: 'orange',\n//   },\n//   {\n//     id: 7,\n//     className: 'grey',\n//   },\n//   {\n//     id: 8,\n//     className: 'ming',\n//   },\n//   {\n//     id: 9,\n//     className: 'blue2',\n//   },\n// ])\n\n// export type Status = 'playing' | 'paused' | 'stopped'\n\n// export const status = ref<Status>('stopped')\nexport const isPlay = ref(false)\n\nexport const musicInfo = shallowReactive<{\n  id: string | null\n  name: string\n  singer: string\n  album: string | null\n}>({\n  id: null,\n  name: '^',\n  singer: '^',\n  album: null,\n})\n"
  },
  {
    "path": "src/renderer-lyric/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"compilerOptions\": {\n    \"isolatedModules\": true,\n    \"paths\": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */\n      \"@common/*\": [\"common/*\"],\n      // \"@renderer/*\": [\"renderer/*\"],\n      \"@lyric/*\": [\"renderer-lyric/*\"],\n      \"@static/*\": [\"static/*\"],\n      \"@root/*\": [\"./*\"],\n    },\n    \"typeRoots\": [                                       /* Specify multiple folders that act like './node_modules/@types'. */\n      \"./types\"\n    ],\n  },\n  // \"vueCompilerOptions\": {\n  //   \"plugins\": [\n  //     \"@volar/vue-language-plugin-pug\"\n  //   ]\n  // }\n  // \"include\": [\n\t//   \"./**/*.ts\",\n\t//   // \"./**/*.js\",\n\t//   \"./**/*.vue\",\n\t//   \"./**/*.json\",\n\t// ],\n}\n"
  },
  {
    "path": "src/renderer-lyric/types/app.d.ts",
    "content": "import { type I18n } from '@lyric/plugins/i18n'\n\ndeclare global {\n  interface Window {\n    ELECTRON_DISABLE_SECURITY_WARNINGS?: string\n\n    i18n: I18n\n\n    lxData: any\n\n    setTheme: (colors: Record<string, string>) => void\n    setLang: (lang?: string) => void\n    setLyricColor: (colors: Record<string, string>) => void\n    os: 'windows' | 'linux' | 'mac'\n  }\n\n  namespace LX {\n\n  }\n\n}\n\n\n// declare const ELECTRON_DISABLE_SECURITY_WARNINGS: string\n// declare const userApiPath: string\n"
  },
  {
    "path": "src/renderer-lyric/types/common.d.ts",
    "content": "// import '@common/types/app_setting'\n// import '@common/types/common'\n// import '@common/types/user_api'\n// import '@common/types/sync'\n// import '@common/types/music'\n// import '@common/types/list'\n// import '@common/types/download_list'\n// import '@common/types/player'\nimport '@common/types/shims_vue'\n// import '@common/types/utils'\nimport '@common/types/theme'\nimport '@common/types/desktop_lyric'\nimport '@common/types/ipc_renderer'\n"
  },
  {
    "path": "src/renderer-lyric/useApp/useCommon.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport { setting } from '@lyric/store/state'\n\nexport default () => {\n  watch(() => setting['common.langId'], (id) => {\n    if (!id) return\n    window.i18n.setLanguage(id)\n    window.setLang(id)\n  })\n}\n"
  },
  {
    "path": "src/renderer-lyric/useApp/useHoverHide.ts",
    "content": "import { computed, ref } from '@common/utils/vueTools'\nimport { setting } from '@lyric/store/state'\n\nlet mouseCheckTools: {\n  x: number\n  y: number\n  preX: number\n  preY: number\n  timeout: NodeJS.Timeout | null\n  handleCheck: (setShow: () => void) => void\n  handleMove: (x: number, y: number, setShow: () => void) => void\n  startTimeout: (setShow: () => void) => void\n  stopTimeout: () => void\n} = {\n  x: 0,\n  y: 0,\n  preX: 0,\n  preY: 0,\n  timeout: null,\n  handleCheck(setShow: () => void) {\n    let xDiff = Math.abs(this.x - this.preX)\n    let yDiff = Math.abs(this.y - this.preY)\n    if (xDiff > 8) {\n      if (this.x > this.preX) {\n        if (this.x + xDiff * 1.25 > window.innerWidth - 16) {\n          setShow()\n          return\n        }\n      } else {\n        if (this.x - xDiff * 1.25 < 8) {\n          setShow()\n          return\n        }\n      }\n    }\n    if (yDiff > 8) {\n      if (this.y > this.preY) {\n        if (this.y + yDiff * 1.25 > window.innerHeight - 16) {\n          setShow()\n        }\n      } else {\n        if (this.y - yDiff * 1.25 < 8) {\n          setShow()\n        }\n      }\n    }\n\n    // setShow(false)\n  },\n  handleMove(x: number, y: number, setShow: () => void) {\n    // console.log(x, y, this.x, this.y)\n    this.preX = this.x\n    this.preY = this.y\n    this.x = x\n    this.y = y\n    this.startTimeout(setShow)\n  },\n  startTimeout(setShow: () => void) {\n    this.stopTimeout()\n    this.timeout = setTimeout(this.handleCheck.bind(this), 200, setShow)\n  },\n  stopTimeout() {\n    if (!this.timeout) return\n    clearTimeout(this.timeout)\n    this.timeout = null\n  },\n}\n\nexport default () => {\n  const isMouseEnter = ref(false)\n\n  const isHoverHide = computed(() => {\n    return setting['desktopLyric.isLock'] && setting['desktopLyric.isHoverHide']\n  })\n\n  const handleMouseMoveMain = (event: MouseEvent) => {\n    if (!isHoverHide.value) return\n    handleMouseEnter()\n    mouseCheckTools.handleMove(event.clientX, event.clientY, () => {\n      handleMouseLeave()\n    })\n  }\n  const handleMouseEnter = () => {\n    if (!isHoverHide.value || isMouseEnter.value) return\n    isMouseEnter.value = true\n  }\n  const handleMouseLeave = () => {\n    if (!isHoverHide.value) return\n    isMouseEnter.value = false\n    mouseCheckTools.stopTimeout()\n  }\n\n  return {\n    isMouseEnter,\n    isHoverHide,\n    handleMouseMoveMain,\n  }\n}\n"
  },
  {
    "path": "src/renderer-lyric/useApp/useLyric.ts",
    "content": "import { watch } from '@common/utils/vueTools'\nimport { setLyric, setVertical, setPlaybackRate } from '@lyric/core/lyric'\nimport { getStatus } from '@lyric/core/mainWindowChannel'\nimport { isPlay, setting } from '@lyric/store/state'\n\nexport default () => {\n  watch(() => setting['player.isShowLyricTranslation'], setLyric)\n  watch(() => setting['player.isShowLyricRoma'], setLyric)\n  watch(() => setting['player.isSwapLyricTranslationAndRoma'], setLyric)\n  watch(() => setting['player.isPlayLxlrc'], setLyric)\n  watch(() => setting['player.playbackRate'], (rate) => {\n    setPlaybackRate(rate)\n    if (isPlay.value) {\n      setTimeout(() => {\n        getStatus()\n      })\n    }\n  })\n  watch(() => setting['desktopLyric.direction'], (direction) => {\n    setVertical(direction == 'vertical')\n    // if (isPlay.value)\n  })\n}\n"
  },
  {
    "path": "src/renderer-lyric/useApp/usePauseHide.ts",
    "content": "import { ref, watch } from '@common/utils/vueTools'\nimport { isPlay, setting } from '@lyric/store/state'\n\nexport default () => {\n  let unWatch: (() => void) | null = null\n  let isHide = ref(false)\n  let timeout: NodeJS.Timeout | null = null\n  const clearIntv = () => {\n    if (!timeout) return\n    clearTimeout(timeout)\n    timeout = null\n  }\n  watch(() => setting['desktopLyric.pauseHide'], (enable) => {\n    if (enable) {\n      unWatch = watch(isPlay, (isPlay) => {\n        clearIntv()\n        if (isPlay) {\n          isHide.value &&= false\n        } else {\n          timeout = setTimeout(() => {\n            timeout = null\n            isHide.value = true\n          }, 200)\n        }\n      }, {\n        immediate: true,\n      })\n    } else {\n      clearIntv()\n      isHide.value &&= false\n      if (unWatch) {\n        unWatch()\n        unWatch = null\n      }\n    }\n  }, {\n    immediate: true,\n  })\n\n  return isHide\n}\n"
  },
  {
    "path": "src/renderer-lyric/useApp/useTheme.ts",
    "content": "import { onBeforeUnmount, watch } from '@common/utils/vueTools'\nimport { setting } from '@lyric/store/state'\nimport { onThemeChange } from '@lyric/utils/ipc'\nimport { RGB_Alpha_Shade } from '@common/theme/colorUtils'\n\nexport default () => {\n  const rThemeChange = onThemeChange(({ params: setting }) => {\n    window.setTheme(setting.theme.colors)\n  })\n  watch(() => [setting['desktopLyric.style.lyricUnplayColor'], setting['desktopLyric.style.lyricPlayedColor'], setting['desktopLyric.style.lyricShadowColor']], ([unplayColor, playedColor, shadowColor]) => {\n    window.setLyricColor({\n      '--color-lyric-unplay': unplayColor,\n      '--color-lyric-played': playedColor,\n      '--color-lyric-shadow': shadowColor,\n      '--color-lyric-shadow-font-mode': RGB_Alpha_Shade(0.49, shadowColor),\n    })\n  }, {\n    immediate: true,\n  })\n\n  onBeforeUnmount(() => {\n    rThemeChange()\n  })\n}\n"
  },
  {
    "path": "src/renderer-lyric/useApp/useWindowSize.ts",
    "content": "import { setting } from '@lyric/store/state'\nimport { onBeforeUnmount, onMounted } from '@common/utils/vueTools'\nimport { setWindowBounds } from '@lyric/utils/ipc'\n\ntype Origin = 'left'\n| 'top'\n| 'right'\n| 'bottom'\n| 'top-left'\n| 'top-right'\n| 'bottom-left'\n| 'bottom-right'\n\nexport default () => {\n  const resize: {\n    origin: Origin | null\n    msDownX: number\n    msDownY: number\n  } = {\n    origin: null,\n    msDownX: 0,\n    msDownY: 0,\n  }\n\n  const handleMove = (clientX: number, clientY: number) => {\n    if (!resize.origin || setting['desktopLyric.isLock']) return\n    // if (!event.target.classList.contains('resize-' + resize.origin)) return\n    // console.log(event.target)\n    let bounds: LX.DesktopLyric.NewBounds = {\n      w: 0,\n      h: 0,\n      x: 0,\n      y: 0,\n    }\n    let temp\n    switch (resize.origin) {\n      case 'left':\n        temp = clientX - resize.msDownX\n        bounds.w = -temp\n        bounds.x = temp\n        break\n      case 'right':\n        bounds.w = clientX - resize.msDownX\n        resize.msDownX += bounds.w\n        break\n      case 'top':\n        temp = clientY - resize.msDownY\n        bounds.y = temp\n        bounds.h = -temp\n        break\n      case 'bottom':\n        bounds.h = clientY - resize.msDownY\n        resize.msDownY += bounds.h\n        break\n      case 'top-left':\n        temp = clientX - resize.msDownX\n        bounds.w = -temp\n        bounds.x = temp\n        temp = clientY - resize.msDownY\n        bounds.y = temp\n        bounds.h = -temp\n        break\n      case 'top-right':\n        temp = clientY - resize.msDownY\n        bounds.y = temp\n        bounds.h = -temp\n        bounds.w = clientX - resize.msDownX\n        resize.msDownX += bounds.w\n        break\n      case 'bottom-left':\n        temp = clientX - resize.msDownX\n        bounds.w = -temp\n        bounds.x = temp\n        bounds.h = clientY - resize.msDownY\n        resize.msDownY += bounds.h\n        break\n      case 'bottom-right':\n        bounds.w = clientX - resize.msDownX\n        resize.msDownX += bounds.w\n        bounds.h = clientY - resize.msDownY\n        resize.msDownY += bounds.h\n        break\n    }\n    // console.log(bounds)\n    bounds.w = window.innerWidth + bounds.w\n    bounds.h = window.innerHeight + bounds.h\n    setWindowBounds(bounds)\n  }\n\n  const handleDown = (origin: Origin, clientX: number, clientY: number) => {\n    handleMouseUp()\n    resize.origin = origin\n    resize.msDownX = clientX\n    resize.msDownY = clientY\n  }\n  const handleMouseUp = () => {\n    resize.origin = null\n  }\n\n  const handleMouseDown = (origin: Origin, event: MouseEvent) => {\n    handleDown(origin, event.clientX, event.clientY)\n  }\n  const handleTouchDown = (origin: Origin, event: TouchEvent) => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      handleDown(origin, touch.clientX, touch.clientY)\n    }\n  }\n\n  const handleMouseMove = (event: MouseEvent) => {\n    handleMove(event.clientX, event.clientY)\n  }\n  const handleTouchMove = (event: TouchEvent) => {\n    if (event.changedTouches.length) {\n      const touch = event.changedTouches[0]\n      handleMove(touch.clientX, touch.clientY)\n    }\n  }\n\n\n  onMounted(() => {\n    document.addEventListener('mousemove', handleMouseMove)\n    document.addEventListener('mouseup', handleMouseUp)\n    document.addEventListener('touchmove', handleTouchMove)\n    document.addEventListener('touchend', handleMouseUp)\n  })\n  onBeforeUnmount(() => {\n    document.removeEventListener('mousemove', handleMouseMove)\n    document.removeEventListener('mouseup', handleMouseUp)\n    document.removeEventListener('touchmove', handleTouchMove)\n    document.removeEventListener('touchend', handleMouseUp)\n  })\n\n  return {\n    handleMouseDown,\n    handleTouchDown,\n  }\n}\n"
  },
  {
    "path": "src/renderer-lyric/utils/ipc.ts",
    "content": "import { rendererSend, rendererInvoke, rendererOn, rendererOff } from '@common/rendererIpc'\nimport { CMMON_EVENT_NAME, WIN_LYRIC_RENDERER_EVENT_NAME } from '@common/ipcNames'\n\ntype RemoveListener = () => void\n\nexport const getSetting = async() => {\n  return rendererInvoke<LX.DesktopLyric.Config>(WIN_LYRIC_RENDERER_EVENT_NAME.get_config)\n}\nexport const updateSetting = async(setting: Partial<LX.DesktopLyric.Config>) => {\n  await rendererInvoke(WIN_LYRIC_RENDERER_EVENT_NAME.set_config, setting)\n}\nexport const onSettingChanged = (listener: LX.IpcRendererEventListenerParams<Partial<LX.DesktopLyric.Config>>): RemoveListener => {\n  rendererOn<Partial<LX.DesktopLyric.Config>>(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, listener)\n  return () => {\n    rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.on_config_change, listener)\n  }\n}\nexport const setWindowBounds = (bounds: LX.DesktopLyric.NewBounds) => {\n  rendererSend<LX.DesktopLyric.NewBounds>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, bounds)\n}\nlet previousResizable: boolean | null = null\nexport const setWindowResizeable = (resizable: boolean) => {\n  if (previousResizable === resizable) return\n  previousResizable = resizable\n  // https://github.com/electron/electron/issues/48352\n  // rendererSend<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, resizable)\n}\n\nexport const sendConnectMainWindowEvent = () => {\n  rendererSend(WIN_LYRIC_RENDERER_EVENT_NAME.request_main_window_channel)\n}\nexport const onProvideMainWindowChannel = (listener: LX.IpcRendererEventListener): RemoveListener => {\n  rendererOn(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, listener)\n  return () => {\n    rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.provide_main_window_channel, listener)\n  }\n}\nexport const onMainWindowInited = (listener: LX.IpcRendererEventListener): RemoveListener => {\n  rendererOn(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited, listener)\n  return () => {\n    rendererOff(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited, listener)\n  }\n}\n\n/**\n * On Theme Change\n * @param listener LX.IpcRendererEventListenerParams<shouldUseDarkColors: boolean>\n * @returns RemoveListener Fn\n */\nexport const onThemeChange = (listener: LX.IpcRendererEventListenerParams<LX.ThemeSetting>): RemoveListener => {\n  rendererOn(CMMON_EVENT_NAME.theme_change, listener)\n  return () => {\n    rendererOff(CMMON_EVENT_NAME.theme_change, listener)\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  // https://github.com/tsconfig/bases#recommended-tsconfigjson\n  \"extends\": \"@tsconfig/recommended/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"allowJs\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"resolveJsonModule\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./src\",                                     /* Specify the base directory to resolve non-relative module names. */\n    // \"paths\": {                                           /* Specify a set of entries that re-map imports to additional lookup locations. */\n    //   \"@common/*\": [\"common/*\"],\n    //   \"@renderer/*\": [\"renderer/*\"],\n    //   \"@main/*\": [\"main/*\"],\n    //   // \"@lyric/*\": [\"renderer-lyric/*\"],\n    //   \"@static/*\": [\"static/*\"],\n    //   \"@root/*\": [\"./*\"],\n    // },\n  },\n}\n"
  }
]