[
  {
    "path": ".bundle/config",
    "content": "BUNDLE_PATH: \"vendor/bundle\"\nBUNDLE_FORCE_RUBY_PLATFORM: 1\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.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  'react/display-name': 'off',\n  'react/prop-types': 'off',\n}\n\nmodule.exports = {\n  root: true,\n  extends: [\n    'standard',\n    'plugin:react/recommended',\n    'plugin:react-hooks/recommended',\n    'plugin:react/jsx-runtime',\n  ],\n  plugins: [\n    'react',\n  ],\n  rules: baseRule,\n  parser: '@babel/eslint-parser',\n  overrides: [\n    {\n      files: ['*.ts', '*.tsx'],\n      extends: ['standard-with-typescript'],\n      rules: {\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': [\n          1,\n          {\n            allowBoolean: true,\n          },\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/comma-dangle': 'off',\n        '@typescript-eslint/no-dynamic-delete': 'off',\n        '@typescript-eslint/ban-ts-comment': 'off',\n        '@typescript-eslint/ban-types': 'off',\n      },\n      parserOptions: {\n        project: './tsconfig.json',\n      },\n    },\n  ],\n  settings: {\n    react: {\n      version: 'detect', // React version. \"detect\" automatically picks the version you have installed.\n      // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.\n      // It will default to \"latest\" and warn if missing, and to \"detect\" in the future\n    },\n  },\n  ignorePatterns: [\n    'node_modules',\n    '*.min.js',\n    'test.js',\n    '*Test.ts',\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/mobile/faq)，但没有找到解决方案。\n        required: true\n      - label: 我已搜索 [Issue 列表](https://github.com/lyswhut/lx-music-mobile/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: 例如 1.6.0 arm64-v8a\n  validations:\n    required: true\n- type: input\n  id: last-known-working-version\n  attributes:\n    label: 最后正常的版本\n    description: 如果有，请在此处填写最后正常的版本。\n    placeholder: 1.5.0 arm64-v8a\n- type: input\n  id: operating-system-version\n  attributes:\n    label: 操作系统版本\n    description: |\n      你使用什么版本的操作系统？\n      在 Android 上，通常单击「系统设置 > 关于」。\n      各类厂商定制 Android 系统的位置可能有所不同。\n    placeholder: 例如 Android 15\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/mobile/faq)，但没有找到解决方案。\n        required: true\n      - label: 我已搜索 [Issue 列表](https://github.com/lyswhut/lx-music-mobile/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  validations:\n    required: false\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 Env\n\nruns:\n  using: composite\n  steps:\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version-file: .nvmrc\n\n    - name: Setup Java Env\n      uses: actions/setup-java@v4\n      with:\n        distribution: 'microsoft'\n        java-version: '17'\n        cache: gradle\n\n    - name: Get npm cache directory\n      id: npm-cache-dir\n      shell: bash\n      run: echo \"cachedir=$(npm config get cache)\" >> ${GITHUB_OUTPUT}\n\n    - name: Cache node modules\n      id: cache-npm\n      uses: actions/cache@v4\n      with:\n        # npm cache files are stored in `~/.npm` on Linux/macOS\n        path: ${{ steps.npm-cache-dir.outputs.cachedir }}\n        key: ${{ runner.os }}-npm-cache-${{ hashFiles('package-lock.json') }}\n        restore-keys: |\n          ${{ runner.os }}-npm-cache-\n\n    - name: Install dependencies\n      shell: bash\n      run: npm ci\n"
  },
  {
    "path": ".github/actions/upload-artifact/action.yml",
    "content": "name: Setup\ndescription: Setup Env\n\nruns:\n  using: composite\n  steps:\n    - name: Upload Artifact arm64-v8a\n      if: env.PACKAGE_TYPE != 'Android_5'\n      uses: actions/upload-artifact@v4\n      with:\n        name: app-v${{ env.PACKAGE_VERSION }}-arm64-v8a-release\n        path: android/app/build/outputs/apk/release/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-arm64-v8a.apk\n\n    - name: Upload Artifact armeabi-v7a\n      if: env.PACKAGE_TYPE == null\n      uses: actions/upload-artifact@v4\n      with:\n        name: app-v${{ env.PACKAGE_VERSION }}-armeabi-v7a-release\n        path: android/app/build/outputs/apk/release/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-armeabi-v7a.apk\n\n    - name: Upload Artifact universal\n      if: env.PACKAGE_TYPE != 'Android_SL'\n      uses: actions/upload-artifact@v4\n      with:\n        name: app-v${{ env.PACKAGE_VERSION }}-universal-release\n        path: android/app/build/outputs/apk/release/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-universal.apk\n\n    - name: Upload Artifact x86_64\n      if: env.PACKAGE_TYPE == null\n      uses: actions/upload-artifact@v4\n      with:\n        name: app-v${{ env.PACKAGE_VERSION }}-x86_64-release\n        path: android/app/build/outputs/apk/release/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-x86_64.apk\n\n    - name: Upload Artifact x86\n      if: env.PACKAGE_TYPE == null\n      uses: actions/upload-artifact@v4\n      with:\n        name: app-v${{ env.PACKAGE_VERSION }}-x86-release\n        path: android/app/build/outputs/apk/release/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-x86.apk\n"
  },
  {
    "path": ".github/workflows/beta-pack.yml",
    "content": "name: Build Beta\n\non:\n  push:\n    branches:\n      - beta\n\njobs:\n  Android:\n    name: Android\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup Env\n        uses: ./.github/actions/setup\n\n      - name: Build Packages\n        env:\n          DISABLE_SVG: 1\n        shell: bash\n        run: |\n          cd android\n          echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}\n          ./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'\n          rm -f app/${{ secrets.KEYSTORE_STORE_FILE }}\n\n      # Push tag to GitHub if package.json version's tag is not tagged\n      - name: Get package version\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Generate file MD5\n        run: |\n          cd android/app/build/outputs/apk/release\n          md5sum *.apk\n\n      - name: Upload Artifact\n        uses: ./.github/actions/upload-artifact\n        env:\n          PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}\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: 20\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-test\n"
  },
  {
    "path": ".github/workflows/publish-version-info.yml",
    "content": "name: Publish NPM Version Info\n\non:\n  workflow_dispatch:\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-mobile-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\njobs:\n  Android:\n    name: Android\n    runs-on: ubuntu-latest\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Setup Env\n        uses: ./.github/actions/setup\n\n      - name: Build Packages\n        env:\n          DISABLE_SVG: 1\n        shell: bash\n        run: |\n          cd android\n          echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}\n          ./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'\n          rm -f app/${{ secrets.KEYSTORE_STORE_FILE }}\n\n      # Push tag to GitHub if package.json version's tag is not tagged\n      - name: Get package version\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Create git tag\n        uses: pkgdeps/git-tag-action@v3\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          github_repo: ${{ github.repository }}\n          version: ${{ env.PACKAGE_VERSION }}\n          git_commit_sha: ${{ github.sha }}\n          git_tag_prefix: \"v\"\n\n      - name: Generate file MD5\n        run: |\n          cd android/app/build/outputs/apk/release\n          md5sum *.apk\n\n      - name: Upload Artifact\n        uses: ./.github/actions/upload-artifact\n        env:\n          PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}\n\n  # Android_SL:\n  #   name: Android_SL\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - name: Check out git repository\n  #       uses: actions/checkout@v4\n  #       with:\n  #         ref: statusbar_lyric\n\n  #     - name: Setup Env\n  #       uses: ./.github/actions/setup\n\n  #     - name: Build Packages\n  #       shell: bash\n  #       run: |\n  #         cd android\n  #         echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}\n  #         ./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'\n  #         rm -f app/${{ secrets.KEYSTORE_STORE_FILE }}\n\n  #     # Push tag to GitHub if package.json version's tag is not tagged\n  #     - name: Get package version\n  #       run: |\n  #         node -p -e '`PACKAGE_VERSION=${require(\"./package.json\").version}`' >> $GITHUB_ENV\n  #         echo \"COMMIT_SHA=$(git show -s --format=%H)\" >> $GITHUB_ENV\n\n  #     - name: Create git tag\n  #       uses: pkgdeps/git-tag-action@v3\n  #       with:\n  #         github_token: ${{ secrets.GITHUB_TOKEN }}\n  #         github_repo: ${{ github.repository }}\n  #         version: ${{ env.PACKAGE_VERSION }}\n  #         git_commit_sha: ${{ env.COMMIT_SHA }}\n  #         git_tag_prefix: \"v\"\n\n  #     - name: Generate file MD5\n  #       run: |\n  #         echo \"current commit sha: ${{ env.COMMIT_SHA }}\"\n  #         cd android/app/build/outputs/apk/release\n  #         md5sum *.apk\n\n  #     - name: Upload Artifact\n  #       uses: ./.github/actions/upload-artifact\n  #       env:\n  #         PACKAGE_TYPE: 'Android_SL'\n  #         PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}\n\n  # Android_5:\n  #   name: Android_5\n  #   runs-on: ubuntu-latest\n  #   steps:\n  #     - name: Check out git repository\n  #       uses: actions/checkout@v4\n  #       with:\n  #         ref: android_5\n\n  #     - name: Setup Env\n  #       uses: ./.github/actions/setup\n\n  #     - name: Build Packages\n  #       shell: bash\n  #       run: |\n  #         cd android\n  #         echo ${{ secrets.KEYSTORE_STORE_FILE_BASE64 }} | base64 --decode > app/${{ secrets.KEYSTORE_STORE_FILE }}\n  #         ./gradlew assembleRelease -PMYAPP_UPLOAD_STORE_FILE='${{ secrets.KEYSTORE_STORE_FILE }}' -PMYAPP_UPLOAD_KEY_ALIAS='${{ secrets.KEYSTORE_KEY_ALIAS }}' -PMYAPP_UPLOAD_STORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' -PMYAPP_UPLOAD_KEY_PASSWORD='${{ secrets.KEYSTORE_KEY_PASSWORD }}'\n  #         rm -f app/${{ secrets.KEYSTORE_STORE_FILE }}\n\n  #     # Push tag to GitHub if package.json version's tag is not tagged\n  #     - name: Get package version\n  #       run: |\n  #         node -p -e '`PACKAGE_VERSION=${require(\"./package.json\").version}`' >> $GITHUB_ENV\n  #         echo \"COMMIT_SHA=$(git show -s --format=%H)\" >> $GITHUB_ENV\n\n  #     - name: Create git tag\n  #       uses: pkgdeps/git-tag-action@v3\n  #       with:\n  #         github_token: ${{ secrets.GITHUB_TOKEN }}\n  #         github_repo: ${{ github.repository }}\n  #         version: ${{ env.PACKAGE_VERSION }}\n  #         git_commit_sha: ${{ env.COMMIT_SHA }}\n  #         git_tag_prefix: \"v\"\n\n  #     - name: Generate file MD5\n  #       run: |\n  #         echo \"current commit sha: ${{ env.COMMIT_SHA }}\"\n  #         cd android/app/build/outputs/apk/release\n  #         md5sum *.apk\n\n  #     - name: Upload Artifact\n  #       uses: ./.github/actions/upload-artifact\n  #       env:\n  #         PACKAGE_TYPE: 'Android_5'\n  #         PACKAGE_VERSION: ${{ env.PACKAGE_VERSION }}\n\n  Release:\n    name: Release\n    runs-on: ubuntu-latest\n    # needs: [Android, Android_SL]\n    needs: [Android]\n    steps:\n      - name: Check out git repository\n        uses: actions/checkout@v4\n\n      - name: Download Artifacts\n        uses: actions/download-artifact@v4\n        with:\n          path: ./artifacts\n          merge-multiple: true\n\n      - name: Generate file MD5\n        run: |\n          echo -e '\\n### File MD5\\n```' >> ./publish/changeLog.md\n          cd artifacts\n          md5sum *.apk >> ../publish/changeLog.md\n          echo -e '```\\n' >> ../publish/changeLog.md\n          echo -e '\\n[软件安装包说明](https://lyswhut.github.io/lx-music-doc/download#%E8%BD%AF%E4%BB%B6%E5%AE%89%E8%A3%85%E5%8C%85%E8%AF%B4%E6%98%8E)\\n' >> ../publish/changeLog.md\n\n      - name: Get package version\n        run: |\n          PACKAGE_VERSION=$(node -p \"require('./package.json').version\")\n          echo \"PACKAGE_VERSION=$PACKAGE_VERSION\" >> $GITHUB_ENV\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        with:\n          body_path: ./publish/changeLog.md\n          prerelease: false\n          draft: false\n          tag_name: v${{ env.PACKAGE_VERSION }}\n          files: |\n            artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-arm64-v8a.apk\n            artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-armeabi-v7a.apk\n            artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-x86_64.apk\n            artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-x86.apk\n            artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-universal.apk\n\n          # artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-sl-arm64-v8a.apk\n          # artifacts/lx-music-mobile-v${{ env.PACKAGE_VERSION }}-android_5-universal.apk\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# OSX\n#\n.DS_Store\n\n# Xcode\n#\nbuild/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspectivev3\nxcuserdata\n*.xccheckout\n*.moved-aside\nDerivedData\n*.hmap\n*.ipa\n*.xcuserstate\nios/.xcode.env.local\n\n# Android/IntelliJ\n#\nbuild/\n.idea\n.gradle\nlocal.properties\nkeystore.properties\n*.iml\n*.hprof\n.cxx/\n\n# node.js\n#\nnode_modules/\nnpm-debug.log\nyarn-error.log\n\n# BUCK\nbuck-out/\n\\.buckd/\n*.keystore\n!debug.keystore\n\n# fastlane\n#\n# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the\n# screenshots whenever they are needed.\n# For more information about the recommended setup visit:\n# https://docs.fastlane.tools/best-practices/source-control/\n\n**/fastlane/report.xml\n**/fastlane/Preview.html\n**/fastlane/screenshots\n**/fastlane/test_output\n\n# Bundle artifact\n*.jsbundle\n\n# Ruby / CocoaPods\n/ios/Pods/\n/vendor/bundle/\n\n# testing\n/coverage\n"
  },
  {
    "path": ".ncurc.js",
    "content": "module.exports = {\n  upgrade: true,\n  reject: [\n    '@types/react',\n    '@types/react-native',\n    'message2call',\n    'react',\n    'react-native',\n    'react-native-pager-view',\n    'react-native-navigation',\n    'eslint-plugin-react-hooks',\n    '@react-native/metro-config',\n    '@react-native/babel-preset',\n    '@react-native/typescript-config',\n    '@react-native-community/slider',\n    '@react-native-async-storage/async-storage'\n  ],\n\n  // target: 'newest',\n  // filter: [\n  //   'react-native-navigation',\n  // ],\n\n  // target: 'patch',\n  // filter: [\n  //   '@types/react',\n  //   '@types/react-native',\n  //   'react',\n  //   'react-native',\n  //   '@react-native/metro-config',\n  //   '@react-native/babel-preset',\n  //   '@react-native/typescript-config',\n  //   '@react-native-community/slider',\n  //   'eslint-plugin-react-hooks',\n  //   '@react-native-async-storage/async-storage'\n  // ],\n}\n"
  },
  {
    "path": ".nvmrc",
    "content": "v18\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  - javascriptreact\n  - typescript\n  - typescriptreact\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 LxMusicMobile 工作区 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\t\"Import translation\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"imtl\",\n\t\t\"body\": [\n\t\t\t\"import { useTranslation } from '@/plugins/i18n'\",\n\t\t\t\"$1const { t } = useTranslation()\"\n\t\t],\n\t\t\"description\": \"Translation Language\"\n\t},\n\t\"Import store setting\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"imss\",\n\t\t\"body\": [\n\t\t\t\"import settingState from '@/store/setting/state'\"\n\t\t],\n\t\t\"description\": \"Import store setting\"\n\t},\n\t\"Import store player\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"imsp\",\n\t\t\"body\": [\n\t\t\t\"import playerState from '@/store/player/state'\"\n\t\t],\n\t\t\"description\": \"Import store player\"\n\t},\n\t\"Import store list\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"imsl\",\n\t\t\"body\": [\n\t\t\t\"import listState from '@/store/list/state'\"\n\t\t],\n\t\t\"description\": \"Import store list\"\n\t},\n\t\"Import toast\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"imts\",\n\t\t\"body\": [\n\t\t\t\"import { toast } from '@/utils/tools'\",\n\t\t\t\"$1toast(t(''), 'long')\"\n\t\t],\n\t\t\"description\": \"Import toast\"\n\t},\n\t\"Use getter theme\": {\n\t\t\"scope\": \"javascript,typescript,typescriptreact\",\n\t\t\"prefix\": \"ugt\",\n\t\t\"body\": [\n\t\t\t\"const theme = useGetter('common', 'theme')\"\n\t\t],\n\t\t\"description\": \"Use getter theme\"\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  \"typescript.tsdk\": \"packages/shared/eslint/node_modules/typescript/lib\",\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# lx-music-mobile 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## [1.8.1](https://github.com/lyswhut/lx-music-mobile/compare/v1.8.0...v1.8.1) - 2026-02-16\n\n我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 优化\n\n- 启动APP时将最后播放的歌曲信息初始化到播放器（显示到系统通知栏，可用媒体键控制播放）\n- 优化图片组件性能\n- 优化版本检查提示，使用 toast 显示未知版本信息（#946）\n\n### 修复\n\n- 修复在某些Android设备上字体显示异常的问题（#926, #718）\n\n## [1.8.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.7.1...v1.8.0) - 2025-11-29\n\n我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 新增\n\n- 新增「设置 → 基本设置 → 允许通过底栏进度条调整播放进度」设置（#778）\n   *默认为原来的启用状态，若你觉得在进入播放详情页时会误触调整进度，则可以将其关闭*\n- 新增 Any Listen 歌词标签数据读取与播放\n- 编辑本地歌曲的标签信息时，添加 Any Listen 歌词标签数据生成（用于支持已下载歌曲的歌词在桌面版逐字播放）\n\n### 优化\n\n- 设置-备份与恢复 导入列表数据时，增加二次确认提示（#809）\n\n### 修复\n\n- 修复切歌时，偶现歌词不播放的问题\n- 修复TX源搜索失败 (@Folltoshe)\n- 修复MG源歌单加载失败（#913）\n- 修复MG源评论加载失败（#914）\n\n### 其他\n\n- 更新 Media3 到 v1.8.0\n\n## [1.7.1](https://github.com/lyswhut/lx-music-mobile/compare/v1.7.0...v1.7.1) - 2025-05-01\n\n### 修复\n\n- 修复 tx 歌单搜索名字、描述出现乱码的问题\n- 修复解析某些本地歌词文件时出现乱码的问题（#694）\n- 修复 Android 5.1 下添加本地歌曲时报错的问题（#730）\n- 修复 kw 歌单详情出现打开失败的问题\n- 修复 kg 热门评论无法获取的问题\n- 修复 kg 歌单打开失败的问题（thanks @Folltoshe）\n\n### 优化\n\n- 优化软件文案编排（#701, #703, @3gf8jv4dv）\n\n### 变更\n\n- 我的列表-歌曲菜单中的 歌曲换源 功能从之前的类似软连接的形式改成替换歌曲的形式，也就是说，现在该功能相当于快速在线搜索歌曲，确认换源后将自动将原来的歌曲删除再将选择的歌曲插入被删除歌曲的位置。\n\n### 其他\n\n- 更新项目文档（@3gf8jv4dv）\n\n## [1.7.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.6.0...v1.7.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*为了防止歌曲缓存被第三方软件当做垃圾意外清理，歌曲缓存不再存储到缓存目录。若想清理缓存，需去「设置 → 其他 → 资源缓存管理」清理。*\n\n*更新到该版本后首次播放歌曲时，会将之前的歌曲缓存迁移到新位置，需要等待的时间取决于你已缓存资源的大小。*\n\n*感谢 @3gf8jv4dv 对 LX 系列项目翻译、文档等文案的大幅修订优化。*\n\n### 新增\n\n- 新增蓝牙歌词支持，可以通过「设置 → 播放设置 → 显示蓝牙歌词」启用（#615）\n- 新增繁体中文语言（#659, @3gf8jv4dv）\n- 将 LX Music 设置为「音乐应用」类，允许将 LX Music 设置为系统默认音乐播放器\n- 支持在程序外使用 LX Music 打开常见音乐文件及 `.js`、`.json`、`.lxmc` 等文件\n\n### 优化\n\n- 防止歌曲缓存被第三方软件当做垃圾意外清理\n- 优化正常播放结束时的下一首歌曲播放衔接度，在歌曲即将结束播放时将预获取下一首歌曲的播放链接，减少自动切歌时的等待时间\n- 优化歌曲换源机制，提升换源正确率\n- 首次使用的提示窗口可以通过点击背景或者返回键关闭（#577）\n- 上移 Toast 位置避免遮挡播放模式图标（#603, @sibojia）\n- 优化简体中文文案编排，大幅修订英语文案编排（#658, #660 等, @3gf8jv4dv）\n- 优化应用图标质量，在高版本系统上使用矢量图标\n\n### 修复\n\n- 修复导出文件到范围存储类型的目录时，扩展名丢失的问题\n- 修复切换列表播放歌曲时可能会出现播放的歌曲不对应的问题\n- 修复内置列表名称硬编码和语言切换显示的问题（#662）\n- 修复某些情况下进播放详情页时，详情页不显示或应用界面无响应的问题\n- 修复低版本 Android 在某些情况下对 Emoji 字符编码的处理问题\n\n### 变更\n\n- 歌曲缓存不再存储到缓存目录\n- 不再长期缓存换源歌曲信息\n\n### 其他\n\n- 更新 Readme 文档，优化文案编排（#651, Thanks @3gf8jv4dv）\n- 更新 Issue 模板（#652, @3gf8jv4dv）\n- 更新项目文档（@3gf8jv4dv）\n- 更新 React Native 到 v0.73.11\n\n## [1.6.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.5.0...v1.6.0) - 2024-08-24\n\n### 新增\n\n- 新增 我的列表-歌曲右击菜单-歌曲换源 功能，换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲，该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题\n- 新增 Scheme URL 调用支持，调用传参格式与PC端一致，详情看文档说明： https://lyswhut.github.io/lx-music-doc/mobile/scheme-url\n\n## [1.5.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.4.2...v1.5.0) - 2024-08-03\n\n我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\n\n### 新增\n\n- 新增重复歌曲列表，可以方便移除我的列表中的重复歌曲，此列表会列出目标列表里歌曲名相同的歌曲，可在“我的列表”里的列表名菜单中使用（注：该功能与PC端的区别是可以点击歌曲名多选删除）\n- 新增打开当前歌曲详情页菜单，可以在歌曲菜单中使用\n\n### 修复\n\n- 修复潜在桌面歌词导致的崩溃问题\n\n### 其他\n\n- 更新 React native 到 v0.73.9\n- 更新 exoplayer 到 v1.4.0\n\n## [1.4.2](https://github.com/lyswhut/lx-music-mobile/compare/v1.4.1...v1.4.2) - 2024-06-01\n\n我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\n\n### 修复\n\n- 修复数据存储管理在移除数据时可能出现移除失败的问题\n\n## [1.4.1](https://github.com/lyswhut/lx-music-mobile/compare/v1.4.0...v1.4.1) - 2024-06-01\n\n我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\n\n### 修复\n\n- 修复播放详情页歌词滚动问题（#518）\n\n## [1.4.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.3.0...v1.4.0) - 2024-06-01\n\n我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\n\n### 新增\n\n- 新增 设置-基本设置-启动后打开播放详情界面 设置，默认关闭（#502 @mingcc7）\n\n### 修复\n\n- 修复重复的数据初始化调用\n- 修复导入歌单时可能会导致歌单数据存储异常的问题（#500）\n\n### 变更\n\n- 设置-播放设置-优先播放320k音质选项改为“优先播放的音质”，允许选择更高优先播放的音质，如果歌曲及音源支持的话（#487）\n\n### 其他\n\n- 更新 React native 到 v0.73.8\n\n## [1.3.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.2.0...v1.3.0) - 2024-04-14\n\n### 新增\n\n- 新增棕色主题“泥牛入海”\n- 新增设置-基本设置-总是保留状态栏高度设置，如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况，可以启用该设置以始终为系统状态栏保留空间\n- 新增在线自定义源导入功能，允许通过http/https链接导入自定义源\n\n### 优化\n\n- 不再丢弃kg源逐行歌词（@helloplhm-qwq）\n- 支持kw源排行榜显示大小（revert @Folltoshe #1460）\n- 优化本地歌曲换源匹配机制\n\n### 修复\n\n- 修复mg歌词在某些情况下获取失败的问题\n- 修复mg歌单搜索（@helloplhm-qwq）\n- 修复kg最新评论无法获取的问题（@helloplhm-qwq）\n\n### 其他\n\n- 更新 React native 到 v0.73.6\n\n## [1.2.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.1.1...v1.2.0) - 2024-02-01\n\n提前祝大家新年快乐！\n\n### 新增\n\n- 新增自定义源（实验性功能），调用方式与PC端一致，但需要注意的是，移动端自定义源的环境与PC端不同，某些环境API不可用，详情看自定义说明文档\n- 新增长按收藏列表名自动跳转列表顶部的功能\n- 新增实验性的添加本地歌曲到我的收藏支持，与PC端类似，在我的收藏的列表菜单中选择歌曲目录，将添加所选目录下的所有歌曲，目前支持mp3/flac/ogg/wav等格式\n- 新增歌曲标签编辑功能，允许编辑本地源且文件存在的歌曲标签信息\n- 新增动态背景，启用后将使用当前播放歌曲封面做APP背景，默认关闭，可到设置-主题设置启用\n- 新增APP全局字体阴影，默认关闭，可到设置-主题设置启用\n- 新增启用竖屏首页横向滚动设置，默认开启（原来的行为），如果你不想要竖屏的首页左右滑动则可以关闭此设置（#397）\n- 新增“使用系统文件选择器”设置，默认启用，启用该选项后，导入备份文件、自定义源等操作将不需要申请存储权限，但可能在某些系统上不可用\n- 播放详情页新增桌面歌词显示/隐藏切换按钮，长按可切换歌词锁定状态\n- 我的列表菜单列表新增“新建列表”菜单\n- 我的列表菜单列表新增“排序歌曲”菜单，可以排序所选列表内的歌曲，排序功能与PC一致\n- 添加 墨·状态栏特别版（版本号包含`sl`）的 release 构建\n\n### 优化\n\n- 添加是否忽略电池优化检查，用于提醒用户添加白名单，确保APP后台播放稳定性\n- 在设置界面返回时，不再直接返回桌面，将回到进入设置界面前的界面，在非设置界面返回时才会返回桌面\n- 更新播放栏进度条样式，进度条允许拖动调整进度\n- 优化播放详情页歌曲封面、控制按钮对各尺寸屏幕的适配，修改横屏下的控制栏按钮布局\n- 优化横竖屏界面的展示判断，现在趋于方屏的屏幕按竖屏的方式显示，横屏下的播放栏添加上一曲切歌按钮\n- 添加对wy源某些歌曲有问题的歌词进行修复（#370）\n- 文件选择器允许选择外置存储设备上的路径，添加SD卡、USB存储等外置存储设备的读写支持\n- 图片显示改用第三方的图片组件，支持gif类型的图片显示，尝试解决某些设备上图片过多导致的应用崩溃问题\n- 歌曲评论内容过长时自动折叠，需手动展开\n- 改进本地音乐在线信息的匹配机制\n- 移除播放服务唤醒锁，解决APP在空闲时仍然处于唤醒状态的问题\n- 添加创建同名列表时的二次确认\n\n### 修复\n\n- 修复主题背景覆盖不全的问题\n- 修复清理缓存后查看日志时会导致APP崩溃的问题\n- 修复临时列表变更会意外触发同步的问题\n\n### 变更\n\n- 在更低版本的安卓上启用跟随系统亮暗主题功能（#317）\n- 由于歌曲评论的图片太大占用较多资源，评论图片不再直接加载，需要点击图片区域后再加载\n- 导入文件（歌单备份、自定义源文件等）默认不再需要设备存储权限，但如果这导致在你的设备上无法选择文件，则可以关闭基本设置的“使用系统文件选择器”设置，回退到原来的文件选择方式\n\n### 其他\n\n- 移除所有内置源，由于收到腾讯投诉要求停止提供软件内置的连接到他们平台的在线播放及下载服务，所以从即日（2023年10月18日）起LX本身不再提供上述服务\n- 更新许可协议的排版，使其看起来更加清晰明了，更新数据来源原理说明\n- 更新 React native 到 v0.73.3\n- 核心播放器从 ExoPlayer 迁移到 media3 v1.2.1\n\n## [1.1.1](https://github.com/lyswhut/lx-music-mobile/compare/v1.1.0...v1.1.1) - 2023-09-28\n\n落雪提前祝大家中秋快乐~🥮😘！\n\n### 优化\n\n- 通过歌曲菜单添加不喜欢歌曲时需要二次确认防止手抖\n- 减慢歌词详情页歌词滚动速度\n- 更改应用窗口大小获取方式，尝试解决在某些设备上的背景、弹出菜单显示问题\n- 优化同步功能错误消息提示，因同步服务版本不匹配导致的连接失败现在将区分提示\n\n### 修复\n\n- 修复横屏状态下的歌词滚动位置计算问题\n- 修复切歌时歌词激活行的重置问题\n- 修复更新翻译歌词、罗马音歌词设置后需重启应用才生效的问题，现在更新设置后会立即生效\n\n### 其他\n\n- 更新 React native 到 v0.72.5\n\n## [1.1.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.6...v1.1.0) - 2023-09-09\n\n目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** （在设置-关于有说明），其他渠道均为第三方转载发布，可信度请自行鉴别。\n\n本项目无微信公众号之类的官方账号，也未在小米、华为、vivo等应用商店发布应用，商店内的“LX Music”、“洛雪音乐”相关的应用全部属于假冒应用，谨防被骗。\n\n本软件完全无广告且无引流（如需要加群、关注公众号之类才能使用或者升级）的行为，若你使用过程中遇到广告或者引流的信息，则表明你当前运行的软件是第三方修改版。\n\n若在升级新版本时提示签名不一致，则表明你手机上的旧版本或者将要安装的新版本中有一方是第三方修改版。\n\n若在升级新版本时提示无法降级安装，则表明你使用的是universal（通用）版安装包（安装包大小20M+），尝试使用arm64-v8a版安装包或者到GitHub下载其他版本安装包。\n\n该版本针对一加、OPPO、Pixel无法播放歌曲（提示音频加载出错，5 秒后切换下一首）或者无法完整播放歌曲的问题做了处理，但如果你使用该版本后问题依然存在，临时的解决方案是去设置-播放设置关闭“音频卸载”选项后完全重启应用\n\n### 不兼容性变更\n\n该版本修改了同步协议逻辑，同步功能至少需要PC端v2.4.0或移动端v1.1.0或同步服务v2.0.0版本才能连接使用\n\n### 新增\n\n- 新增列表设置-是否显示歌曲专辑名，默认关闭\n- 新增列表设置-是否显示歌曲时长，默认开启\n- 新增是否允许通过歌词调整播放进度功能，默认关闭，可到播放详情页右上角设置开启\n- 新增“不喜欢歌曲”功能，可以在我的列表或者在线列表内歌曲的右击菜单使用，还可以去“设置-其他”手动编辑不喜欢规则，注：“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲，但你仍可以手动播放这些歌曲\n- 新增同步功能对“不喜欢歌曲”列表的同步\n- 新增设置-播放设置-是否启用音频卸载，该设置之前默认是启用的，现在添加开关允许将其关闭，若出现播放器问题可尝试将其关闭\n- 新增设置-播放设置-自动清空已播放列表选项，默认关闭\n\n### 优化\n\n- 优化歌单列表歌单封面大小计算方式\n- 调整竖屏下的排行榜布局\n- 调整歌曲列表信息布局\n- 调整横屏下的歌曲列表为两列\n- 调整桌面歌词主题配色，增强歌词字体阴影（#276）\n- 优化数据传输逻辑，列表同步指令使用队列机制，保证列表同步操作的顺序\n- 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置\n- 播放详情页歌词添加延迟滚动及着色动画\n- 优化息屏下的逻辑处理，尽量减少电量消耗\n\n### 修复\n\n- 修复wy歌单分类切换无效的问题\n- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题，同时也可能导致同步数据丢失的问题（此问题会影响PC端，要完全修复这个问题还需要同时将PC端、同步服务更新到最新版本）\n- 修复在线列表、我的列表内的歌曲批量操作后，没有自动取消选择的问题\n- 修复tx热门评论昵称被错误切割的问题 (By: @helloplhm-qwq, @Folltoshe)\n- 修复wy源热搜词失效的问题（@Folltoshe）\n- 修复mg歌单搜索歌单播放数量显示问题\n- 修复搜索提示功能失效的问题（@Folltoshe）\n- 修复潜在导致列表数据不同步的问题\n- 修复kg无评论时的加载处理问题\n- 修复顺序播放时播放完列表的最后一首歌播放按钮状态没有更新的问题（#300）\n\n### 变更\n\n- 随机模式下，通过点击与播放列表相同的列表切歌时，将不再清空已播放列表，即已播放的歌曲不再重新参与随机，若想恢复之前的行为可以去设置-播放设置启用清空已播放列表选项\n\n### 其他\n\n- 更新 React native 到 v0.72.4\n\n## [1.0.6](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.5...v1.0.6) - 2023-05-01\n\n### 修复\n\n- 修复wy歌单分类切换无效的问题\n\n## [1.0.5](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.4...v1.0.5) - 2023-05-01\n\n### 优化\n\n- 增加kg歌单歌曲flac24bit显示（@helloplhm-qwq）\n- 增加tx源热门评论图片显示（@Folltoshe）\n- 支持wy热门评论翻页\n- 微调排行榜列表宽度及字体大小\n\n### 修复\n\n- 修复wy我喜欢列表使用token的方式导入，现在移动端可以使用token的方式导入我喜欢列表的音乐了，这意味着从PC端同步过来的歌单也可以在移动端上更新\n- 修复在线列表的多选问题\n- 修复mg搜索不显示时长的问题（@Folltoshe）\n- 修复mg评论加载失败的问题（@Folltoshe）\n- 修复在Android 5.1下报错的问题\n- 修复对存在错误时间标签的歌词的解析\n- 修复聚合搜索时未显示源名称的问题\n- 修复更改音源的列表歌曲颜色的实时更新问题\n\n### 其他\n\n- 更新kg、tx、wy等平台排行榜列表\n- 更新react native到v0.71.7\n\n## [1.0.4](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.3...v1.0.4) - 2023-04-01\n\n### 新增\n\n- 隐藏黑色主题背景设置，默认关闭，可以去设置-主题设置更改\n\n### 优化\n\n- 添加歌单分类、排行榜激活指示器\n- 调整设置界面竖屏下的UI布局\n\n### 修复\n\n- 修复歌单排序列表滚动重置问题\n- 修复搜索提示列表的显示时机问题\n- 就放tx源歌词获取失败的问题\n- 修复将播放速率调整为0.6后，再次打开设置面板将会导致app崩溃的问题\n- 修复播放详情页设置面板当前音量显示格式问题\n\n### 其他\n\n- 升级 React Native 到 v0.71.5\n\n## [1.0.3](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.2...v1.0.3) - 2023-03-26\n\n### 修复\n\n- 修复歌单详情页内歌曲最多只加载30首的问题\n\n## [1.0.2](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.1...v1.0.2) - 2023-03-26\n\n### 优化\n\n- 竖屏下的首页允许滑动切换页面（恢复v0.x.x的切页操作）\n- 优化更新语言、主题设置时的流畅度\n\n### 其他\n\n- 启用新架构\n\n## [1.0.1](https://github.com/lyswhut/lx-music-mobile/compare/v1.0.0...v1.0.1) - 2023-03-26\n\n### 修复\n\n- 修复在线列表翻页问题\n\n## [1.0.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.5...v1.0.0) - 2023-03-26\n\n从v1.0.0起，我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)，如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明\n\n由于该版本涉及旧版数据迁移，建议更新前先到设置-备份与恢复备份歌单\n\n### 不兼容性变更说明\n\n- 同步功能，该功能不支持与PC端v2.2.0之前的版本使用\n\n### 新增\n\n- 新增聚合搜索，注：由于这个方式需要对各个源的结果进行排序，所以需要以“歌曲名 歌手”的顺序输入（例如：突然的自我 伍佰），否则排序后的结果可能不是你想要的\n- 新增歌单搜索功能\n- 新增热门搜索显示，默认关闭，需要到设置-搜索设置开启\n- 新增搜索历史记录，默认关闭，需要到设置-搜索设置开启\n- 启动软件时自动回到上次的界面，例如上次退出软件时在我的收藏，下次启动软件时会自动进入我的收藏\n- 新增PC端所拥有的内置皮肤\n- 新增界面字体大小设置\n- 新增播放器音量大小设置，可以去播放详情页-播放器设置-音量大小更改\n- 新增播放器播放速率设置，可以去播放详情页-播放器设置-播放速率更改\n- 新增播放详情页歌词对齐方式设置，可以去播放详情页-播放器设置-歌词对齐方式更改\n- 新增是否在左侧导航栏显示返回桌面按钮设置，默认关闭，可以去设置-基本设置-是否显示返回桌面按钮开启\n- 新增是否在左侧导航栏显示退出应用按钮设置，默认关闭，可以去设置-基本设置-是否显示退出应用按钮开启\n- 支持wy源flac hires歌曲类型的显示\n- 添加kg源评论图片展示（@helloplhm-qwq）\n- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示（@helloplhm-qwq, @Folltoshe）\n\n### 优化（界面/交互/功能）\n\n- 调整了首页的界面布局\n- 优化大屏幕下的字体大小及界面布局显示\n- 支持wy源flac hires歌曲类型的显示\n- 优化列表数据导入导出的性能，现在进行这些操作应该可以一下子完成且不会再冻结UI了\n- 支持kg源搜索列表flac hires歌曲类型的显示（@helloplhm-qwq）\n\n### 优化（程序）\n\n- 优化程序启动性能，优化与程序交互的流畅度\n- 重构整个程序，重新梳理了程序逻辑，使其更容易扩展及维护，将大部分代码从JavaScript迁移到TypeScript\n- 重写配置管理、列表管理功能，使其与PC端同步，更容易复用PC端的代码\n\n### 修复\n\n- 修复使用酷狗码无法打开某些类型的歌单的问题\n- 修复tx源某些歌单无法打开的问题\n\n### 变更\n\n- 原来播放详情页的歌词字体大小设置改为播放器设置\n\n### 其他\n\n- 升级React Native到v0.71.4\n\n## [0.15.5](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.4...v0.15.5) - 2023-01-02\n\n### 修复\n\n- 修复导入PC端v2列表文件歌曲信息转换丢失的问题\n- 修复上面问题导致的tx源评论加载失败的问题\n\n## [0.15.4](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.3...v0.15.4) - 2022-12-10\n\n### 修复\n\n- 修复播放详情页歌词翻译、罗马音歌词匹配问题\n\n## [0.15.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.2...v0.15.3) - 2022-12-10\n\n### 修复\n\n- 修复鸿蒙系统下的崩溃问题\n\n## [0.15.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.1...v0.15.2) - 2022-12-10\n\n### 修复\n\n- 修复潜在的歌词解析导致应用崩溃问题\n\n## [0.15.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.15.0...v0.15.1) - 2022-12-10\n\n### 修复\n\n- 修复某些歌曲的桌面歌词翻译或罗马音没有显示的问题\n- 修复kg某些歌单链接无法打开的问题\n\n## [0.15.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.14.3...v0.15.0) - 2022-11-20\n\n### 新增\n\n- 支持导入PC端v2版本的列表数据\n- 添加kg源罗马音歌词的支持\n- 支持打开波点音乐歌单（需在酷我源打开）\n\n### 修复\n\n- 支持单行多时间标签歌词解析，修复某些歌词会出现时间标签的问题\n- 修复某些类型的kg歌单无法导入的问题\n- 修复异常歌单、歌曲数据导致的崩溃问题（#157）\n\n### 其他\n\n- 升级react-native到 v0.68.5\n\n## [0.14.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.14.2...v0.14.3) - 2022-09-03\n\n### 修复\n\n- 修复因音源的域名到期导致的音源失效的问题\n\n## [0.14.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.14.1...v0.14.2) - 2022-08-18\n\n### 优化\n\n- 为tx、kw源添加 Flac 24bit 音质显示，注：由于之前没有记录此音质，所以之前收藏的歌曲信息中不包含它\n\n### 修复\n\n- 修复排行榜在旋转屏幕后，选中的榜单被重置回第一个的问题\n- 修复企鹅音乐搜索失效的问题\n\n## [0.14.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.14.0...v0.14.1) - 2022-07-09\n\n### 优化\n\n- 添加“弹出键盘时自动隐藏播放栏”设置，默认启用（原来的行为），若在某些设备上播放栏无法显示时则可以关闭此设置\n- 优化切歌时桌面歌词的切换动画显示\n- 暂停播放时自动隐藏桌面歌词\n- 在我的列表-列表名左侧添加了一个图标，以表示此处可以点击切换列表\n\n### 修复\n\n- 修复tx源搜索失效的问题\n\n## [0.14.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.13.0...v0.14.0) - 2022-06-17\n\n### 新增\n\n- 新增设置-桌面歌词-单行歌词设置，默认关闭，启用后只显示一行歌词，超出窗口宽度自动滚动到末尾\n- 新增设置-桌面歌词-显示歌词切换动画，默认启用，如果你觉得切换动画影响视觉可以将其关闭\n- 新增设置-基本设置-启动后自动播放音乐，默认关闭\n\n### 优化\n\n- 支持mg源的歌词翻译（之前添加的歌曲需要去设置清空缓存才会刷新歌词）\n- 添加歌曲列表更新操作的二次确认\n- 添加导入文件错误时的指引提示\n\n### 修复\n\n- 修复桌面歌词转繁体设置不立即生效的问题\n- 修复搜索、歌单、排行榜列表可能在切换新内容后出现上次列表内容的残留问题（#118）\n- 修复在某些系统上播放音乐会导致应用崩溃的问题（#129）\n- 修复停止播放后的播放器状态清理问题\n\n### 文档\n\n移动版文档已迁移到：<https://lyswhut.github.io/lx-music-doc/mobile>\n\n## [0.13.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.12.0...v0.13.0) - 2022-05-22\n\n从这个版本起，你可以将桌面歌词拖动到状态栏上，然后将歌词字体调小后配合新增的歌词窗口宽度、行数设置，模拟出类似状态栏歌词的效果。\n\n如果你的设备装有Xposed框架，可以使用状态栏版（详情看GitHub置顶issue），它通过调用第三方Xposed模块【墨•状态栏歌词】的API支持来状态栏歌词（感谢@ftevxk）。\n但考虑到要依赖第三方应用，并且是Xposed模块，预计用的人会比较少，所以暂不考虑将此特性包含在正式版中。\n\n### 新增\n\n- 新增设置-播放设置-显示歌词罗马音，默认关闭，注：目前只有网易源能获取到罗马音歌词（得益于 Binaryify/NeteaseCloudMusicApi/pull/1523），如果你知道其他源的歌词罗马音获取方式，欢迎PR或开issue交流！\n- 新增黑、白桌面歌词主题\n- 桌面歌词新增窗口宽度百分比、最大歌词行数调整设置，允许将歌词拖动到刘海屏状态栏上。提示：有了这组功能你就可以模拟状态栏歌词了\n- 新增设置-播放设置-将播放的歌词转繁体功能（#114）\n\n### 优化\n\n- 允许桌面歌词拖动到状态栏上（感谢@ftevxk）\n- 允许选择更新日志弹窗里的文本内容\n- 桌面歌词的最大字体大小允许调整到500（#107）\n\n### 修复\n\n- 修复潜在的桌面歌词导致应用崩溃问题\n\n### 文档\n\n- 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\n- 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\n\n### 其他\n\n- 升级React native到v0.68.2\n\n## [0.12.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.11.1...v0.12.0) - 2022-04-16\n\n### 新增\n\n- 为搜索、歌单、排行榜的歌曲菜单添加分享“分享歌曲”按钮\n- 新增设置-基本设置-分享设置，它用于控制歌曲菜单的分享行为，默认使用系统分享\n- 新增是否在通知栏显示歌曲图片设置，默认开启（原来的行为）\n- 新增黑色皮肤“黑灯瞎火”\n- 新增设置-基本设置-主题颜色-跟随系统亮、暗模式切换主题设置，注：此设置需要android 10或ios 13及以上的版本才支持\n\n### 优化\n\n- 现在即使切歌模式处于单曲循环、顺序播放、禁用时，手动切歌将会按照列表循环的规则处理（#69）\n- 添加定时退出计时结束后的提示\n\n### 修复\n\n- 修复wy源搜索某些歌曲时第一页之后的歌曲无法加载的问题\n- 每次启动时过滤无效的歌曲\n- 修复换源失败时的处理问题\n- 修复非循环模式下播放结束后的状态显示问题及无法重新播放的问题（#104）\n- 修复定时退出可能导致崩溃的问题\n- 修复播放详情页歌词界面在把应用切到后台再切回来会导致屏幕常亮失效的问题\n\n### 变更\n\n- 歌曲菜单的“复制歌曲名”改为“分享歌曲”，点击后可以选择第三方应用分享歌曲详情页链接\n- 已存在目录列表的歌曲再次添加时将不会变成移除\n\n### 其他\n\n- 升级react-native到 v0.68.1\n\n## [0.11.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.11.0...v0.11.1) - 2022-03-20\n\n### 修复\n\n- 修复播放栏在某些设备不显示的问题\n\n## [0.11.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.10.3...v0.11.0) - 2022-03-19\n\n### 新增\n\n- 新增“点击列表里的歌曲时自动切换到当前列表播放”设置，此功能仅对歌单、排行榜有效，默认关闭\n- 添加试听接口，这是测试接口、临时接口都不可用时最后的选择...\n\n### 优化\n\n- 过滤tx源某些不支持播放的歌曲，解决播放此类内容会导致意外的问题\n- 备份与恢复兼容单个列表文件的导入\n- 添加通知权限的检查提醒，点击“不再提示”后，将会在设置-清空缓存后才会恢复提示\n\n### 修复\n\n- 修复Android 12下的桌面歌词锁定后还是无法在应用外点击歌词后面下面的内容的问题\n\n### 其他\n\n- 升级React native到v0.67.4\n\n## [0.10.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.10.2...v0.10.3) - 2022-01-28\n\n### 优化\n\n- 优化kw源英文与翻译歌词的匹配\n\n### 修复\n\n- 修复桌面歌词播放器会导致应用崩溃的问题\n\n## [0.10.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.10.1...v0.10.2) - 2022-01-22\n\n### 修复\n\n- 修复某些系统下的虚拟导航栏会导致播放栏隐藏的问题（react-native v0.67.x导致的）\n\n### 其他\n\n- 降级react-native到 v0.66.4\n\n## [0.10.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.10.0...v0.10.1) - 2022-01-22\n\n### 优化\n\n- 优化通知栏的更新机制，尝试修复魅族的通知栏图片不显示的问题\n- 我的列表-列表名的右击菜单更新已收藏的在线列表时，将始终重新加载，不再使用缓存，解决在原平台更新歌单后，在LX点击更新可能看到的还是在原平台更新前的歌单的问题\n\n### 修复\n\n- 修复tx源无搜索结果的问题\n- 修复小米等设备下面的手势提示线背景颜色为黑色的问题\n\n### 其他\n\n- 升级React native到v0.67.1\n\n## [0.10.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.9.2...v0.10.0) - 2021-12-30\n\n### 新增\n\n- 同步功能新增对列表位置调整的支持（需v1.15.3以上的PC端版本才支持）\n- 新增播放详情页歌词字体大小调整设置，可在详情页右上角的按钮进行调整\n- 新增同步服务地址历史列表功能\n- 横屏播放详情页新增评论入口\n- 我的列表歌曲三个点的菜单新增复制歌曲名\n\n### 优化\n\n- 修改对播放模块的调用，杜绝应用显示正在播放的歌曲与实际播放歌曲不一致的问题（这是播放模块歌曲队列与应用内歌曲队列在某些情况下出现不一致时导致的）\n- 支持PC端同步功能添加对列表顺序调整的控制，确保手动调整位置后的列表与不同的电脑同步时，列表位置不会被还原\n- 调整横屏下的导航栏、播放详情页布局，提高屏幕空间利用率并使其更易操作\n- 调整歌单类别、我的列表弹出层界面\n- 播放栏移除上一曲按钮，将多出来的空间加给播放、下一曲按钮\n- 现在点击、长按播放栏歌曲标题也可以进入详情页、定位当前播放歌曲了\n\n### 修复\n\n- 修复kw源某些歌曲的歌词提取异常的问题\n\n### 其他\n\n- 升级react-native到v0.66.4\n\n## [0.9.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.9.1...v0.9.2) - 2021-11-28\n\n### 优化\n\n- 添加应用初始化出错时的错误捕获输出\n- 优化歌词自动换源机制\n\n### 修复\n\n- 修复因kw源歌词接口停用导致该源歌词获取失败的问题\n\n### 其他\n\n- 更新react-native到v0.66.3\n- 更新Exoplayer到v2.16.0\n\n## [0.9.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.9.0...v0.9.1) - 2021-10-23\n\n### 修复\n\n- 修复删除列表时会导致应用崩溃的问题\n- 修复原生代码导致的错误日志记录\n\n## [0.9.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.8.3...v0.9.0) - 2021-10-22\n\n### 新增\n\n- 新增歌曲评论显示，可在播放详情页进入。（与PC端一样，目前仅支持显示部分评论）\n- 新增播放、收藏整个排行榜功能，可长按排行榜名字后在弹出的菜单中操作\n- 新增单个列表导入/导出功能，可以方便分享歌曲列表，可在点击“我的列表”里的列表名右侧的按钮后弹出的菜单中使用\n- 新增删除列表前的确认弹窗，防止误删列表\n\n### 优化\n\n- 添加更多同步功能的日志记录\n\n### 修复\n\n- 修复kg源的歌单链接无法打开的问题\n- 修复同一首歌的URL、歌词等同时需要换源时的处理问题\n- 修复在排行榜页面无法时无法通过点击我的列表图标切换到我的列表的问题\n\n### 其他\n\n- 更新react-native到v0.66.1\n\n## [0.8.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.8.2...v0.8.3) - 2021-10-07\n\n### 修复\n\n- 修复我的列表搜索无法搜索小括号、中括号等字符，并会导致应用崩溃的问题\n- 修复使用同步功能同步完成后，列表没有被保存，导致下次再连接同步时被同步新增的歌曲被移除的问题（此问题由v0.8.2的存储切片改造引入的）\n\n### 其他\n\n- 更新React native到v0.66.0\n\n## [0.8.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.8.1...v0.8.2) - 2021-10-02\n\n### 优化\n\n- 缓冲进度条颜色\n- 优化数据存储，若需要存储的数据过大时会将数据切片后存储，现在存储大列表不会导致列表丢失了\n\n### 修复\n\n- 修复随机播放模式下在同列表切其他歌曲不会清空已播放列表的问题\n- 修复歌曲播放出错时的URL刷新问题\n\n## [0.8.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.8.0...v0.8.1) - 2021-09-16\n\n### 优化\n\n- 添加更多错误信息的记录\n\n### 修复\n\n- 修复潜在的获取缓存大小报错问题\n- 修复mg排行榜无法加载的问题\n- 修复列表导出失败时的提示信息缺失翻译的问题\n- 修复 Android 11 导入列表时，不显示备份文件的问题\n- 修复其他应用播放声音时，软件临时暂停播放后通知栏的状态仍显示正在播放的问题\n\n## [0.8.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.7.1...v0.8.0) - 2021-09-06\n\n### 新增\n\n- 添加对通知栏歌曲进度条的支持\n\n### 修复\n\n- 修复某些情况下桌面歌词会导致APP崩溃的问题\n- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题\n\n### 其他\n\n- 升级React native到v0.65.1\n- 升级播放模块`react-native-track-player`到v2版本，优化通知栏歌曲信息显示逻辑\n\n## [0.7.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.7.0...v0.7.1) - 2021-08-23\n\n### 修复\n\n- 修复无法从歌单界面打开网易歌单详情的问题\n\n## [0.7.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.6.2...v0.7.0) - 2021-08-22\n\n如果你喜欢并经常使用洛雪音乐，并想要第一时间尝鲜洛雪的新功能，可以加入测试企鹅群768786588，\n注意：测试版的功可能会不稳定，打算潜水的勿加。\n\n### 新增\n\n- 新增横屏状态下的播放详情页\n- 新增橙、粉、灰主题色\n- 新增桌面歌词的字体大小、透明度设置\n- 新增我的列表内歌曲搜索定位功能\n\n### 调整\n\n- 为了与搜索、歌单操作栏位置统一，现将我的列表-收藏的列表操作栏由底部挪到顶部\n\n### 修复\n\n- 修复tx源的歌词无法显示的问题\n- 修复随机播放模式下使用稍后播放功能会导致歌曲单曲循环的问题\n- 修复某些情况下桌面歌词会导致APP崩溃的问题\n\n## [0.6.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.6.1...v0.6.2) - 2021-08-11\n\n### 优化\n\n- 优化设置界面的输入框输入机制，现在只要键盘收起即可自动保存输入的内容\n- 添加在启用桌面歌词时对悬浮层权限的检查，这应该可以修复某些设备上点击启用桌面歌词时不显示无权限弹窗也不显示桌面歌词的情况\n\n### 变更\n\n- 不再自动聚焦定时退出、调整位置弹窗内的输入框，这应该可以修复某些设备无法在这两个地方弹出键盘的问题\n\n### 修复\n\n- 修复启用桌面歌词时的权限提示弹窗会导致应用报错的问题\n- 修复我的列表无法更新从收藏的排行榜的问题\n\n## [0.6.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.6.0...v0.6.1) - 2021-08-08\n\n### 修复\n\n- 修复随机播放下无法切歌的问题\n\n## [0.6.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.5.3...v0.6.0) - 2021-08-08\n\n### 新增\n\n- 新增局域网同步功能（实验性，首次使用前建议先备份一次列表），此功能需要配合PC端使用，移动端与PC端处在同一个局域网（路由器的网络）下时，可以多端实时同步歌曲列表，使用问题请看\"常见问题\"。\n- 新增桌面歌词\n\n### 优化\n\n- 优化退出应用的机制，现在在需要退出应用的场景将会完全退出应用\n\n### 修复\n\n- 修复某些情况下出现恢复播放信息失败的问题\n- 修复删除列表中正在播放的歌曲时会自动跳到第一首的问题\n- 修复因其他应用需要播放声音而暂停播放音乐时歌词不会暂停播放导致恢复播放后歌词与播放进度不一致的问题\n\n## [0.5.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.5.2...v0.5.3) - 2021-07-23\n\n### 修复\n\n- 修复歌曲缓存失效的问题\n\n## [0.5.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.5.1...v0.5.2) - 2021-07-22\n\n### 优化\n\n- 优化mg源打开歌单的链接兼容\n\n### 修复\n\n- 修复单曲循环播放时循环次数为偶数时歌词不重新播放的问题\n- 添加针对进入歌词界面时某些情况下会弹出`scrollToIndex out of range: requested index ...`崩溃错误弹窗的处理\n- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注：现在可以加载1000+首歌曲的歌单，但出于未知原因会导致部分歌曲无法加载（可能是无版权导致的），目前酷狗码仍然最多只能加载500首歌\n\n## [0.5.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.5.0...v0.5.1) - 2021-07-05\n\n### 优化\n\n- 添加切换播放模式时的文字提示\n- 优化单首歌曲的添加弹窗操作，当选择当前歌曲已存在目标列表时（列表名灰色显示），会将当前歌曲从目标列表移除，否则将当前歌曲添加到目标列表，添加在弹窗内对歌曲的添加、移动、删除操作时的文字提示\n\n### 修复\n\n- 修复mg源搜索失效的问题\n\n### 移除\n\n- 因wy源的歌单列表已没有“最新”排序的选项，所以现跟随移除wy源歌单列表按“最新”排序的按钮\n\n## [0.5.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.4.2...v0.5.0) - 2021-06-13\n\n### 新增\n\n- 新增“其他应用播放声音时，自动暂停播放”设置，默认开启\n- 新增“添加歌曲到列表时的位置”设置，可选项为列表的“顶部”与“底部”\n- 新增“显示歌词翻译设置”，默认关闭\n\n### 变更\n\n- 添加歌曲到列表时从原来的底部改为顶部，若想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项\n\n## [0.4.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.4.1...v0.4.2) - 2021-06-06\n\n### 优化\n\n- 优化wy源歌单导入匹配，现在存在链接外的其他字符也可以打开歌单了\n\n### 修复\n\n- 修复定时播放开启歌曲播放完毕再停止时，若倒计时已结束会导致无法播放歌曲的问题\n- 修复打开歌单失败时会导致应用崩溃的问题\n- 修复打开kw歌单失败时会无限重试的问题\n- 尝试修复弹出菜单、列表位置不正确的问题\n- 修复打开kg源歌单链接失败的问题\n- 尝试修复有时候进入播放详情歌词界面时会导致应用UI被冻结的问题\n- 修复有时候进入播放详情页时歌曲封面大小显示不正确的问题\n\n## [0.4.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.4.0...v0.4.1) - 2021-05-30\n\n### 修复\n\n- 修复定时播放开启歌曲播放完毕再停止时，若倒计时已结束会导致无法播放歌曲的问题\n\n## [0.4.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.3.3...v0.4.0) - 2021-05-30\n\n### 新增\n\n- 新增我的列表中已收藏的在线列表的更新功能。注意：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表（与PC端的同步一样）\n- 歌曲添加、移动弹窗新增创建新列表功能\n- 新增定时退出播放\n\n### 优化\n\n- 优化应用布局对手机系统字体大小的适配\n- 调整歌单详情页，现在在歌单详情页按手机上的返回键将会返回歌单列表，而不是直接退出APP\n- 优化进入播放详情页、歌单详情页的动画效果\n\n### 修复\n\n- 尝试修复某些情况下进播放详情歌词界面时报错的问题\n\n## [0.3.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.3.2...v0.3.3) - 2021-05-25\n\n### 修复\n\n- 尝试修复软件启动时恢复上一次播放的歌曲可能导致软件崩溃的问题\n- 尝试修复播放详情页歌词导致UI冻结的问题\n- 修复企鹅音乐搜索歌曲没有结果的问题\n\n### 其他\n\n- 整合日志记录\n- 更新 exoPlayer 到 2.14.0\n\n## [0.3.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.3.1...v0.3.2) - 2021-05-23\n\n### 修复\n\n- 修复手机分享的wy歌单、某些tx、kg歌单无法打开的问题\n- 修复打开空的歌单时，点击播放全部会导致应用崩溃的问题\n- 修复企鹅音乐搜索歌曲没有结果的问题\n\n## [0.3.1](https://github.com/lyswhut/lx-music-mobile/compare/v0.3.0...v0.3.1) - 2021-05-22\n\n### 修复\n\n- 修复进入播放详情歌词界面后的屏幕常亮不会被取消的问题\n\n## [0.3.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.2.0...v0.3.0) - 2021-05-22\n\n### 新增\n\n- 新增通过歌单链接打开歌单的功能\n\n### 优化\n\n- 切换到播放详情歌词界面时将阻止屏幕息屏\n\n### 修复\n\n- 修复一个导致崩溃日志写入文件前会导致APP崩溃的莫名其妙问题\n\n## [0.2.0](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.7...v0.2.0) - 2021-05-21\n\n### 新增\n\n- 新增竖屏下的播放详情页\n\n## [0.1.7](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.6...v0.1.7) - 2021-05-20\n\n### 优化\n\n- 修改歌单导入流程，添加对歌单导入错误的捕获\n\n### 修复\n\n- 修复在系统暗主题下，应用内文字输入框的字体会变成白色的问题\n\n## [0.1.6](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.5...v0.1.6) - 2021-05-18\n\n### 优化\n\n- 改进软件错误处理，添加对软件崩溃的错误日志记录，可在设置-其他查看错误日志历史。注：清理缓存时日志也将会被清理\n\n### 修复\n\n- 修复显示版本更新弹窗会导致应用崩溃的问题\n\n## [0.1.5](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.4...v0.1.5) - 2021-05-18\n\n### 修复\n\n- 修复修复协议弹窗可以被绕过的问题\n- 修复从在线列表使用稍后播放功能播放歌曲时，歌曲封面不显示的问题\n- 修复正在播放“稍后播放”的歌曲时，对“稍后播放”前播放的列表进行添加、删除操作会导致切歌的问题\n\n## [0.1.4](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.3...v0.1.4) - 2021-05-16\n\n### 修复\n\n- 修复获取在线列表时快速切换会导致APP闪退的问题\n\n## [0.1.3](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.2...v0.1.3) - 2021-05-16\n\n### 优化\n\n- 添加导入提示，兼容从PC端“全部数据”类型的备份文件中导入歌单\n- 添加全局异常错误捕获，现在一般情况下APP崩溃前会弹窗提示错误信息。\n\n## [0.1.2](https://github.com/lyswhut/lx-music-mobile/compare/v0.1.1...v0.1.2) - 2021-05-16\n\n### 优化\n\n- 在搜索、歌单、排行榜列表多选音乐后点菜单中的播放将会把已选的歌曲添加到试听列表播放\n\n### 修复\n\n- 修复播放模块没拉取最新代码导致播放器存在无法从通知栏停止等问题\n\n## [0.1.1] - 2021-05-15\n\n- v0.1.1版本发布 🎊 🎉\n"
  },
  {
    "path": "FAQ.md",
    "content": "# lx-music-mobile 常见问题\n\n本文档已迁移至：<https://lyswhut.github.io/lx-music-doc/mobile/faq>\n\n<!-- 在阅读本常见问题后，仍然无法解决你的问题，请提交issue或者加企鹅群`830125506`反馈（无事勿加，入群先看群公告），反馈时请**注明**已阅读常见问题！\n\n## LX Music中的音乐播放列表机制\n\n1. 默认情况下，播放搜索列表、歌单列表、排行榜列表的歌曲时会自动将该歌曲添加到“我的列表”的试听列表后再播放，手动将歌曲添加到试听列表，再去试听列表找到这首歌点播放是等价的\n2. 如果你想要播放多首歌曲，需要使用多选功能（若不知道如何多选请看常见问题）多选后，将歌曲这些歌曲添加到“我的列表”播放，或使用稍后播放功能播放\n3. 第2条适用于搜索列表、歌单列表、排行榜列表、我的列表的歌曲\n4. 对于歌单详情列表，除了可以使用第2条的方式播放外，你可以点击详情页上面的播放按钮临时播放当前歌单，或点击收藏将当前歌单收藏到“我的列表”后再去播放\n5. 对于排行榜详情列表，除了可以使用第2条的方式播放外，你可以在长按排行榜名字后弹出的菜单中，播放或收藏整个排行榜，这与第四条的歌单中的播放、与收藏按钮功能一致\n6. v0.11.0及之后新增了“双击列表里的歌曲时自动切换到当前列表播放”设置，默认关闭，此功能仅对歌单、排行榜有效\n7. 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\n8. 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\n\n## 歌曲无法试听与下载\n\n### 所有歌曲都提示 `请求异常😮，可以多试几次，若还是不行就换一首吧。。。`\n\n尝试更换网络，如切换到移动网络，若移动网络还是不行则尝试开关下手机的飞行模式后再试，<br>\n若使用家庭网络的话，可尝试将光猫断电5分钟左右再通电联网后播放。\n\n### 其他情况\n\n尝试在在浏览器打开这个地址`http://ts.tempmusics.tk`，浏览器显示404是正常的，如果不是404那就证明所在网络无法访问接口服务器，对于此类情况请尝试切换其他网络。\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## 列表多选\n\n长按列表将会进入多选模式。\n\n- 例子一：想要选中1-5项，进入多选模式后，取消所有选中的内容，切换到区间，点击第一项，再点击第五项即可完成选择；\n- 例子二：想要选中1项与第3项，进入多选模式后，点击第一项，再点击第三项即可完成选择；\n- 例子三：想要选中当前列表的全部内容，进入多选模式后，点击全选即可完成选择（注：由于**在线列表**使用分页加载，全选只会选择目前已加载的内容，若要完整选择整个在线列表的内容则需要往下滑动将列表加载完毕再进行全选）。\n\n注：选完后可用歌曲列表三个点的菜单操作已选的内容\n\n## 无法打开外部歌单\n\n不支持垮源打开歌单，请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应（不一样的话请通过右上角切换歌单源）；<br>\n对于分享出来的歌单，若打开失败，可尝试先在浏览器中打开后，再从浏览器地址栏复制URL地址到软件打开；<br>\n或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>\n\n注：网易源的“我喜欢”歌单无法在未登录的情况下打开，所以你需要手动创建一个歌单后将“我喜欢”里的歌曲移动到该歌单打开\n\n## 播放整个歌单或排行榜\n\n播放在线列表内的歌曲需要将它们都添加到我的列表才能播放，你可以全选列表内的歌曲然后添加到现有列表或者新创建的列表，然后去播放该列表内的歌曲。\n\n## 桌面歌词启用后不显示\n\n安卓6及更高的版本需要给予LX Music显示悬浮窗的权限才能使用桌面歌词功能，请确认是否授予此权限（不懂怎么授予的话请自行百度“开启悬浮窗权限”）。\n\n## 下载功能\n\n移动端暂不支持歌曲下载功能。\n\n## 同步功能的使用（实验性，首次使用前建议先备份一次列表）\n\n**注意：由于同步传输时的数据是明文传输，请在受信任的网络下使用此功能！**<br>\n此功能需要配合PC端使用，移动端与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该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效，可右击已收藏的列表名从弹出的菜单中选择“更新”使用该功能，\n\n需要注意的是：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表。\n\n## 杀毒软件提示有病毒或恶意行为\n\n本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为，并且软件代码已开源，请自行查阅，软件安装包也是由CI拉取源代码构建，构建日志：[GitHub Actions](https://github.com/lyswhut/lx-music-mobile/actions)<br>\n尽管如此，但这不意味着软件是100%安全的，由于软件使用了第三方依赖，当这些依赖存在恶意行为时（[供应链攻击](https://docs.microsoft.com/zh-cn/windows/security/threat-protection/intelligence/supply-chain-malware)），软件也将会受到牵连，所以我只能尽量选择使用较多人用、信任度较高的依赖。<br>\n当然，以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的，或者有相关能力者还可以下载源代码自己构建安装包。\n\n最后，若出现杀毒软件报毒、存在恶意行为，请自行判断选择是否继续使用本软件！ -->\n"
  },
  {
    "path": "Gemfile",
    "content": "source 'https://rubygems.org'\n\n# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version\nruby \">= 2.6.10\"\n\ngem 'cocoapods', '~> 1.12'\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-mobile\"><img width=\"200\" src=\"https://github.com/lyswhut/lx-music-mobile/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-mobile/releases\"><img src=\"https://img.shields.io/github/release/lyswhut/lx-music-mobile\" alt=\"Release version\"></a>\n  <a href=\"https://github.com/lyswhut/lx-music-mobile/actions/workflows/release.yml\"><img src=\"https://github.com/lyswhut/lx-music-mobile/workflows/Build/badge.svg\" alt=\"Build status\"></a>\n  <a href=\"https://github.com/lyswhut/lx-music-mobile/actions/workflows/beta-pack.yml\"><img src=\"https://github.com/lyswhut/lx-music-mobile/workflows/Build%20Beta/badge.svg\" alt=\"Build status\"></a>\n  <a href=\"https://github.com/facebook/react-native\"><img src=\"https://img.shields.io/github/package-json/dependency-version/lyswhut/lx-music-mobile/react-native/master\" alt=\"React native version\"></a>\n  <!-- <a href=\"https://github.com/lyswhut/lx-music-mobile/releases\"><img src=\"https://img.shields.io/github/downloads/lyswhut/lx-music-mobile/latest/total\" alt=\"Downloads\"></a> -->\n  <a href=\"https://github.com/lyswhut/lx-music-mobile/tree/dev\"><img src=\"https://img.shields.io/github/package-json/v/lyswhut/lx-music-mobile/dev\" alt=\"Dev branch version\"></a>\n  <!-- <a href=\"https://github.com/lyswhut/lx-music-mobile/blob/master/LICENSE\"><img src=\"https://img.shields.io/github/license/lyswhut/lx-music-mobile\" alt=\"License\"></a> -->\n</p>\n\n<p align=\"center\">一个基于 React Native 开发的音乐软件</p>\n\n## 说明\n\n所用技术栈：\n\n- React Native\n- Redux\n\n已支持的平台：\n\n- Android 5 及以上\n\n***注：目前没有计划支持 iOS 和 HarmonyOS NEXT**。*<br>\n*桌面版项目地址：<https://github.com/lyswhut/lx-music-desktop>*<br>\n*LX Music 项目发展调整与新项目计划：https://github.com/lyswhut/lx-music-desktop/issues/1912*\n\n软件变化请查看[更新日志](https://github.com/lyswhut/lx-music-mobile/blob/master/CHANGELOG.md)。\n\n软件下载请查看 [GitHub Releases](https://github.com/lyswhut/lx-music-mobile/releases)。\n\n使用常见问题请参阅[移动版常见问题](https://lyswhut.github.io/lx-music-doc/mobile/faq)。\n\n目前本项目的原始发布地址只有 [**GitHub**](https://github.com/lyswhut/lx-music-mobile/releases)，其他渠道均为第三方转载发布，与本项目无关！\n\n为了提高使用门槛，本软件内的默认设置、UI 操作不以新手友好为目标，所以使用前建议先根据你的喜好浏览调整一遍软件设置，阅读一遍[音乐播放列表机制](https://lyswhut.github.io/lx-music-doc/mobile/faq/playlist)。\n\n### 数据同步服务\n\n从 v1.0.0 起，我们发布了一个独立的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)。如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明。\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/mobile/use-source-code)设置开发环境；\n2. 克隆本仓库代码并切换至 `dev` 分支进行开发；\n3. 提交 PR 至 `dev` 分支。\n\n<!--\n## 用户界面\n\n<p><img width=\"100%\" src=\"https://github.com/lyswhut/lx-music-mobile/blob/master/doc/images/app.png\" alt=\"lx-music mobile UI\"></p> -->\n\n## 项目协议\n\n本项目基于 [Apache License 2.0](https://github.com/lyswhut/lx-music-mobile/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": "android/app/build.gradle",
    "content": "apply plugin: \"com.android.application\"\napply plugin: \"org.jetbrains.kotlin.android\"\napply plugin: \"com.facebook.react\"\n\nimport com.android.build.OutputFile\nimport groovy.json.JsonSlurper\nimport org.apache.tools.ant.taskdefs.condition.Os\n\n/**\n * This is the configuration block to customize your React Native Android app.\n * By default you don't need to apply any configuration, just uncomment the lines you need.\n */\nreact {\n    /* Folders */\n    //   The root of your project, i.e. where \"package.json\" lives. Default is '..'\n    // root = file(\"../\")\n    //   The folder where the react-native NPM package is. Default is ../node_modules/react-native\n    // reactNativeDir = file(\"../node_modules/react-native\")\n    //   The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen\n    // codegenDir = file(\"../node_modules/@react-native/codegen\")\n    //   The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js\n    // cliFile = file(\"../node_modules/react-native/cli.js\")\n\n    /* Variants */\n    //   The list of variants to that are debuggable. For those we're going to\n    //   skip the bundling of the JS bundle and the assets. By default is just 'debug'.\n    //   If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.\n    // debuggableVariants = [\"liteDebug\", \"prodDebug\"]\n\n    /* Bundling */\n    //   A list containing the node command and its flags. Default is just 'node'.\n    // nodeExecutableAndArgs = [\"node\"]\n    //\n    //   The command to run when bundling. By default is 'bundle'\n    // bundleCommand = \"ram-bundle\"\n    //\n    //   The path to the CLI configuration file. Default is empty.\n    // bundleConfig = file(../rn-cli.config.js)\n    //\n    //   The name of the generated asset file containing your JS bundle\n    // bundleAssetName = \"MyApplication.android.bundle\"\n    //\n    //   The entry file for bundle generation. Default is 'index.android.js' or 'index.js'\n    // entryFile = file(\"../js/MyApplication.android.js\")\n    //\n    //   A list of extra flags to pass to the 'bundle' commands.\n    //   See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle\n    // extraPackagerArgs = []\n\n    /* Hermes Commands */\n    //   The hermes compiler command to run. By default it is 'hermesc'\n    // hermesCommand = \"$rootDir/my-custom-hermesc/bin/hermesc\"\n    //\n    //   The list of flags to pass to the Hermes compiler. By default is \"-O\", \"-output-source-map\"\n    // hermesFlags = [\"-O\", \"-output-source-map\"]\n}\n\n/**\n * Set this to true to create four separate APKs instead of one,\n * one for each native architecture. This is useful if you don't\n * use App Bundles (https://developer.android.com/guide/app-bundle/)\n * and want to have separate APKs to upload to the Play Store.\n */\ndef enableSeparateBuildPerCPUArchitecture = true\n\n/**\n * Set this to true to Run Proguard on Release builds to minify the Java bytecode.\n */\ndef enableProguardInReleaseBuilds = true\n\n/**\n * The preferred build flavor of JavaScriptCore (JSC)\n *\n * For example, to use the international variant, you can use:\n * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`\n *\n * The international variant includes ICU i18n library and necessary data\n * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that\n * give correct results when using with locales other than en-US. Note that\n * this variant is about 6MiB larger per architecture than default.\n */\ndef jscFlavor = 'org.webkit:android-jsc:+'\n\n/**\n * Private function to get the list of Native Architectures you want to build.\n * This reads the value from reactNativeArchitectures in your gradle.properties\n * file and works together with the --active-arch-only flag of react-native run-android.\n */\ndef reactNativeArchitectures() {\n    def value = project.getProperties().get(\"reactNativeArchitectures\")\n    return value ? value.split(\",\") : [\"armeabi-v7a\", \"x86\", \"x86_64\", \"arm64-v8a\"]\n}\n\n// Get version number\ndef getNpmPackageJson() {\n    def inputFile = new File(\"../package.json\")\n    def packageJson = new JsonSlurper().parseText(inputFile.text)\n    return packageJson\n}\ndef npmPackageJson = getNpmPackageJson()\ndef verCode = npmPackageJson[\"versionCode\"]\ndef verName = npmPackageJson[\"version\"]\ndef applicationName = npmPackageJson[\"name\"]\n\nandroid {\n    ndkVersion rootProject.ext.ndkVersion\n\n    buildToolsVersion rootProject.ext.buildToolsVersion\n    compileSdk rootProject.ext.compileSdkVersion\n\n    namespace \"cn.toside.music.mobile\"\n    defaultConfig {\n        applicationId \"cn.toside.music.mobile\"\n        minSdkVersion rootProject.ext.minSdkVersion\n        targetSdkVersion rootProject.ext.targetSdkVersion\n        versionCode verCode\n        versionName verName\n    }\n\n    splits {\n        abi {\n            reset()\n            enable enableSeparateBuildPerCPUArchitecture\n            universalApk true  // If true, also generate a universal APK\n            include (*reactNativeArchitectures())\n        }\n    }\n    signingConfigs {\n        release {\n            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {\n                storeFile file(MYAPP_UPLOAD_STORE_FILE)\n                storePassword MYAPP_UPLOAD_STORE_PASSWORD\n                keyAlias MYAPP_UPLOAD_KEY_ALIAS\n                keyPassword MYAPP_UPLOAD_KEY_PASSWORD\n            } else {\n                def keystorePropertiesFile = rootProject.file(\"keystore.properties\")\n                def keystoreProperties = new Properties()\n                keystoreProperties.load(new FileInputStream(keystorePropertiesFile))\n\n                storeFile file(keystoreProperties['storeFile'])\n                storePassword keystoreProperties['storePassword']\n                keyAlias keystoreProperties['keyAlias']\n                keyPassword keystoreProperties['keyPassword']\n            }\n        }\n        debug {\n            storeFile file('debug.keystore')\n            storePassword 'android'\n            keyAlias 'androiddebugkey'\n            keyPassword 'android'\n        }\n    }\n    buildTypes {\n        debug {\n            signingConfig signingConfigs.debug\n        }\n        release {\n            // Caution! In production, you need to generate your own keystore file.\n            // see https://reactnative.dev/docs/signed-apk-android.\n            signingConfig signingConfigs.debug\n            minifyEnabled enableProguardInReleaseBuilds\n            proguardFiles getDefaultProguardFile(\"proguard-android.txt\"), \"proguard-rules.pro\"\n\n            signingConfig signingConfigs.release\n        }\n    }\n\n    // applicationVariants are e.g. debug, release\n    applicationVariants.all { variant ->\n        variant.outputs.each { output ->\n            // For each separate APK per architecture, set a unique version code as described here:\n            // https://developer.android.com/studio/build/configure-apk-splits.html\n            // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.\n            def versionCodes = [\"armeabi-v7a\": 1, \"x86\": 2, \"arm64-v8a\": 3, \"x86_64\": 4]\n            def abi = output.getFilter(OutputFile.ABI)\n            if (abi == null) {  // null for the universal-debug, universal-release variants\n                output.outputFileName =\n                        \"${applicationName}-v${defaultConfig.versionName}-universal.apk\"\n            } else {\n                output.versionCodeOverride =\n                        defaultConfig.versionCode * 1000 + versionCodes.get(abi)\n                output.outputFileName =\n                        \"${applicationName}-v${defaultConfig.versionName}-${abi}.apk\"\n            }\n\n        }\n    }\n}\n\ndependencies {\n    // The version of react-native is set by the React Native Gradle Plugin\n    implementation(\"com.facebook.react:react-android\")\n    implementation(\"com.facebook.react:flipper-integration\")\n    // implementation \"androidx.javascriptengine:javascriptengine:1.0.0-alpha07\"\n    // implementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3\")\n    implementation 'wang.harlon.quickjs:wrapper-android:2.4.0'\n\n  if (hermesEnabled.toBoolean()) {\n        implementation(\"com.facebook.react:hermes-android\")\n    } else {\n        implementation jscFlavor\n    }\n}\n\napply from: file(\"../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle\"); applyNativeModulesAppBuildGradle(project)\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n-keep class com.reactnativenavigation.views.element.animators.** { *; }\n# -keepclassmembers class com.reactnativenavigation.views.element.animators.** { *; }\n\n\n-keep class org.jaudiotagger.tag.** { *; }\n\n\n-keep public class com.dylanvann.fastimage.* {*;}\n-keep public class com.dylanvann.fastimage.** {*;}\n-keep public class * implements com.bumptech.glide.module.GlideModule\n-keep public class * extends com.bumptech.glide.module.AppGlideModule\n-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {\n  **[] $VALUES;\n  public *;\n}\n"
  },
  {
    "path": "android/app/src/debug/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <application\n        android:usesCleartextTraffic=\"true\"\n        tools:targetApi=\"28\"\n        tools:ignore=\"GoogleAppIndexingWarning\" />\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  package=\"cn.toside.music.mobile\">\n\n    <!-- 获取读写外置存储权限 -->\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\" />\n    <uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\" />\n    <uses-permission android:name=\"android.permission.SYSTEM_ALERT_WINDOW\" />\n    <uses-permission android:name=\"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS\" />\n\n    <application\n      android:name=\".MainApplication\"\n      android:label=\"@string/app_name\"\n      android:networkSecurityConfig=\"@xml/network_security_config\"\n      android:requestLegacyExternalStorage=\"true\"\n      android:icon=\"@mipmap/ic_launcher\"\n      android:roundIcon=\"@mipmap/ic_launcher_round\"\n      android:allowBackup=\"false\"\n      android:theme=\"@style/AppTheme\"\n      tools:targetApi=\"n\">\n      <activity\n        android:name=\".MainActivity\"\n        android:configChanges=\"keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode\"\n        android:launchMode=\"singleTask\"\n        android:windowSoftInputMode=\"adjustResize\"\n        android:exported=\"true\">\n        <intent-filter>\n            <action android:name=\"android.intent.action.MAIN\" />\n            <category android:name=\"android.intent.category.LAUNCHER\" />\n        </intent-filter>\n        <intent-filter>\n          <action android:name=\"android.intent.action.VIEW\" />\n          <category android:name=\"android.intent.category.DEFAULT\" />\n          <category android:name=\"android.intent.category.BROWSABLE\" />\n          <data android:scheme=\"lxmusic\" android:host=\"*\" />\n        </intent-filter>\n        <intent-filter android:priority=\"80\">\n          <action android:name=\"android.intent.action.VIEW\" />\n          <category android:name=\"android.intent.category.DEFAULT\"/>\n          <category android:name=\"android.intent.category.OPENABLE\"/>\n\n          <data android:scheme=\"file\"/>\n          <data android:scheme=\"content\" />\n          <data android:mimeType=\"audio/*\" />\n          <data android:mimeType=\"vnd.android.cursor.dir/audio\"/>\n          <data android:mimeType=\"application/ogg\"/>\n          <data android:mimeType=\"application/x-ogg\"/>\n        </intent-filter>\n        <intent-filter>\n          <action android:name=\"android.intent.action.MAIN\" />\n          <category android:name=\"android.intent.category.APP_MUSIC\" />\n        </intent-filter>\n        <intent-filter>\n          <action android:name=\"com.cyanogenmod.eleven.AUDIO_PLAYER\"/>\n          <category android:name=\"android.intent.category.DEFAULT\"/>\n        </intent-filter>\n\n        <intent-filter android:priority=\"80\">\n          <!-- <action android:name=\"android.intent.action.PICK\"/> -->\n          <action android:name=\"android.intent.action.VIEW\" />\n          <category android:name=\"android.intent.category.DEFAULT\"/>\n          <category android:name=\"android.intent.category.OPENABLE\"/>\n\n          <data android:scheme=\"file\"/>\n          <data android:scheme=\"content\" />\n          <data android:host=\"*\"/>\n          <data android:mimeType=\"application/octet-stream\"/>\n\n          <data android:mimeType=\"text/javascript\"/>\n          <data android:mimeType=\"application/x-javascript\"/>\n          <data android:mimeType=\"application/javascript\"/>\n          <!-- https://stackoverflow.com/a/66402243 -->\n          <data android:pathPattern=\".*\\\\.js\" />\n          <data android:pathPattern=\".*\\\\..*\\\\.js\" />\n          <data android:pathPattern=\".*\\\\..*\\\\..*\\\\.js\" />\n          <data android:pathSuffix=\".js\" tools:targetApi=\"s\" />\n\n          <data android:mimeType=\"application/json\"/>\n          <data android:pathPattern=\".*\\\\.json\" />\n          <data android:pathPattern=\".*\\\\..*\\\\.json\" />\n          <data android:pathPattern=\".*\\\\..*\\\\..*\\\\.json\" />\n          <data android:pathSuffix=\".json\" tools:targetApi=\"s\" />\n\n          <data android:pathPattern=\".*\\\\.lxmc\" />\n          <data android:pathPattern=\".*\\\\..*\\\\.lxmc\" />\n          <data android:pathPattern=\".*\\\\..*\\\\..*\\\\.lxmc\" />\n          <data android:pathSuffix=\".lxmc\" tools:targetApi=\"s\" />\n        </intent-filter>\n      </activity>\n\n      <!-- Define a FileProvider for API24+ -->\n      <!-- note this is the authority name used by other modules like rn-fetch-blob, easy to have conflicts -->\n      <provider\n        android:name=\"androidx.core.content.FileProvider\"\n        android:authorities=\"${applicationId}.provider\"\n        android:exported=\"false\"\n        android:grantUriPermissions=\"true\">\n        <!-- you might need the tools:replace thing to workaround rn-fetch-blob or other definitions of provider -->\n        <!-- just make sure if you \"replace\" here that you include all the paths you are replacing *plus* the cache path we use -->\n        <meta-data tools:replace=\"android:resource\"\n          android:name=\"android.support.FILE_PROVIDER_PATHS\"\n          android:resource=\"@xml/file_paths\" />\n      </provider>\n    </application>\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/assets/script/user-api-preload.js",
    "content": "'use strict'\n\nglobalThis.lx_setup = (key, id, name, description, version, author, homepage, rawScript) => {\n  delete globalThis.lx_setup\n  const _nativeCall = globalThis.__lx_native_call__\n  delete globalThis.__lx_native_call__\n  const checkLength = (str, length = 1048576) => {\n    if (typeof str == 'string' && str.length > length) throw new Error('Input too long')\n    return str\n  }\n  const nativeFuncNames = [\n    '__lx_native_call__set_timeout',\n    '__lx_native_call__utils_str2b64',\n    '__lx_native_call__utils_b642buf',\n    '__lx_native_call__utils_str2md5',\n    '__lx_native_call__utils_aes_encrypt',\n    '__lx_native_call__utils_rsa_encrypt',\n  ]\n  const nativeFuncs = {}\n  for (const name of nativeFuncNames) {\n    const nativeFunc = globalThis[name]\n    delete globalThis[name]\n    nativeFuncs[name.replace('__lx_native_call__', '')] = (...args) => {\n      for (const arg of args) checkLength(arg)\n      return nativeFunc(...args)\n    }\n  }\n  // const set_timeout = globalThis.__lx_native_call__set_timeout\n  // delete globalThis.__lx_native_call__set_timeout\n  // const utils_str2b64 = globalThis.__lx_native_call__utils_str2b64\n  // delete globalThis.__lx_native_call__utils_str2b64\n  // const utils_b642buf = globalThis.__lx_native_call__utils_b642buf\n  // delete globalThis.__lx_native_call__utils_b642buf\n  // const utils_str2md5 = globalThis.__lx_native_call__utils_str2md5\n  // delete globalThis.__lx_native_call__utils_str2md5\n  // const utils_aes_encrypt = globalThis.__lx_native_call__utils_aes_encrypt\n  // delete globalThis.__lx_native_call__utils_aes_encrypt\n  // const utils_rsa_encrypt = globalThis.__lx_native_call__utils_rsa_encrypt\n  // delete globalThis.__lx_native_call__utils_rsa_encrypt\n  const KEY_PREFIX = {\n    publicKeyStart: '-----BEGIN PUBLIC KEY-----',\n    publicKeyEnd: '-----END PUBLIC KEY-----',\n    privateKeyStart: '-----BEGIN PRIVATE KEY-----',\n    privateKeyEnd: '-----END PRIVATE KEY-----',\n  }\n  const RSA_PADDING = {\n    OAEPWithSHA1AndMGF1Padding: 'RSA/ECB/OAEPWithSHA1AndMGF1Padding',\n    NoPadding: 'RSA/ECB/NoPadding',\n  }\n  const AES_MODE = {\n    CBC_128_PKCS7Padding: 'AES/CBC/PKCS7Padding',\n    ECB_128_NoPadding: 'AES',\n  }\n  const nativeCall = (action, data) => {\n    data = JSON.stringify(data)\n    // console.log('nativeCall', action, data)\n    checkLength(data, 2097152)\n    _nativeCall(key, action, data)\n  }\n\n  const callbacks = new Map()\n  let timeoutId = 0\n  const _setTimeout = (callback, timeout = 0, ...params) => {\n    if (typeof callback !== 'function') throw new Error('callback required a function')\n    if (typeof timeout !== 'number' || timeout < 0) throw new Error('timeout required a number')\n    if (timeoutId > 90000000000) throw new Error('max timeout')\n    const id = timeoutId++\n    callbacks.set(id, {\n      callback(...args) {\n        // eslint-disable-next-line n/no-callback-literal\n        callback(...args)\n      },\n      params,\n    })\n    nativeFuncs.set_timeout(id, parseInt(timeout))\n    return id\n  }\n  const _clearTimeout = (id) => {\n    const tagret = callbacks.get(id)\n    if (!tagret) return\n    callbacks.delete(id)\n  }\n  const handleSetTimeout = (id) => {\n    const tagret = callbacks.get(id)\n    if (!tagret) return\n    callbacks.delete(id)\n    tagret.callback(...tagret.params)\n  }\n\n  // 将字节数组解码为字符串（UTF-8）\n  function bytesToString(bytes) {\n    let result = ''\n    let i = 0\n    while (i < bytes.length) {\n      const byte = bytes[i]\n      if (byte < 128) {\n        result += String.fromCharCode(byte)\n        i++\n      } else if (byte >= 192 && byte < 224) {\n        result += String.fromCharCode(((byte & 31) << 6) | (bytes[i + 1] & 63))\n        i += 2\n      } else {\n        result += String.fromCharCode(((byte & 15) << 12) | ((bytes[i + 1] & 63) << 6) | (bytes[i + 2] & 63))\n        i += 3\n      }\n    }\n    return result\n  }\n  // 将字符串编码为字节数组（UTF-8）\n  function stringToBytes(inputString) {\n    const bytes = []\n    for (let i = 0; i < inputString.length; i++) {\n      const charCode = inputString.charCodeAt(i)\n      if (charCode < 128) {\n        bytes.push(charCode)\n      } else if (charCode < 2048) {\n        bytes.push((charCode >> 6) | 192)\n        bytes.push((charCode & 63) | 128)\n      } else {\n        bytes.push((charCode >> 12) | 224)\n        bytes.push(((charCode >> 6) & 63) | 128)\n        bytes.push((charCode & 63) | 128)\n      }\n    }\n    return bytes\n  }\n\n  const NATIVE_EVENTS_NAMES = {\n    init: 'init',\n    showUpdateAlert: 'showUpdateAlert',\n    request: 'request',\n    cancelRequest: 'cancelRequest',\n    response: 'response',\n    // 'utils.crypto.aesEncrypt': 'utils.crypto.aesEncrypt',\n    // 'utils.crypto.rsaEncrypt': 'utils.crypto.rsaEncrypt',\n    // 'utils.crypto.randomBytes': 'utils.crypto.randomBytes',\n    // 'utils.crypto.md5': 'utils.crypto.md5',\n    // 'utils.buffer.from': 'utils.buffer.from',\n    // 'utils.buffer.bufToString': 'utils.buffer.bufToString',\n    // 'utils.zlib.inflate': 'utils.zlib.inflate',\n    // 'utils.zlib.deflate': 'utils.zlib.deflate',\n  }\n  const EVENT_NAMES = {\n    request: 'request',\n    inited: 'inited',\n    updateAlert: 'updateAlert',\n  }\n  const eventNames = Object.values(EVENT_NAMES)\n  const events = {\n    request: null,\n  }\n  const allSources = ['kw', 'kg', 'tx', 'wy', 'mg', 'local']\n  const 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  }\n  const 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\n  const 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\n  const requestQueue = new Map()\n  let isInitedApi = false\n  let isShowedUpdateAlert = false\n\n  const sendNativeRequest = (url, options, callback) => {\n    const requestKey = Math.random().toString()\n    const requestInfo = {\n      aborted: false,\n      abort: () => {\n        nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)\n      },\n    }\n    requestQueue.set(requestKey, {\n      callback,\n      // timeout: setTimeout(() => {\n      //   const req = requestQueue.get(requestKey)\n      //   if (req) req.timeout = null\n      //   nativeCall(NATIVE_EVENTS_NAMES.cancelRequest, requestKey)\n      // }, 30000),\n      requestInfo,\n    })\n\n    nativeCall(NATIVE_EVENTS_NAMES.request, { requestKey, url, options })\n    return requestInfo\n  }\n  const handleNativeResponse = ({ requestKey, error, response }) => {\n    const targetRequest = requestQueue.get(requestKey)\n    if (!targetRequest) return\n    requestQueue.delete(requestKey)\n    targetRequest.requestInfo.aborted = true\n    // if (targetRequest.timeout) clearTimeout(targetRequest.timeout)\n    if (error == null) targetRequest.callback(null, response)\n    else targetRequest.callback(new Error(error), null)\n  }\n\n  const handleRequest = ({ requestKey, data }) => {\n    // console.log(data)\n    if (!events.request) return nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: 'Request event is not defined' })\n    try {\n      events.request.call(globalThis.lx, { source: data.source, action: data.action, info: data.info }).then(response => {\n        let result\n        switch (data.action) {\n          case 'musicUrl':\n            if (typeof response != 'string' || response.length > 2048 || !/^https?:/.test(response)) throw new Error('failed')\n            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            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            result = {\n              source: data.source,\n              action: data.action,\n              data: response,\n            }\n            break\n        }\n        nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: true, result })\n      }).catch(err => {\n        // console.log('handleRequest err', err)\n        nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })\n      })\n    } catch (err) {\n      // console.log('handleRequest call err', err)\n      nativeCall(NATIVE_EVENTS_NAMES.response, { requestKey, status: false, errorMessage: err.message })\n    }\n  }\n\n  const jsCall = (action, data) => {\n    // console.log('jsCall', action, data)\n    switch (action) {\n      case '__run_error__':\n        if (!isInitedApi) isInitedApi = true\n        return\n      case '__set_timeout__':\n        handleSetTimeout(data)\n        return\n      case 'request':\n        handleRequest(data)\n        return\n      case 'response':\n        handleNativeResponse(data)\n        return\n    }\n    return 'Unknown action: ' + action\n  }\n\n  Object.defineProperty(globalThis, '__lx_native__', {\n    enumerable: false,\n    configurable: false,\n    writable: false,\n    value: (_key, action, data) => {\n      if (key != _key) return 'Invalid key'\n      return data == null ? jsCall(action) : jsCall(action, JSON.parse(data))\n    },\n  })\n\n\n  /**\n   *\n   * @param {*} info {\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   */\n  const handleInit = (info) => {\n    if (!info) {\n      nativeCall(NATIVE_EVENTS_NAMES.init, { info: null, status: false, errorMessage: 'Missing required parameter init info' })\n      // sendMessage(NATIVE_EVENTS_NAMES.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')\n      return\n    }\n    // if (!info.status) {\n    //   nativeCall(NATIVE_EVENTS_NAMES.init, { info: null, status: false, errorMessage: 'Init failed' })\n    //   // sendMessage(NATIVE_EVENTS_NAMES.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      nativeCall(NATIVE_EVENTS_NAMES.init, { info: null, status: false, errorMessage: error.message })\n      return\n    }\n    nativeCall(NATIVE_EVENTS_NAMES.init, { info: sourceInfo, status: true })\n  }\n  const 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    nativeCall(NATIVE_EVENTS_NAMES.showUpdateAlert, { log: data.log, updateUrl: data.updateUrl, name })\n    resolve()\n  }\n\n  const dataToB64 = (data) => {\n    if (typeof data === 'string') return nativeFuncs.utils_str2b64(data)\n    else if (Array.isArray(data) || ArrayBuffer.isView(data)) return utils.buffer.bufToString(data, 'base64')\n    throw new Error('data type error: ' + typeof data + ' raw data: ' + data)\n  }\n  const utils = {\n    crypto: {\n      aesEncrypt(buffer, mode, key, iv) {\n        // console.log('aesEncrypt', buffer, mode, key, iv)\n        switch (mode) {\n          case 'aes-128-cbc':\n            return utils.buffer.from(nativeFuncs.utils_aes_encrypt(dataToB64(buffer), dataToB64(key), dataToB64(iv), AES_MODE.CBC_128_PKCS7Padding), 'base64')\n          case 'aes-128-ecb':\n            return utils.buffer.from(nativeFuncs.utils_aes_encrypt(dataToB64(buffer), dataToB64(key), '', AES_MODE.ECB_128_NoPadding), 'base64')\n          default:\n            throw new Error('Binary encoding is not supported for input strings')\n        }\n      },\n      rsaEncrypt(buffer, key) {\n        // console.log('rsaEncrypt', buffer, key)\n        if (typeof key !== 'string') throw new Error('Invalid RSA key')\n        key = key.replace(KEY_PREFIX.publicKeyStart, '')\n          .replace(KEY_PREFIX.publicKeyEnd, '')\n        return utils.buffer.from(nativeFuncs.utils_rsa_encrypt(dataToB64(buffer), key, RSA_PADDING.NoPadding), 'base64')\n      },\n      randomBytes(size) {\n        const byteArray = new Uint8Array(size)\n        for (let i = 0; i < size; i++) {\n          byteArray[i] = Math.floor(Math.random() * 256) // 随机生成一个字节的值（0-255）\n        }\n        return byteArray\n      },\n      md5(str) {\n        if (typeof str !== 'string') throw new Error('param required a string')\n        const md5 = nativeFuncs.utils_str2md5(encodeURIComponent(str))\n        // console.log('md5', str, md5)\n        return md5\n      },\n    },\n    buffer: {\n      from(input, encoding) {\n        // console.log('buffer.from', input, encoding)\n        if (typeof input === 'string') {\n          switch (encoding) {\n            case 'binary':\n              throw new Error('Binary encoding is not supported for input strings')\n            case 'base64':\n              return new Uint8Array(JSON.parse(nativeFuncs.utils_b642buf(input)))\n            case 'hex':\n              return new Uint8Array(input.match(/.{1,2}/g).map(byte => parseInt(byte, 16)))\n            default:\n              return new Uint8Array(stringToBytes(input))\n          }\n        } else if (Array.isArray(input)) {\n          return new Uint8Array(input)\n        } else {\n          throw new Error('Unsupported input type: ' + input + ' encoding: ' + encoding)\n        }\n      },\n      bufToString(buf, format) {\n        // console.log('buffer.bufToString', buf, format)\n        if (Array.isArray(buf) || ArrayBuffer.isView(buf)) {\n          switch (format) {\n            case 'binary':\n              // return new TextDecoder('latin1').decode(new Uint8Array(buf))\n              return buf\n            case 'hex':\n              return new Uint8Array(buf).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '')\n            case 'base64':\n              return nativeFuncs.utils_str2b64(bytesToString(Array.from(buf)))\n            case 'utf8':\n            case 'utf-8':\n            default:\n              return bytesToString(Array.from(buf))\n          }\n        } else {\n          throw new Error('Input is not a valid buffer: ' + buf + ' format: ' + format)\n        }\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\n  globalThis.lx = {\n    EVENT_NAMES,\n    request(url, { method = 'get', timeout, headers, body, form, formData, binary }, callback) {\n      let options = { headers, binary: binary === true }\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      if (timeout && typeof timeout == 'number' && timeout > 0) options.timeout = Math.min(timeout, 60_000)\n\n      let request = sendNativeRequest(url, { method, body, form, formData, ...options }, (err, resp) => {\n        if (err) {\n          callback(err, null, null)\n        } else {\n          callback(err, {\n            statusCode: resp.statusCode,\n            statusMessage: resp.statusMessage,\n            headers: resp.headers,\n            // bytes: resp.bytes,\n            // raw: resp.raw,\n            body: resp.body,\n          }, resp.body)\n        }\n      })\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(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    currentScriptInfo: {\n      name,\n      description,\n      version,\n      author,\n      homepage,\n      rawScript,\n    },\n    version: '2.0.0',\n    env: 'mobile',\n  }\n\n  globalThis.setTimeout = _setTimeout\n  globalThis.clearTimeout = _clearTimeout\n\n  const freezeObject = (obj) => {\n    if (typeof obj != 'object') return\n    Object.freeze(obj)\n    for (const subObj of Object.values(obj)) freezeObject(subObj)\n  }\n  freezeObject(globalThis.lx)\n\n  const _toString = Function.prototype.toString\n  // eslint-disable-next-line no-extend-native\n  Function.prototype.toString = function() {\n    return Object.getOwnPropertyDescriptors(this).name.configurable\n      ? _toString.apply(this)\n      : `function ${this.name}() { [native code] }`\n  }\n  // eslint-disable-next-line no-eval\n  globalThis.eval = function() {\n    throw new Error('eval is not available')\n  }\n  const proxyFunctionConstructor = new Proxy(Function.prototype.constructor, {\n    apply() {\n      throw new Error('Dynamic code execution is not allowed.')\n    },\n    construct() {\n      throw new Error('Dynamic code execution is not allowed.')\n    },\n  })\n  // eslint-disable-next-line no-extend-native\n  Object.defineProperty(Function.prototype, 'constructor', {\n    value: proxyFunctionConstructor,\n    writable: false,\n    configurable: false,\n    enumerable: false,\n  })\n  globalThis.Function = proxyFunctionConstructor\n  // globalThis.Function = function() {\n  //   throw new Error('Function is not available')\n  // }\n\n  const excludes = [\n    Function.prototype.toString,\n    Function.prototype.toLocaleString,\n    Object.prototype.toString,\n  ]\n  const freezeObjectProperty = (obj, freezedObj = new Set()) => {\n    if (obj == null) return\n    switch (typeof obj) {\n      case 'object':\n      case 'function':\n        if (freezedObj.has(obj)) return\n        // Object.freeze(obj)\n        freezedObj.add(obj)\n        for (const [name, { ...config }] of Object.entries(Object.getOwnPropertyDescriptors(obj))) {\n          if (!excludes.includes(config.value)) {\n            if (config.writable) config.writable = false\n            if (config.configurable) config.configurable = false\n            Object.defineProperty(obj, name, config)\n          }\n          freezeObjectProperty(config.value, freezedObj)\n        }\n    }\n  }\n  freezeObjectProperty(globalThis)\n\n  console.log('Preload finished.')\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/MainActivity.java",
    "content": "package cn.toside.music.mobile;\n\nimport com.reactnativenavigation.NavigationActivity;\nimport com.facebook.react.ReactActivityDelegate;\nimport com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;\nimport com.facebook.react.defaults.DefaultReactActivityDelegate;\n\npublic class MainActivity extends NavigationActivity {\n\n\n\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/MainApplication.java",
    "content": "package cn.toside.music.mobile;\n\nimport com.facebook.react.PackageList;\nimport com.facebook.react.flipper.ReactNativeFlipper;\nimport com.reactnativenavigation.NavigationApplication;\nimport com.facebook.react.ReactNativeHost;\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;\nimport com.reactnativenavigation.react.NavigationReactNativeHost;\nimport java.util.List;\n\nimport cn.toside.music.mobile.cache.CachePackage;\nimport cn.toside.music.mobile.crypto.CryptoPackage;\nimport cn.toside.music.mobile.lyric.LyricPackage;\nimport cn.toside.music.mobile.userApi.UserApiPackage;\nimport cn.toside.music.mobile.utils.UtilsPackage;\n\npublic class MainApplication extends NavigationApplication {\n\n  private final ReactNativeHost mReactNativeHost =\n      new NavigationReactNativeHost(this) {\n        @Override\n        public boolean getUseDeveloperSupport() {\n          return BuildConfig.DEBUG;\n        }\n\n        @Override\n        protected List<ReactPackage> getPackages() {\n          @SuppressWarnings(\"UnnecessaryLocalVariable\")\n          List<ReactPackage> packages = new PackageList(this).getPackages();\n          // Packages that cannot be autolinked yet can be added manually here, for example:\n          // packages.add(new MyReactNativePackage());\n          packages.add(new CachePackage());\n          packages.add(new LyricPackage());\n          packages.add(new UtilsPackage());\n          packages.add(new CryptoPackage());\n          packages.add(new UserApiPackage());\n          return packages;\n        }\n\n        @Override\n        protected String getJSMainModuleName() {\n          return \"index\";\n        }\n\n        @Override\n        protected boolean isNewArchEnabled() {\n          return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;\n        }\n\n        @Override\n        protected Boolean isHermesEnabled() {\n          return BuildConfig.IS_HERMES_ENABLED;\n        }\n      };\n\n  @Override\n  public ReactNativeHost getReactNativeHost() {\n    return mReactNativeHost;\n  }\n\n  @Override\n  public void onCreate() {\n    super.onCreate();\n\n    if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {\n      // If you opted-in for the New Architecture, we load the native entry point for this app.\n      DefaultNewArchitectureEntryPoint.load();\n    }\n    ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/cache/CacheClearAsyncTask.java",
    "content": "package cn.toside.music.mobile.cache;\n\nimport android.os.AsyncTask;\n\nimport com.facebook.react.bridge.Promise;\n\n// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear\npublic class CacheClearAsyncTask extends AsyncTask<Integer,Integer,String> {\n  public CacheModule cacheModule = null;\n  public Promise promise;\n  public CacheClearAsyncTask(CacheModule clearCacheModule, Promise promise) {\n    super();\n    this.cacheModule = clearCacheModule;\n    this.promise = promise;\n  }\n\n  @Override\n  protected void onPreExecute() {\n    super.onPreExecute();\n  }\n\n  @Override\n  protected void onPostExecute(String s) {\n    super.onPostExecute(s);\n    promise.resolve(null);\n  }\n\n  @Override\n  protected String doInBackground(Integer... params) {\n    cacheModule.clearCache();\n    return null;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/cache/CacheModule.java",
    "content": "package cn.toside.music.mobile.cache;\n\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactContextBaseJavaModule;\nimport com.facebook.react.bridge.ReactMethod;\n\nimport java.io.File;\n\nimport static cn.toside.music.mobile.cache.Utils.clearCacheFolder;\nimport static cn.toside.music.mobile.cache.Utils.getDirSize;\nimport static cn.toside.music.mobile.cache.Utils.isMethodsCompat;\n\n// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear\npublic class CacheModule extends ReactContextBaseJavaModule {\n  private final CacheModule cacheModule;\n\n  CacheModule(ReactApplicationContext reactContext) {\n    super(reactContext);\n    this.cacheModule = this;\n  }\n\n  @Override\n  public String getName() {\n    return \"CacheModule\";\n  }\n\n\n  @ReactMethod\n  public void getAppCacheSize(Promise promise) {\n    // 计算缓存大小\n    long fileSize = 0;\n    // File filesDir = getReactApplicationContext().getFilesDir();// /data/data/package_name/files\n    File cacheDir = getReactApplicationContext().getCacheDir();// /data/data/package_name/cache\n    // fileSize += getDirSize(filesDir);\n    fileSize += getDirSize(cacheDir);\n    // 2.2版本才有将应用缓存转移到sd卡的功能\n    if (isMethodsCompat(android.os.Build.VERSION_CODES.FROYO)) {\n      File externalCacheDir = Utils.getExternalCacheDir(getReactApplicationContext());//\"<sdcard>/Android/data/<package_name>/cache/\"\n      fileSize += getDirSize(externalCacheDir);\n    }\n\n    promise.resolve(String.valueOf(fileSize));\n  }\n\n  //清除缓存\n  @ReactMethod\n  public void clearAppCache(Promise promise) {\n    CacheClearAsyncTask asyncTask = new CacheClearAsyncTask(cacheModule, promise);\n    asyncTask.execute(10);\n  }\n\n  /**\n   * 清除app缓存\n   */\n  public void clearCache() {\n\n    getReactApplicationContext().deleteDatabase(\"webview.db\");\n    getReactApplicationContext().deleteDatabase(\"webview.db-shm\");\n    getReactApplicationContext().deleteDatabase(\"webview.db-wal\");\n    getReactApplicationContext().deleteDatabase(\"webviewCache.db\");\n    getReactApplicationContext().deleteDatabase(\"webviewCache.db-shm\");\n    getReactApplicationContext().deleteDatabase(\"webviewCache.db-wal\");\n    //清除数据缓存\n    // clearCacheFolder(getReactApplicationContext().getFilesDir(), System.currentTimeMillis());\n    clearCacheFolder(getReactApplicationContext().getCacheDir(), System.currentTimeMillis());\n    //2.2版本才有将应用缓存转移到sd卡的功能\n    if (isMethodsCompat(android.os.Build.VERSION_CODES.FROYO)) {\n      clearCacheFolder(Utils.getExternalCacheDir(getReactApplicationContext()), System.currentTimeMillis());\n    }\n\n  }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/cache/CachePackage.java",
    "content": "package cn.toside.music.mobile.cache;\n\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.bridge.NativeModule;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.uimanager.ViewManager;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class CachePackage implements ReactPackage {\n\n  @Override\n  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {\n    return Arrays.<NativeModule>asList(new CacheModule(reactContext));\n  }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/cache/Utils.java",
    "content": "package cn.toside.music.mobile.cache;\n\nimport android.content.Context;\n\nimport java.io.File;\n\n// https://github.com/midas-gufei/react-native-clear-app-cache/tree/master/android/src/main/java/com/learnta/clear\npublic class Utils {\n  /**\n   * 获取目录文件大小\n   *\n   * @param dir\n   * @return\n   */\n  static public long getDirSize(File dir) {\n    if (dir == null || !dir.isDirectory()) return 0;\n    long dirSize = 0;\n    File[] files = dir.listFiles();\n    if (files == null) return dirSize;\n    for (File file : files) {\n      if (file.isFile()) {\n        dirSize += file.length();\n      } else if (file.isDirectory()) {\n        dirSize += file.length();\n        dirSize += getDirSize(file); // 递归调用继续统计\n      }\n    }\n    return dirSize;\n  }\n\n  /**\n   * 判断当前版本是否兼容目标版本的方法\n   *\n   * @param VersionCode\n   * @return\n   */\n  static public boolean isMethodsCompat(int VersionCode) {\n    int currentVersion = android.os.Build.VERSION.SDK_INT;\n    return currentVersion >= VersionCode;\n  }\n\n  static public File getExternalCacheDir(Context context) {\n\n    // return context.getExternalCacheDir(); API level 8\n\n    // e.g. \"<sdcard>/Android/data/<package_name>/cache/\"\n\n    return context.getExternalCacheDir();\n  }\n\n  /**\n   * 清除缓存目录\n   * 目录\n   * 当前系统时间\n   */\n  static public int clearCacheFolder(File dir, long curTime) {\n    int deletedFiles = 0;\n    if (dir == null || !dir.isDirectory()) return deletedFiles;\n    File[] files = dir.listFiles();\n    if (files == null) return deletedFiles;\n    try {\n      for (File child : files) {\n        if (child.isDirectory()) {\n          deletedFiles += clearCacheFolder(child, curTime);\n        }\n        if (child.lastModified() < curTime) {\n          if (child.delete()) {\n            deletedFiles++;\n          }\n        }\n      }\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n    return deletedFiles;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/crypto/AES.java",
    "content": "package cn.toside.music.mobile.crypto;\n\nimport android.util.Base64;\n\nimport java.nio.charset.StandardCharsets;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\n\npublic class AES {\n  private static final String AES_MODE_CBC_PKCS7Padding = \"AES/CBC/PKCS7Padding\";\n  private static final String AES_MODE_ECB_NoPadding = \"AES/ECB/NoPadding\";\n\n  private static byte[] decodeBase64(String data) {\n    return Base64.decode(data, Base64.DEFAULT);\n  }\n\n  private static String encodeBase64(byte[] data) {\n    return new String(Base64.encode(data, Base64.NO_WRAP), StandardCharsets.UTF_8);\n  }\n\n  public static String encrypt(byte[] data, byte[] key, byte[] iv, String mode) {\n    String encryptedBase64 = \"\";\n    try {\n      Cipher cipher = Cipher.getInstance(mode);\n      SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n      byte[] finalIvs = new byte[16];\n      int len = Math.min(iv.length, 16);\n      System.arraycopy(iv, 0, finalIvs, 0, len);\n      IvParameterSpec ivps = new IvParameterSpec(finalIvs);\n      cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivps);\n      encryptedBase64 = encodeBase64(cipher.doFinal(data));\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return encryptedBase64;\n  }\n\n  public static String encrypt(byte[] data, byte[] key, String mode) {\n    String encryptedBase64 = \"\";\n    try {\n      Cipher cipher = Cipher.getInstance(mode);\n      SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n      cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);\n      encryptedBase64 = encodeBase64(cipher.doFinal(data));\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return encryptedBase64;\n  }\n\n  public static String encrypt(String data, String key, String iv, String mode) {\n    return \"\".equals(iv) ? encrypt(decodeBase64(data), decodeBase64(key), mode)\n      : encrypt(decodeBase64(data), decodeBase64(key), decodeBase64(iv), mode);\n  }\n\n\n  public static String decrypt(byte[] data, byte[] key, byte[] iv, String mode) {\n    String decryptedString = \"\";\n    try {\n      Cipher cipher = Cipher.getInstance(mode);\n      SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n      byte[] finalIvs = new byte[16];\n      int len = Math.min(iv.length, 16);\n      System.arraycopy(iv, 0, finalIvs, 0, len);\n      IvParameterSpec ivps = new IvParameterSpec(finalIvs);\n      cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivps);\n      decryptedString = new String(cipher.doFinal(data), StandardCharsets.UTF_8);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return decryptedString;\n  }\n\n  public static String decrypt(byte[] data, byte[] key, String mode) {\n    String decryptedString = \"\";\n    try {\n      Cipher cipher = Cipher.getInstance(mode);\n      SecretKeySpec secretKeySpec = new SecretKeySpec(key, \"AES\");\n      cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);\n      decryptedString = new String(cipher.doFinal(data), StandardCharsets.UTF_8);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return decryptedString;\n  }\n\n  public static String decrypt(String data, String key, String iv, String mode) {\n    return \"\".equals(iv) ? decrypt(decodeBase64(data), decodeBase64(key), mode)\n      : decrypt(decodeBase64(data), decodeBase64(key), decodeBase64(iv), mode);\n  }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/crypto/CryptoModule.java",
    "content": "package cn.toside.music.mobile.crypto;\n\nimport android.util.Base64;\n\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactContextBaseJavaModule;\nimport com.facebook.react.bridge.ReactMethod;\nimport com.facebook.react.bridge.WritableMap;\n\nimport java.security.Key;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.spec.InvalidKeySpecException;\nimport java.security.spec.KeySpec;\nimport java.security.spec.PKCS8EncodedKeySpec;\n\npublic class CryptoModule extends ReactContextBaseJavaModule {\n  private final ReactApplicationContext reactContext;\n\n  CryptoModule(ReactApplicationContext reactContext) {\n    super(reactContext);\n    this.reactContext = reactContext;\n  }\n\n  @Override\n  public String getName() {\n    return \"CryptoModule\";\n  }\n\n  @ReactMethod\n  public void generateRsaKey(Promise promise) {\n    KeyPair kp = RSA.getKeyPair();\n    String publicKeyBytesBase64 = new String(Base64.encode(kp.getPublic().getEncoded(), Base64.DEFAULT));\n\n    KeyFactory keyFac;\n    try {\n      keyFac = KeyFactory.getInstance(\"RSA\");\n    } catch (NoSuchAlgorithmException e) {\n      promise.reject(\"-1\", e.toString());\n      return;\n    }\n    KeySpec keySpec = new PKCS8EncodedKeySpec(kp.getPrivate().getEncoded());\n    Key key;\n    try {\n      key = keyFac.generatePrivate(keySpec);\n    } catch (InvalidKeySpecException e) {\n      promise.reject(\"-1\", e.toString());\n      return;\n    }\n    String privateKeyBytesBase64 = new String(Base64.encode(key.getEncoded(), Base64.DEFAULT));\n    WritableMap params = Arguments.createMap();\n    params.putString(\"publicKey\", publicKeyBytesBase64);\n    params.putString(\"privateKey\", privateKeyBytesBase64);\n    promise.resolve(params);\n  }\n\n  @ReactMethod\n  public void rsaEncrypt(String text, String key, String padding, Promise promise) {\n    promise.resolve(RSA.encryptRSAToString(text, key, padding));\n    //    TaskRunner taskRunner = new TaskRunner();\n    //    try {\n    //      taskRunner.executeAsync(new GzipModule.UnGzip(source, target, force), (String errMessage) -> {\n    //        if (\"\".equals(errMessage)) {\n    //          promise.resolve(null);\n    //        } else promise.reject(\"-2\", errMessage);\n    //      });\n    //    } catch (RuntimeException err) {\n    //      promise.reject(\"-2\", err.getMessage());\n    //    }\n  }\n\n  @ReactMethod\n  public void rsaDecrypt(String text, String key, String padding, Promise promise) {\n    promise.resolve(RSA.decryptRSAToString(text, key, padding));\n    //    TaskRunner taskRunner = new TaskRunner();\n    //    try {\n    //      taskRunner.executeAsync(new GzipModule.UnGzip(source, target, force), (String errMessage) -> {\n    //        if (\"\".equals(errMessage)) {\n    //          promise.resolve(null);\n    //        } else promise.reject(\"-2\", errMessage);\n    //      });\n    //    } catch (RuntimeException err) {\n    //      promise.reject(\"-2\", err.getMessage());\n    //    }\n  }\n\n  @ReactMethod(isBlockingSynchronousMethod = true)\n  public String rsaEncryptSync(String text, String key, String padding) {\n    return RSA.encryptRSAToString(text, key, padding);\n  }\n\n  @ReactMethod(isBlockingSynchronousMethod = true)\n  public String rsaDecryptSync(String text, String key, String padding) {\n    return RSA.decryptRSAToString(text, key, padding);\n  }\n\n  @ReactMethod\n  public void aesEncrypt(String text, String key, String iv, String mode, Promise promise) {\n    promise.resolve(AES.encrypt(text, key, iv, mode));\n    //    TaskRunner taskRunner = new TaskRunner();\n    //    try {\n    //      taskRunner.executeAsync(new GzipModule.UnGzip(source, target, force), (String errMessage) -> {\n    //        if (\"\".equals(errMessage)) {\n    //          promise.resolve(null);\n    //        } else promise.reject(\"-2\", errMessage);\n    //      });\n    //    } catch (RuntimeException err) {\n    //      promise.reject(\"-2\", err.getMessage());\n    //    }\n  }\n\n  @ReactMethod\n  public void aesDecrypt(String text, String key, String iv, String mode, Promise promise) {\n    promise.resolve(AES.decrypt(text, key, iv, mode));\n    //    TaskRunner taskRunner = new TaskRunner();\n    //    try {\n    //      taskRunner.executeAsync(new GzipModule.UnGzip(source, target, force), (String errMessage) -> {\n    //        if (\"\".equals(errMessage)) {\n    //          promise.resolve(null);\n    //        } else promise.reject(\"-2\", errMessage);\n    //      });\n    //    } catch (RuntimeException err) {\n    //      promise.reject(\"-2\", err.getMessage());\n    //    }\n  }\n\n  @ReactMethod(isBlockingSynchronousMethod = true)\n  public String aesEncryptSync(String text, String key, String iv, String mode) {\n    return AES.encrypt(text, key, iv, mode);\n  }\n\n  @ReactMethod(isBlockingSynchronousMethod = true)\n  public String aesDecryptSync(String text, String key, String iv, String mode) {\n    return AES.decrypt(text, key, iv, mode);\n  }\n\n}\n\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/crypto/CryptoPackage.java",
    "content": "package cn.toside.music.mobile.crypto;\n\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.bridge.NativeModule;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.uimanager.ViewManager;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class CryptoPackage implements ReactPackage {\n\n  @Override\n  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {\n    return Arrays.<NativeModule>asList(new CryptoModule(reactContext));\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/crypto/RSA.java",
    "content": "package cn.toside.music.mobile.crypto;\n\nimport android.util.Base64;\n\nimport java.security.Key;\nimport java.security.KeyFactory;\nimport java.security.KeyPair;\nimport java.security.KeyPairGenerator;\nimport java.security.spec.KeySpec;\nimport java.security.spec.PKCS8EncodedKeySpec;\nimport java.security.spec.X509EncodedKeySpec;\n\nimport javax.crypto.Cipher;\n\npublic class RSA {\n  static final String PADDING_OAEPWithSHA1AndMGF1Padding = \"RSA/ECB/OAEPWithSHA1AndMGF1Padding\";\n  static final String PADDING_NoPadding = \"RSA/ECB/NoPadding\";\n\n  // https://stackoverflow.com/a/40978042\n//  public void testEncryptData(String dataToEncrypt) throws NoSuchAlgorithmException, InvalidKeySpecException {\n//    // generate a new public/private key pair to test with (note. you should only do this once and keep them!)\n//    KeyPair kp = getKeyPair();\n//\n//    PublicKey publicKey = kp.getPublic();\n//    byte[] publicKeyBytes = publicKey.getEncoded();\n//    String publicKeyBytesBase64 = new String(Base64.encode(publicKeyBytes, Base64.DEFAULT));\n//    Log.d(\"RSATest\", \"publicKeyBytesBase64: \" + publicKeyBytesBase64);\n//\n//    PrivateKey privateKey = kp.getPrivate();\n//    KeyFactory keyFac = KeyFactory.getInstance(\"RSA\");\n//    KeySpec keySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());\n//    Key key = keyFac.generatePrivate(keySpec);\n//\n//    StringBuilder sb = new StringBuilder();\n//    sb.append(\"-----BEGIN PRIVATE KEY-----\");\n//    sb.append(new String(Base64.encode(key.getEncoded(), Base64.DEFAULT)));\n//    sb.append(\"-----END PRIVATE KEY-----\");\n//    Log.d(\"RSATest\", \"privateKeyBytesBase64: \" + sb);\n//\n//    //    return new String(Base64.encode(sb.toString().getBytes()));\n//\n//    byte[] privateKeyBytes = privateKey.getEncoded();\n//    String privateKeyBytesBase64 = new String(Base64.encode(privateKeyBytes, Base64.DEFAULT));\n//    Log.d(\"RSATest\", \"privateKeyBytesBase64: \" + privateKeyBytesBase64);\n//\n//    // test encryption\n//    String encrypted = encryptRSAToString(dataToEncrypt, publicKeyBytesBase64);\n//\n//    Log.d(\"RSATest\", \"encrypted: \" + encrypted);\n//\n//    // test decryption\n//    String decrypted = decryptRSAToString(encrypted, privateKeyBytesBase64);\n//\n//    Log.d(\"RSATest\", \"decrypted: \" + decrypted);\n//  }\n\n  public static KeyPair getKeyPair() {\n    KeyPair kp = null;\n    try {\n      KeyPairGenerator kpg = KeyPairGenerator.getInstance(\"RSA\");\n      kpg.initialize(2048);\n      kp = kpg.generateKeyPair();\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return kp;\n  }\n\n   public static String encryptRSAToString(String decryptedBase64, String publicKey, String padding) {\n    String encryptedBase64 = \"\";\n    try {\n      KeyFactory keyFac = KeyFactory.getInstance(\"RSA\");\n      KeySpec keySpec = new X509EncodedKeySpec(Base64.decode(publicKey.trim().getBytes(), Base64.DEFAULT));\n      Key key = keyFac.generatePublic(keySpec);\n\n      // get an RSA cipher object and print the provider\n      final Cipher cipher = Cipher.getInstance(padding);\n      // encrypt the plain text using the public key\n      cipher.init(Cipher.ENCRYPT_MODE, key);\n\n      byte[] encryptedBytes = cipher.doFinal(Base64.decode(decryptedBase64, Base64.DEFAULT));\n      encryptedBase64 = new String(Base64.encode(encryptedBytes, Base64.NO_WRAP));\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return encryptedBase64;\n  }\n\n  public static String decryptRSAToString(String encryptedBase64, String privateKey, String padding) {\n\n    String decryptedString = \"\";\n    try {\n      KeyFactory keyFac = KeyFactory.getInstance(\"RSA\");\n      KeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(privateKey.trim().getBytes(), Base64.DEFAULT));\n      Key key = keyFac.generatePrivate(keySpec);\n\n      // get an RSA cipher object and print the provider\n      final Cipher cipher = Cipher.getInstance(padding);\n      // encrypt the plain text using the public key\n      cipher.init(Cipher.DECRYPT_MODE, key);\n\n      byte[] encryptedBytes = Base64.decode(encryptedBase64, Base64.DEFAULT);\n      byte[] decryptedBytes = cipher.doFinal(encryptedBytes);\n      decryptedString = new String(decryptedBytes);\n    } catch (Exception e) {\n      e.printStackTrace();\n    }\n\n    return decryptedString;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/Lyric.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.os.Bundle;\nimport android.util.Log;\n\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.WritableMap;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class Lyric extends LyricPlayer {\n  LyricView lyricView = null;\n  LyricEvent lyricEvent = null;\n  ReactApplicationContext reactAppContext;\n\n  boolean isRunPlayer = false;\n  // String lastText = \"LX Music ^-^\";\n  int lastLine = 0;\n  List lines = new ArrayList();\n  boolean isShowTranslation;\n  boolean isShowRoma;\n  boolean isShowLyricView = false;\n  boolean isSendLyricTextEvent = false;\n  boolean isScreenOff = false;\n  String lyricText = \"\";\n  String translationText = \"\";\n  String romaLyricText = \"\";\n\n  Lyric(ReactApplicationContext reactContext, boolean isShowTranslation, boolean isShowRoma, float playbackRate) {\n    this.reactAppContext = reactContext;\n    this.isShowTranslation = isShowTranslation;\n    this.isShowRoma = isShowRoma;\n    this.playbackRate = playbackRate;\n    registerScreenBroadcastReceiver();\n    // checkA2DPConnection(reactContext);\n  }\n\n  private void registerScreenBroadcastReceiver() {\n    final IntentFilter theFilter = new IntentFilter();\n    /** System Defined Broadcast */\n    theFilter.addAction(Intent.ACTION_SCREEN_ON);\n    theFilter.addAction(Intent.ACTION_SCREEN_OFF);\n\n    BroadcastReceiver screenOnOffReceiver = new BroadcastReceiver() {\n      @Override\n      public void onReceive(Context context, Intent intent) {\n        String strAction = intent.getAction();\n\n        switch (Objects.requireNonNull(strAction)) {\n          case Intent.ACTION_SCREEN_OFF:\n            Log.d(\"Lyric\", \"ACTION_SCREEN_OFF\");\n            handleScreenOff();\n            break;\n          case Intent.ACTION_SCREEN_ON:\n            Log.d(\"Lyric\", \"ACTION_SCREEN_ON\");\n            handleScreenOn();\n            break;\n        }\n      }\n    };\n\n    reactAppContext.registerReceiver(screenOnOffReceiver, theFilter);\n  }\n\n  // private void checkA2DPConnection(Context context) {\n  //   BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();\n\n  //   if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {\n  //     bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {\n  //       @Override\n  //       public void onServiceConnected(int profile, BluetoothProfile proxy) {\n  //         if (profile == BluetoothProfile.A2DP) {\n  //           List<BluetoothDevice> connectedDevices = proxy.getConnectedDevices();\n  //           if (!connectedDevices.isEmpty()) {\n  //             System.out.println(\"已连接的 A2DP 媒体设备：\");\n  //             for (BluetoothDevice device : connectedDevices) {\n  //               System.out.println(\"设备名称: \" + \"地址: \" + device.getAddress());\n  //             }\n  //           } else {\n  //             System.out.println(\"没有连接的 A2DP 媒体设备\");\n  //           }\n  //         }\n  //         bluetoothAdapter.closeProfileProxy(profile, proxy);\n  //       }\n\n  //       @Override\n  //       public void onServiceDisconnected(int profile) {\n  //         // 服务断开时的处理\n  //         System.out.println(\"蓝牙服务断开时的处理\");\n  //       }\n  //     }, BluetoothProfile.A2DP);\n  //   } else {\n  //     System.out.println(\"蓝牙未开启或设备不支持蓝牙\");\n  //   }\n  // }\n\n  private boolean isDisableAutoPause() {\n    return !isRunPlayer || isSendLyricTextEvent;\n  }\n  private void handleScreenOff() {\n    isScreenOff = true;\n    if (isDisableAutoPause()) return;\n    setTempPause(true);\n  }\n\n  private void handleScreenOn() {\n    isScreenOff = false;\n    if (isDisableAutoPause()) return;\n    if (lyricView == null) lyricView = new LyricView(reactAppContext, lyricEvent);\n    lyricView.runOnUiThread(() -> {\n      handleGetCurrentLyric(lastLine);\n      setTempPause(false);\n    });\n  }\n\n  private void pausePlayer() {\n    if (!isRunPlayer || isShowLyricView || isSendLyricTextEvent) return;\n    isRunPlayer = false;\n    this.pause();\n  }\n\n  private void setCurrentLyric(String lyric, ArrayList<String> extendedLyrics) {\n    if (isShowLyricView && !isScreenOff && lyricView != null) {\n      lyricView.setLyric(lyric, extendedLyrics);\n    }\n    if (isSendLyricTextEvent) {\n      WritableMap params = Arguments.createMap();\n      params.putString(\"text\", lyric);\n      params.putArray(\"extendedLyrics\", Arguments.makeNativeArray(extendedLyrics));\n      lyricEvent.sendEvent(lyricEvent.LYRIC_Line_PLAY, params);\n    }\n  }\n  private void handleGetCurrentLyric(int lineNum) {\n    lastLine = lineNum;\n    if (lineNum >= 0 && lineNum < lines.size()) {\n      HashMap line = (HashMap) lines.get(lineNum);\n      if (line != null) {\n        setCurrentLyric((String) line.get(\"text\"), (ArrayList<String>) line.get(\"extendedLyrics\"));\n        return;\n      }\n    }\n    setCurrentLyric(\"\", new ArrayList<>(0));\n  }\n\n  public void setSendLyricTextEvent(boolean isSend) {\n    if (isSendLyricTextEvent == isSend) return;\n    isSendLyricTextEvent = isSend;\n    if (isSend) {\n      if (lyricEvent == null) lyricEvent = new LyricEvent(reactAppContext);\n      isRunPlayer = true;\n    } else {\n      pausePlayer();\n    }\n  }\n\n  public void showDesktopLyric(Bundle options, Promise promise) {\n    if (isShowLyricView) return;\n    if (lyricEvent == null) lyricEvent = new LyricEvent(reactAppContext);\n    isShowLyricView = true;\n    if (lyricView == null) lyricView = new LyricView(reactAppContext, lyricEvent);\n    try {\n      lyricView.showLyricView(options);\n    } catch (Exception e) {\n      promise.reject(e);\n      Log.e(\"Lyric\", e.getMessage());\n      return;\n    }\n    isRunPlayer = true;\n    promise.resolve(null);\n  }\n\n  public void hideDesktopLyric() {\n    if (!isShowLyricView) return;\n    isShowLyricView = false;\n    pausePlayer();\n    if (lyricView != null) {\n      lyricView.destroy();\n      lyricView = null;\n    }\n  }\n\n  private void refreshLyric() {\n    if (!isRunPlayer) return;\n    ArrayList<String> extendedLyrics = new ArrayList<>(2);\n    if (isShowTranslation && !\"\".equals(translationText)) extendedLyrics.add(translationText);\n    if (isShowRoma && !\"\".equals(romaLyricText)) extendedLyrics.add(romaLyricText);\n    super.setLyric(lyricText, extendedLyrics);\n  }\n\n  public void setLyric(String lyric, String translation, String romaLyric) {\n    lyricText = lyric;\n    translationText = translation;\n    romaLyricText = romaLyric;\n    refreshLyric();\n  }\n\n  @Override\n  public void onSetLyric(List lines) {\n    this.lines = lines;\n    handleGetCurrentLyric(-1);\n    // for (int i = 0; i < lines.size(); i++) {\n    //   HashMap line = (HashMap) lines.get(i);\n    //   Log.d(\"Lyric\", \"onSetLyric: \" +(String) line.get(\"text\") + \" \" + line.get(\"extendedLyrics\"));\n    // }\n  }\n\n  @Override\n  public void onPlay(int lineNum) {\n    handleGetCurrentLyric(lineNum);\n    // Log.d(\"Lyric\", lineNum + \" \" + text + \" \" + (String) line.get(\"translation\"));\n  }\n\n  public void pauseLyric() {\n    pause();\n    if (!isRunPlayer) return;\n    handleGetCurrentLyric(-1);\n  }\n\n  public void lockLyric() {\n    if (lyricView == null) return;\n    lyricView.lockView();\n  }\n\n  public void unlockLyric() {\n    if (lyricView == null) return;\n    lyricView.unlockView();\n  }\n\n  public void setMaxLineNum(int maxLineNum) {\n    if (lyricView == null) return;\n    lyricView.setMaxLineNum(maxLineNum);\n  }\n\n  public void setWidth(int width) {\n    if (lyricView == null) return;\n    lyricView.setWidth(width);\n  }\n\n  public void setSingleLine(boolean singleLine) {\n    if (lyricView == null) return;\n    lyricView.setSingleLine(singleLine);\n  }\n\n  public void setShowToggleAnima(boolean showToggleAnima) {\n    if (lyricView == null) return;\n    lyricView.setShowToggleAnima(showToggleAnima);\n  }\n\n  public void toggleTranslation(boolean isShowTranslation) {\n    this.isShowTranslation = isShowTranslation;\n    refreshLyric();\n  }\n\n  public void toggleRoma(boolean isShowRoma) {\n    this.isShowRoma = isShowRoma;\n    refreshLyric();\n  }\n\n  public void setPlayedColor(String unplayColor, String playedColor, String shadowColor) {\n    if (lyricView == null) return;\n    lyricView.setColor(unplayColor, playedColor, shadowColor);\n  }\n\n  public void setAlpha(float alpha) {\n    if (lyricView == null) return;\n    lyricView.setAlpha(alpha);\n  }\n\n  public void setTextSize(float size) {\n    if (lyricView == null) return;\n    lyricView.setTextSize(size);\n  }\n\n  public void setLyricTextPosition(String positionX, String positionY) {\n    if (lyricView == null) return;\n    lyricView.setLyricTextPosition(positionX, positionY);\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricEvent.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport androidx.annotation.Nullable;\n\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.WritableMap;\nimport com.facebook.react.modules.core.DeviceEventManagerModule;\n\npublic class LyricEvent {\n  final String SET_VIEW_POSITION = \"set-position\";\n  final String LYRIC_Line_PLAY = \"lyric-line-play\";\n\n  private final ReactApplicationContext reactContext;\n  LyricEvent(ReactApplicationContext reactContext) { this.reactContext = reactContext; }\n\n  public void sendEvent(String eventName, @Nullable WritableMap params) {\n    // Log.d(\"Lyric\", \"senEvent: \" + eventName);\n    reactContext\n      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)\n      .emit(eventName, params);\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricModule.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.provider.Settings;\nimport android.util.Log;\n\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactContextBaseJavaModule;\nimport com.facebook.react.bridge.ReactMethod;\nimport com.facebook.react.bridge.ReadableMap;\n\npublic class LyricModule extends ReactContextBaseJavaModule {\n  private final ReactApplicationContext reactContext;\n  Lyric lyric;\n  // final Map<String, Object> constants = new HashMap<>();\n\n  boolean isShowTranslation = false;\n  boolean isShowRoma = false;\n  float playbackRate = 1;\n\n  private int listenerCount = 0;\n\n  LyricModule(ReactApplicationContext reactContext) {\n    super(reactContext);\n    this.reactContext = reactContext;\n\n    // constants.put(\"THEME_GREEN\", \"#07c556\");\n    // constants.put(\"THEME_YELLOW\", \"#fffa12\");\n    // constants.put(\"THEME_BLUE\", \"#19b5fe\");\n    // constants.put(\"THEME_RED\", \"#ff1222\");\n    // constants.put(\"THEME_PINK\", \"#f1828d\");\n    // constants.put(\"THEME_PURPLE\", \"#c851d4\");\n    // constants.put(\"THEME_ORANGE\", \"#fffa12\");\n    // constants.put(\"THEME_GREY\", \"#bdc3c7\");\n  }\n\n  @Override\n  public String getName() {\n    return \"LyricModule\";\n  }\n\n//  @Override\n//  public Map<String, Object> getConstants() {\n//    return constants;\n//  }\n\n  @ReactMethod\n  public void addListener(String eventName) {\n    if (listenerCount == 0) {\n      // Set up any upstream listeners or background tasks as necessary\n    }\n\n    listenerCount += 1;\n  }\n\n  @ReactMethod\n  public void removeListeners(Integer count) {\n    listenerCount -= count;\n    if (listenerCount == 0) {\n      // Remove upstream listeners, stop unnecessary background tasks\n    }\n  }\n\n  @ReactMethod\n  public void showDesktopLyric(ReadableMap data, Promise promise) {\n    if (lyric == null) lyric = new Lyric(reactContext, isShowTranslation, isShowRoma, playbackRate);\n    lyric.showDesktopLyric(Arguments.toBundle(data), promise);\n  }\n\n  @ReactMethod\n  public void hideDesktopLyric(Promise promise) {\n    if (lyric != null) lyric.hideDesktopLyric();\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setSendLyricTextEvent(boolean isSend, Promise promise) {\n    if (lyric == null) lyric = new Lyric(reactContext, isShowTranslation, isShowRoma, playbackRate);\n    lyric.setSendLyricTextEvent(isSend);\n    promise.resolve(null);\n  }\n\n\n  @ReactMethod\n  public void setLyric(String lyric, String translation, String romaLyric, Promise promise) {\n    // Log.d(\"Lyric\", \"set lyric: \" + lyric);\n    // Log.d(\"Lyric\", \"set lyric translation: \" + translation);\n    if (this.lyric != null) this.lyric.setLyric(lyric, translation, romaLyric);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setPlaybackRate(float playbackRate, Promise promise) {\n    this.playbackRate = playbackRate;\n    if (lyric != null) lyric.setPlaybackRate(playbackRate);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void toggleTranslation(boolean isShowTranslation, Promise promise) {\n    this.isShowTranslation = isShowTranslation;\n    if (lyric != null) lyric.toggleTranslation(isShowTranslation);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void toggleRoma(boolean isShowRoma, Promise promise) {\n    this.isShowRoma = isShowRoma;\n    if (lyric != null) lyric.toggleRoma(isShowRoma);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void play(int time, Promise promise) {\n    Log.d(\"Lyric\", \"play lyric: \" + time);\n    if (lyric != null) lyric.play(time);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void pause(Promise promise) {\n    Log.d(\"Lyric\", \"play pause\");\n    if (lyric != null) lyric.pauseLyric();\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void toggleLock(boolean isLock, Promise promise) {\n    if (lyric != null) {\n      if (isLock) {\n        lyric.lockLyric();\n      } else {\n        lyric.unlockLyric();\n      }\n    }\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setColor(String unplayColor, String playedColor, String shadowColor, Promise promise) {\n    if (lyric != null) lyric.setPlayedColor(unplayColor, playedColor, shadowColor);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setAlpha(float alpha, Promise promise) {\n    if (lyric != null) lyric.setAlpha(alpha);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setTextSize(float size, Promise promise) {\n    if (lyric != null) lyric.setTextSize(size);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setMaxLineNum(int maxLineNum, Promise promise) {\n    if (lyric != null) lyric.setMaxLineNum(maxLineNum);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setSingleLine(boolean singleLine, Promise promise) {\n    if (lyric != null) lyric.setSingleLine(singleLine);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setShowToggleAnima(boolean showToggleAnima, Promise promise) {\n    if (lyric != null) lyric.setShowToggleAnima(showToggleAnima);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setWidth(int width, Promise promise) {\n    if (lyric != null) lyric.setWidth(width);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void setLyricTextPosition(String positionX, String positionY, Promise promise) {\n    if (lyric != null) lyric.setLyricTextPosition(positionX, positionY);\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void checkOverlayPermission(Promise promise) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(reactContext)) {\n      promise.reject(new Exception(\"Permission denied\"));\n    }\n    promise.resolve(null);\n  }\n\n  @ReactMethod\n  public void openOverlayPermissionActivity(Promise promise) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(reactContext)) {\n      Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse(\"package:\" + reactContext.getApplicationContext().getPackageName()));\n      reactContext.startActivityForResult(intent, 1, null);\n    }\n    promise.resolve(null);\n  }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricPackage.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.bridge.NativeModule;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.uimanager.ViewManager;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class LyricPackage implements ReactPackage {\n  @Override\n  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {\n    return Arrays.<NativeModule>asList(new LyricModule(reactContext));\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricPlayer.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Set;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\npublic class LyricPlayer {\n  final String timeFieldExp = \"^(?:\\\\[[\\\\d:.]+])+\";\n  final String timeExp = \"\\\\d{1,3}(:\\\\d{1,3}){0,2}(?:\\\\.\\\\d{1,3})\";\n//  HashMap tagRegMap;\n  Pattern timeFieldPattern;\n  Pattern timePattern;\n\n  String lyric = \"\";\n  ArrayList<String> extendedLyrics = new ArrayList<>();\n  List<HashMap> lines = new ArrayList<>();\n  HashMap tags = new HashMap();\n  boolean isPlay = false;\n  float playbackRate = 1;\n  int curLineNum = 0;\n  int maxLine = 0;\n  int offset = 150;\n  int performanceTime = 0;\n  int startPlayTime = 0;\n  // int delay = 0;\n  Object tid = null;\n  boolean tempPause = false;\n  boolean tempPaused = false;\n\n  LyricPlayer() {\n//    tagRegMap = new HashMap<String, String>();\n//    tagRegMap.put(\"title\", \"ti\");\n//    tagRegMap.put(\"artist\", \"ar\");\n//    tagRegMap.put(\"album\", \"al\");\n//    tagRegMap.put(\"offset\", \"offset\");\n//    tagRegMap.put(\"by\", \"by\");\n//    tags = new HashMap();\n\n    timeFieldPattern = Pattern.compile(timeFieldExp);\n    timePattern = Pattern.compile(timeExp);\n  }\n\n  public void setTempPause(boolean isPaused) {\n    if (isPaused) {\n      tempPause = true;\n    } else {\n      tempPause = false;\n      if (tempPaused) {\n        tempPaused = false;\n        if (isPlay) refresh();\n      }\n    }\n  }\n\n//  @RequiresApi(api = Build.VERSION_CODES.N)\n//  private void initTag() {\n//    tagRegMap.forEach((tag, value) -> {\n//      Pattern pattern = Pattern.compile(\"\\\\[\" + value + \":([^\\\\]]*)]\", Pattern.CASE_INSENSITIVE);\n//      Matcher matcher = pattern.matcher(lyric);\n//\n//      tags.put(tag, matcher.group(1));\n//    });\n//  }\n\n  private void startTimeout(Runnable runnable, long delay) {\n    if (tid != null) Utils.clearTimeout(tid);\n    tid = Utils.setTimeout(runnable, delay);\n  }\n\n  private void stopTimeout() {\n    if (tid == null) return;\n    Utils.clearTimeout(tid);\n    tid = null;\n  }\n\n  private int getNow() {\n    return (int)(System.nanoTime() / 1000000);\n  }\n\n  private int getCurrentTime() {\n    return (int)((getNow() - this.performanceTime) * this.playbackRate) + startPlayTime;\n  }\n\n  private void initTag() {\n    tags = new HashMap();\n    Matcher matcher = Pattern.compile(\"\\\\[(ti|ar|al|offset|by):\\\\s*(\\\\S+(?:\\\\s+\\\\S+)*)\\\\s*]\").matcher(this.lyric);\n    while (matcher.find()) {\n      String key = matcher.group(1);\n      if (key == null) continue;\n      String val = matcher.group(2);\n      if (val == null) val = \"\";\n      tags.put(key, val);\n    }\n\n    String offsetStr = (String) tags.get(\"offset\");\n    if (offsetStr == null || offsetStr.equals(\"\")) {\n      tags.put(\"offset\", 0);\n    } else {\n      int offset;\n      try {\n        offset = Integer.parseInt(offsetStr);\n      } catch (Exception err) {\n        offset = 0;\n      }\n      tags.put(\"offset\", offset);\n    }\n  }\n\n\n  final String t_rxp_1 = \"^0+(\\\\d+)\";\n  final String t_rxp_2 = \":0+(\\\\d+)\";\n  final String t_rxp_3 = \"\\\\.0+(\\\\d+)\";\n  private String formatTimeLabel(String label) {\n    return label.replaceAll(t_rxp_1, \"$1\")\n      .replaceAll(t_rxp_2, \":$1\")\n      .replaceAll(t_rxp_3, \".$1\");\n  }\n\n  private void parseExtendedLyric(HashMap linesMap, String extendedLyric) {\n    String[] extendedLyricLines = extendedLyric.split(\"\\r\\n|\\n|\\r\");\n    for (String translationLine : extendedLyricLines) {\n      String line = translationLine.trim();\n      Matcher timeFieldResult = timeFieldPattern.matcher(line);\n      if (timeFieldResult.find()) {\n        String timeField = timeFieldResult.group();\n        String text = line.replaceAll(timeFieldExp, \"\").trim();\n        if (text.length() > 0) {\n          Matcher timeMatchResult = timePattern.matcher(timeField);\n          while (timeMatchResult.find()) {\n            String timeStr = timeMatchResult.group();\n            timeStr = formatTimeLabel(timeStr);\n            HashMap targetLine = (HashMap) linesMap.get(timeStr);\n            if (targetLine != null) ((ArrayList<String>) targetLine.get(\"extendedLyrics\")).add(text);\n          }\n        }\n      }\n    }\n  }\n\n  private void initLines() {\n    String[] linesStr = lyric.split(\"\\r\\n|\\n|\\r\");\n    lines = new ArrayList<>();\n\n    HashMap linesMap = new HashMap<String, HashMap>();\n    HashMap timeMap = new HashMap<String, Integer>();\n\n    for (String lineStr : linesStr) {\n      String line = lineStr.trim();\n      Matcher timeFieldResult = timeFieldPattern.matcher(line);\n      if (timeFieldResult.find()) {\n        String timeField = timeFieldResult.group();\n        String text = line.replaceAll(timeFieldExp, \"\").trim();\n        if (text.length() > 0) {\n          Matcher timeMatchResult = timePattern.matcher(timeField);\n          while (timeMatchResult.find()) {\n            String timeStr = formatTimeLabel(timeMatchResult.group());\n            if (linesMap.containsKey(timeStr)) {\n              ((ArrayList<String>) ((HashMap) linesMap.get(timeStr)).get(\"extendedLyrics\")).add(text);\n              continue;\n            }\n            String[] timeArr = timeStr.split(\":\");\n            String hours;\n            String minutes;\n            String seconds;\n            String milliseconds = \"0\";\n            switch (timeArr.length) {\n              case 3:\n                hours = timeArr[0];\n                minutes = timeArr[1];\n                seconds = timeArr[2];\n                break;\n              case 2:\n                hours = \"0\";\n                minutes = timeArr[0];\n                seconds = timeArr[1];\n                break;\n              case 1:\n                hours = \"0\";\n                minutes = \"0\";\n                seconds = timeArr[0];\n                break;\n              default:\n                continue;\n            }\n            if (seconds.contains(\".\")) {\n              timeArr = seconds.split(\"\\\\.\");\n              seconds = timeArr[0];\n              if (timeArr.length > 1) milliseconds = timeArr[1];\n            }\n            HashMap<String, Object> lineInfo = new HashMap<>();\n            int time = Integer.parseInt(hours) * 60 * 60 * 1000\n              + Integer.parseInt(minutes) * 60 * 1000\n              + Integer.parseInt(seconds) * 1000\n              + Integer.parseInt(milliseconds);\n            lineInfo.put(\"time\", time);\n            lineInfo.put(\"text\", text);\n            lineInfo.put(\"extendedLyrics\", new ArrayList<String>(extendedLyrics.size()));\n            timeMap.put(timeStr, time);\n            linesMap.put(timeStr, lineInfo);\n          }\n        }\n      }\n    }\n\n    for (String extendedLyric : extendedLyrics) {\n      parseExtendedLyric(linesMap, extendedLyric);\n    }\n\n    Set<Entry<String, Integer>> set = timeMap.entrySet();\n    List<Entry<String, Integer>> list = new ArrayList<Entry<String, Integer>>(set);\n    Collections.sort(list, new Comparator<Entry<String, Integer>>() {\n      public int compare(Map.Entry<String, Integer> o1,\n                         Map.Entry<String, Integer> o2) {\n        return o1.getValue().compareTo(o2.getValue());\n      }\n    });\n\n    // lines = new ArrayList<HashMap>(list.size());\n    for (Entry<String, Integer> entry : list) {\n      lines.add((HashMap) linesMap.get(entry.getKey()));\n    }\n\n    this.maxLine = lines.size() - 1;\n  }\n\n  private void  init() {\n    if (lyric == null) lyric = \"\";\n    if (extendedLyrics == null) extendedLyrics = new ArrayList<>();\n    initTag();\n    initLines();\n    onSetLyric(lines);\n  }\n\n  public void pause() {\n    if (!isPlay) return;\n    isPlay = false;\n    tempPaused = false;\n    stopTimeout();\n    if (curLineNum == maxLine) return;\n    int curLineNum = this.findCurLineNum(getCurrentTime());\n    if (this.curLineNum != curLineNum) {\n      this.curLineNum = curLineNum;\n      this.onPlay(curLineNum);\n    }\n  }\n\n  public void play(int curTime) {\n    if (this.lines.size() == 0) return;\n    pause();\n    isPlay = true;\n\n    Object tagOffset = tags.get(\"offset\");\n    if (tagOffset == null) tagOffset = 0;\n    performanceTime = getNow() - (int) tagOffset - offset;\n    startPlayTime = curTime;\n\n    curLineNum = findCurLineNum(getCurrentTime()) - 1;\n\n    refresh();\n  }\n\n  private int findCurLineNum(int curTime, int startIndex) {\n    // Log.d(\"Lyric\", \"findCurLineNum: \" + startIndex);\n    if (curTime <= 0) return 0;\n    int length = lines.size();\n    for (int index = startIndex; index < length; index++) {\n      if (curTime < (int) ((HashMap)lines.get(index)).get(\"time\")) return index == 0 ? 0 : index - 1;\n    }\n    return length - 1;\n  }\n\n  private int findCurLineNum(int curTime) {\n    return findCurLineNum(curTime, 0);\n  }\n\n  private void handleMaxLine() {\n    this.onPlay(this.curLineNum);\n    this.pause();\n  }\n\n  private void refresh() {\n    if (tempPaused) tempPaused = false;\n\n    curLineNum++;\n    // Log.d(\"Lyric\", \"refresh: \" + curLineNum);\n\n    if (curLineNum >= maxLine) {\n      handleMaxLine();\n      return;\n    }\n    HashMap curLine = lines.get(curLineNum);\n\n    int currentTime = getCurrentTime();\n    int driftTime = currentTime - (int)curLine.get(\"time\");\n    // Log.d(\"Lyric\", \"driftTime: \" + driftTime + \"  time: \" + curLine.get(\"time\") + \"  currentTime: \" + currentTime);\n\n    if (driftTime >= 0 || curLineNum == 0) {\n      HashMap nextLine = lines.get(curLineNum + 1);\n      int delay = (int)(((int)nextLine.get(\"time\") - (int)curLine.get(\"time\") - driftTime) / this.playbackRate);\n      // Log.d(\"Lyric\", \"delay: \" + delay + \"  driftTime: \" + driftTime);\n      if (delay > 0) {\n        if (isPlay) {\n          startTimeout(() -> {\n            if (tempPause) {\n              tempPaused = true;\n              return;\n            }\n            if (!isPlay) return;\n            refresh();\n          }, delay);\n        }\n        onPlay(curLineNum);\n      } else {\n        int newCurLineNum = this.findCurLineNum(currentTime, curLineNum + 1);\n        if (newCurLineNum > curLineNum) curLineNum = newCurLineNum - 1;\n        // Log.d(\"Lyric\", \"refresh--: \" + curLineNum + \"  newCurLineNum: \" + newCurLineNum);\n        refresh();\n      }\n      return;\n    }\n\n    curLineNum = this.findCurLineNum(currentTime, curLineNum) - 1;\n    refresh();\n  }\n\n  public void setLyric(String lyric, ArrayList<String> extendedLyrics) {\n    if (isPlay) pause();\n    this.lyric = lyric;\n    this.extendedLyrics = extendedLyrics;\n    init();\n  }\n\n  public void setPlaybackRate(float playbackRate) {\n    this.playbackRate = playbackRate;\n    if (this.lines.size() == 0) return;\n    if (!this.isPlay) return;\n    this.play(this.getCurrentTime());\n  }\n\n  public void onPlay(int lineNum) {}\n\n  public void onSetLyric(List lines) {}\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricSwitchView.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.Typeface;\nimport android.text.TextPaint;\nimport android.text.TextUtils;\nimport android.view.View;\nimport android.view.animation.AlphaAnimation;\nimport android.view.animation.Animation;\nimport android.view.animation.AnimationSet;\nimport android.view.animation.TranslateAnimation;\nimport android.widget.TextSwitcher;\nimport android.widget.TextView;\n\nimport androidx.annotation.Nullable;\n\nimport java.util.ArrayList;\n\n// https://github.com/Block-Network/StatusBarLyric/blob/main/app/src/main/java/statusbar/lyric/view/LyricSwitchView.kt\n@SuppressLint({\"ViewConstructor\"})\npublic final class LyricSwitchView extends TextSwitcher {\n  private final TextView textView;\n  private final TextView textView2;\n  private final ArrayList<TextView> viewArray;\n  // private final boolean isSingleLine;\n  private boolean isShowAnima;\n\n  private boolean isSingleLine;\n\n  public LyricSwitchView(Context context, boolean isSingleLine, boolean isShowAnima) {\n    super(context);\n    // this.isSingleLine = isSingleLine;\n    this.isShowAnima = isShowAnima;\n    this.isSingleLine = isSingleLine;\n\n    if (isSingleLine) {\n      viewArray = new ArrayList<>(2);\n      textView = new LyricTextView(context);\n      textView2 = new LyricTextView(context);\n      viewArray.add(textView);\n      viewArray.add(textView2);\n//      for (TextView v : viewArray) {\n//        v.setShadowLayer(0.1f, 0, 0, Color.BLACK);\n//      }\n    } else {\n      viewArray = new ArrayList<>(2);\n      textView = new TextView(context);\n      textView2 = new TextView(context);\n      viewArray.add(textView);\n      viewArray.add(textView2);\n      for (TextView v : viewArray) {\n//        v.setShadowLayer(0.2f, 0, 0, Color.BLACK);\n        v.setEllipsize(TextUtils.TruncateAt.END);\n      }\n    }\n    setAnima();\n    this.addView(textView);\n    this.addView(textView2);\n  }\n\n  @Nullable\n  public Animation inAnim(String str, float height) {\n    AnimationSet animationSet = new AnimationSet(true);\n    if (str == null) return null;\n\n    TranslateAnimation translateAnimation;\n    switch (str) {\n      case \"top\":\n        translateAnimation = new TranslateAnimation(0.0F, 0.0F, height, 0.0F);\n        break;\n      case \"bottom\":\n        translateAnimation = new TranslateAnimation(0.0F, 0.0F, -height, 0.0F);\n        break;\n      case \"left\":\n        translateAnimation = new TranslateAnimation(100.0F, 0.0F, 0.0F, 0.0F);\n        break;\n      case \"right\":\n        translateAnimation = new TranslateAnimation(-100.0F, 0.0F, 0.0F, 0.0F);\n        break;\n      default: return null;\n    }\n\n    translateAnimation.setDuration(300L);\n    AlphaAnimation alphaAnimation = new AlphaAnimation(0.0F, 1.0F);\n    alphaAnimation.setDuration(300L);\n    animationSet.addAnimation(translateAnimation);\n    animationSet.addAnimation(alphaAnimation);\n    return animationSet;\n  }\n\n  @Nullable\n  public Animation outAnim(String str, float height) {\n    AnimationSet animationSet = new AnimationSet(true);\n    if (str == null) return null;\n\n    TranslateAnimation translateAnimation;\n    switch (str) {\n      case \"top\":\n        translateAnimation = new TranslateAnimation(0.0F, 0.0F, 0.0F, -height);\n        break;\n      case \"bottom\":\n        translateAnimation = new TranslateAnimation(0.0F, 0.0F, 0.0F, height);\n        break;\n      case \"left\":\n        translateAnimation = new TranslateAnimation(0.0F, -100.0F, 0.0F, 0.0F);\n        break;\n      case \"right\":\n        translateAnimation = new TranslateAnimation(0.0F, 100.0F, 0.0F, 0.0F);\n        break;\n      default: return null;\n    }\n    translateAnimation.setDuration(300L);\n    AlphaAnimation alphaAnimation = new AlphaAnimation(1.0F, 0.0F);\n    alphaAnimation.setDuration(300L);\n    animationSet.addAnimation(translateAnimation);\n    animationSet.addAnimation(alphaAnimation);\n    return animationSet;\n  }\n\n  private void setAnima() {\n    if (textView == null) return;\n    if (isShowAnima) {\n      float size = textView.getTextSize();\n      setInAnimation(inAnim(\"top\", size));\n      setOutAnimation(outAnim(\"top\", size));\n    } else {\n      setInAnimation(null);\n      setOutAnimation(null);\n    }\n  }\n\n  public void setShowAnima(boolean showAnima) {\n    isShowAnima = showAnima;\n    setAnima();\n  }\n\n  public CharSequence getText() {\n    View currentView = this.getCurrentView();\n    return currentView == null ? \"\" : ((TextView)currentView).getText();\n  }\n\n  public TextPaint getPaint() {\n    TextView v = (TextView)this.getCurrentView();\n    if (v == null) return null;\n    return v.getPaint();\n  }\n\n  public void setWidth(int i) {\n    for (TextView v : viewArray) v.setWidth(i);\n  }\n\n  public void setTextColor(int i) {\n    for (TextView v : viewArray) v.setTextColor(i);\n  }\n\n  public void setShadowColor(int i) {\n    // float radius;\n    // if (isSingleLine) {\n    //   radius = 1.2f;\n    // } else {\n    //   radius = 2f;\n    // }\n    // https://stackoverflow.com/a/28367917\n    for (TextView v : viewArray) v.setShadowLayer(1.6f, 1.5f, 1.3f, i);\n  }\n\n  public void setSourceText(CharSequence str) {\n    for (TextView v : viewArray) v.setText(str);\n  }\n\n  public void setLetterSpacings(float letterSpacing) {\n    for (TextView v : viewArray) v.setLetterSpacing(letterSpacing);\n  }\n\n  public void setHeight(int i) {\n    for (TextView v : viewArray) v.setHeight(i);\n  }\n\n  public void setTypeface(Typeface typeface) {\n    for (TextView v : viewArray) v.setTypeface(typeface);\n  }\n\n  public void setSingleLine(boolean bool) {\n    for (TextView v : viewArray) v.setSingleLine(bool);\n  }\n\n  public void setMaxLines(int i) {\n    for (TextView v : viewArray) v.setMaxLines(i);\n  }\n\n  public void setTextSize(float f) {\n    for (TextView v : viewArray) v.setTextSize(f);\n    setAnima();\n  }\n\n  public void setGravity(int i) {\n    for (TextView v : viewArray) v.setGravity(i);\n  }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricTextView.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Paint;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.widget.TextView;\n\n// https://github.com/Block-Network/StatusBarLyric/blob/main/app/src/main/java/statusbar/lyric/view/LyricTextView.kt\n@SuppressLint(\"AppCompatCustomView\")\npublic class LyricTextView extends TextView {\n  private boolean isStop = true;\n  private float textLength = 0F;\n  private float viewWidth = 0F;\n  private float viewHeight = 0F;\n  private final float SPEED_LIMIT = 0.135F;\n  private float speed;\n  private float xx = 0F;\n  private int gravityVertical = Gravity.TOP;\n  private int gravityHorizontal = Gravity.CENTER;\n  private float y = 0F;\n  private String text = null;\n  private final Paint mPaint;\n  private final Runnable mStartScrollRunnable;\n  private final Runnable invalidateRunnable;\n  public static final int startScrollDelay = 1500;\n  public static final int invalidateDelay = 10;\n\n  public LyricTextView(Context context) {\n    super(context);\n    mStartScrollRunnable = LyricTextView.this::startScroll;\n    invalidateRunnable = LyricTextView.this::invalidate;\n    mPaint = getPaint();\n    speed = SPEED_LIMIT * getTextSize();\n  }\n\n  private void init() {\n    xx = 0.0F;\n    textLength = getTextLength();\n    // viewWidth = (float) getWidth();\n  }\n\n  @Override\n  protected void onDetachedFromWindow() {\n    removeCallbacks(mStartScrollRunnable);\n    super.onDetachedFromWindow();\n  }\n\n  @Override\n  protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {\n    super.onTextChanged(text, start, lengthBefore, lengthAfter);\n    stopScroll();\n    this.text = text.toString();\n    init();\n    postInvalidate();\n    postDelayed(mStartScrollRunnable, startScrollDelay);\n  }\n\n  @Override\n  public void setTextColor(int color) {\n    if (mPaint != null) mPaint.setColor(color);\n    postInvalidate();\n  }\n\n  @Override\n  public void setShadowLayer(float radius, float dx, float dy, int shadowColor) {\n    if (mPaint != null) mPaint.setShadowLayer(radius, dx, dy, shadowColor);\n    post(mStartScrollRunnable);\n  }\n\n  @Override\n  public void setTextSize(float size) {\n    super.setTextSize(size);\n    speed = SPEED_LIMIT * size;\n    if (text == null) return;\n    post(mStartScrollRunnable);\n  }\n\n  @Override\n  public void setWidth(int pixels) {\n    super.setWidth(pixels);\n    viewWidth = pixels;\n    if (text == null) return;\n    post(mStartScrollRunnable);\n  }\n\n  @Override\n  public void setHeight(int pixels) {\n    super.setHeight(pixels);\n    viewHeight = pixels;\n    y = getDrawY();\n    if (text == null) return;\n    post(mStartScrollRunnable);\n  }\n\n  @Override\n  public void setGravity(int gravity) {\n    if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {\n      gravity |= Gravity.START;\n    }\n    if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {\n      gravity |= Gravity.TOP;\n    }\n\n    gravityVertical = gravity & Gravity.VERTICAL_GRAVITY_MASK;\n    gravityHorizontal = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;\n\n    y = getDrawY();\n    // Log.d(\"Lyric\", \"gravityVertical: \" + gravityVertical + \" gravityHorizontal: \" + gravityHorizontal);\n\n    if (text == null) return;\n    post(mStartScrollRunnable);\n  }\n\n  @Override\n  protected void onDraw(Canvas canvas) {\n    float mSpeed = speed;\n    if (text != null) {\n      Log.d(\"Lyric\", \"getHeight: \" + getHeight() + \" y: \" + y);\n      canvas.drawText(text, getDrawX(), y, mPaint);\n      if (getText().length() >= 20) {\n        mSpeed += mSpeed;\n      }\n    }\n\n    if (!isStop) {\n      if (viewWidth - xx + mSpeed >= textLength) {\n        xx = viewWidth - textLength - 2;\n        stopScroll();\n      } else {\n        xx -= mSpeed;\n      }\n\n      invalidateAfter();\n    }\n\n  }\n\n  private void invalidateAfter() {\n    removeCallbacks(invalidateRunnable);\n    postDelayed(invalidateRunnable, invalidateDelay);\n  }\n\n  private void startScroll() {\n    init();\n    isStop = false;\n    postInvalidate();\n  }\n\n  private void stopScroll() {\n    isStop = true;\n    removeCallbacks(mStartScrollRunnable);\n    postInvalidate();\n  }\n\n  private float getTextLength() {\n    return mPaint == null ? 0.0F : mPaint.measureText(text);\n  }\n\n  private float getDrawY() {\n    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();\n    float top = fontMetrics.top;\n    float bottom = fontMetrics.bottom;\n    float ascent = fontMetrics.ascent;\n    // float descent = fontMetrics.descent;\n\n    float y;\n\n    // float y = Math.abs(mPaint.ascent() + mPaint.descent()) / 2;\n    switch (gravityVertical) {\n      case Gravity.CENTER_VERTICAL:\n        y = viewHeight / 2F + (bottom - top) / 2 - bottom;\n        break;\n      case Gravity.BOTTOM:\n        y = viewHeight - bottom;\n        break;\n      default:\n        y = -ascent;\n        break;\n    }\n    return y;\n  }\n\n  private float getDrawX() {\n    float x;\n    if (textLength < viewWidth) {\n      switch (gravityHorizontal) {\n        case Gravity.CENTER_HORIZONTAL:\n          x = (viewWidth - textLength) / 2;\n          break;\n        case Gravity.END:\n          x = viewWidth - textLength;\n          break;\n        default:\n          x = 0;\n          break;\n      }\n      isStop = true;\n    } else {\n      x = xx;\n    }\n    return x;\n  }\n\n  // public void setSpeed(float speed) {\n  //  this.speed = speed;\n  // }\n\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/LyricView.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.graphics.PixelFormat;\nimport android.graphics.Point;\nimport android.hardware.SensorManager;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.util.DisplayMetrics;\nimport android.util.Log;\nimport android.view.Display;\nimport android.view.Gravity;\nimport android.view.MotionEvent;\nimport android.view.OrientationEventListener;\nimport android.view.View;\nimport android.view.WindowManager;\n\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.WritableMap;\n\nimport java.util.ArrayList;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport cn.toside.music.mobile.R;\n\npublic class LyricView extends Activity implements View.OnTouchListener {\n  LyricSwitchView textView = null;\n  WindowManager windowManager = null;\n  WindowManager.LayoutParams layoutParams = null;\n  final private ReactApplicationContext reactContext;\n  final private LyricEvent lyricEvent;\n\n  // private int winWidth = 0;\n\n  private float lastX; //上一次位置的X.Y坐标\n  private float lastY;\n  private float nowX;  //当前移动位置的X.Y坐标\n  private float nowY;\n  private float tranX; //悬浮窗移动位置的相对值\n  private float tranY;\n  private float prevViewPercentageX = 0;\n  private float prevViewPercentageY = 0;\n  private float widthPercentage = 1f;\n\n  private float preY = 0;\n  // private static boolean isVibrated = false;\n\n  private boolean isLock = false;\n  private boolean isSingleLine = false;\n  private boolean isShowToggleAnima = false;\n  private String unplayColor = \"rgba(255, 255, 255, 1)\";\n  private String playedColor = \"rgba(7, 197, 86, 1)\";\n  private String shadowColor = \"rgba(0, 0, 0, 0.15)\";\n  // private String lastText = \"LX Music ^-^\";\n  private String textX = \"LEFT\";\n  private String textY = \"TOP\";\n  private float alpha = 1f;\n  private float textSize = 18f;\n  private int maxWidth = 0;\n  private int maxHeight = 0;\n\n  private int maxLineNum = 5;\n  // private float lineHeight = 1;\n  private String currentLyric = \"LX Music ^-^\";\n  private ArrayList<String> currentExtendedLyrics = new ArrayList<>();\n\n  private int mLastRotation;\n  private OrientationEventListener orientationEventListener = null;\n\n  final Handler fixViewPositionHandler;\n  final Runnable fixViewPositionRunnable = this::updateViewPosition;\n\n  LyricView(ReactApplicationContext reactContext, LyricEvent lyricEvent) {\n    this.reactContext = reactContext;\n    this.lyricEvent = lyricEvent;\n    fixViewPositionHandler = new Handler();\n  }\n\n  private void listenOrientationEvent() {\n    if (orientationEventListener == null) {\n      orientationEventListener = new OrientationEventListener(reactContext, SensorManager.SENSOR_DELAY_NORMAL) {\n        @Override\n        public void onOrientationChanged(int orientation) {\n          Display display = windowManager.getDefaultDisplay();\n          int rotation = display.getRotation();\n          if(rotation != mLastRotation){\n            //rotation changed\n            // if (rotation == Surface.ROTATION_90){} // check rotations here\n            // if (rotation == Surface.ROTATION_270){} //\n            // Log.d(\"Lyric\", \"rotation: \" + rotation);\n            fixViewPositionHandler.postDelayed(fixViewPositionRunnable, 300);\n          }\n          mLastRotation = rotation;\n        }\n      };\n    }\n    // Log.d(\"Lyric\", \"orientationEventListener: \" + orientationEventListener.canDetectOrientation());\n    if (orientationEventListener.canDetectOrientation()) {\n      orientationEventListener.enable();\n    }\n  }\n  private void removeOrientationEvent() {\n    if (orientationEventListener == null) return;\n    orientationEventListener.disable();\n    // orientationEventListener = null;\n  }\n\n  private int getLayoutParamsFlags() {\n    int flag = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |\n      WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |\n      WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |\n      WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;\n\n    if (isLock) {\n      flag = flag | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;\n    }\n\n    return flag;\n  }\n\n  /**\n   * update screen width and height\n   * @return has updated\n   */\n  private boolean updateWH() {\n    Display display = windowManager.getDefaultDisplay();\n    Point size = new Point();\n    display.getRealSize(size);\n    if (maxWidth == size.x && maxHeight == size.y) return false;\n    maxWidth = size.x;\n    maxHeight = size.y;\n    return true;\n  }\n\n  private void setLayoutParamsHeight() {\n    if (textView == null) return;\n    int height = textView.getPaint().getFontMetricsInt(null) * maxLineNum;\n    if (height > maxHeight - 100) height = maxHeight - 100;\n    layoutParams.height = height;\n    textView.setHeight(height);\n  }\n\n  private void fixViewPosition() {\n    int maxX = maxWidth - layoutParams.width;\n    int x = (int)(maxWidth * prevViewPercentageX);\n    if (x < 0) x = 0;\n    else if (x > maxX) x = maxX;\n    if (layoutParams.x != x) layoutParams.x = x;\n\n    setLayoutParamsHeight();\n\n    int maxY = maxHeight - layoutParams.height;\n    int y = (int)(maxHeight * prevViewPercentageY);\n    if (y < 0) y = 0;\n    else if (y > maxY) y = maxY;\n    if (layoutParams.y != y) layoutParams.y = y;\n  }\n\n  private void updateViewPosition() {\n    if (!updateWH()) return;\n\n    int width = (int)(maxWidth * widthPercentage);\n    if (layoutParams.width != width) {\n      layoutParams.width = width;\n      if (textView != null) textView.setWidth(width);\n    }\n\n    fixViewPosition();\n    // Log.d(\"Lyric\", \"widthPercentage: \" + widthPercentage + \"  prevViewPercentageX: \" + prevViewPercentageX);\n    // Log.d(\"Lyric\", \"prevViewPercentageY: \" + prevViewPercentageY + \"  layoutParams.x: \" + layoutParams.x);\n    // Log.d(\"Lyric\", \"layoutParams.y: \" + layoutParams.y + \"  layoutParams.width: \" + layoutParams.width);\n\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void sendPositionEvent(float x, float y) {\n    WritableMap params = Arguments.createMap();\n    params.putDouble(\"x\", x);\n    params.putDouble(\"y\", y);\n    lyricEvent.sendEvent(lyricEvent.SET_VIEW_POSITION, params);\n  }\n\n//  public void permission(){\n//    if (Build.VERSION.SDK_INT >= 23) {\n//      if(!Settings.canDrawOverlays(this)) {\n//        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);\n//        startActivity(intent);\n//        return;\n//      } else {\n//        //Android6.0以上\n//        if (mFloatView!=null && mFloatView.isShow()==false) {\n//          mFloatView.show();\n//        }\n//      }\n//    } else {\n//      //Android6.0以下，不用动态声明权限\n//      if (mFloatView!=null && mFloatView.isShow()==false) {\n//        mFloatView.show();\n//      }\n//    }\n//  }\n// boolean isLock, String themeColor, float alpha, int lyricViewX, int lyricViewY, String textX, String textY\n  public void showLyricView(Bundle options) {\n    isLock = options.getBoolean(\"isLock\", isLock);\n    isSingleLine = options.getBoolean(\"isSingleLine\", isSingleLine);\n    isShowToggleAnima = options.getBoolean(\"isShowToggleAnima\", isShowToggleAnima);\n    unplayColor = options.getString(\"unplayColor\", unplayColor);\n    playedColor = options.getString(\"playedColor\", playedColor);\n    shadowColor = options.getString(\"shadowColor\", shadowColor);\n    prevViewPercentageX = (float) options.getDouble(\"lyricViewX\", 0f) / 100f;\n    prevViewPercentageY = (float) options.getDouble(\"lyricViewY\", 0f) / 100f;\n    textX = options.getString(\"textX\", textX);\n    textY = options.getString(\"textY\", textY);\n    alpha = (float) options.getDouble(\"alpha\", alpha);\n    textSize = (float) options.getDouble(\"textSize\", textSize);\n    widthPercentage = (float) options.getDouble(\"width\", 100) / 100f;\n    maxLineNum = (int) options.getDouble(\"maxLineNum\", maxLineNum);\n    handleShowLyric();\n    listenOrientationEvent();\n  }\n  public void showLyricView() {\n    try {\n      handleShowLyric();\n    } catch (Exception e) {\n      Log.e(\"Lyric\", e.getMessage());\n      return;\n    }\n    listenOrientationEvent();\n  }\n  public static int parseColor(String input) {\n    if (input.startsWith(\"#\")) return Color.parseColor(input);\n    Pattern c = Pattern.compile(\"rgba? *\\\\( *(\\\\d+), *(\\\\d+), *(\\\\d+)(?:, *([\\\\d.]+))? *\\\\)\");\n    Matcher m = c.matcher(input);\n    if (m.matches()) {\n      int red = Integer.parseInt(m.group(1));\n      int green = Integer.parseInt(m.group(2));\n      int blue = Integer.parseInt(m.group(3));\n      float a = 1;\n      if (m.group(4) != null) a = Float.parseFloat(m.group(4));\n      return Color.argb((int) (a * 255), red, green, blue);\n    }\n    return Color.parseColor(\"#000000\");\n  }\n\n  private void createTextView() {\n    textView = new LyricSwitchView(reactContext, isSingleLine, isShowToggleAnima);\n    textView.setText(\"\");\n    textView.setText(currentLyric);\n\n    textView.setTextColor(parseColor(playedColor));\n    textView.setShadowColor(parseColor(shadowColor));\n    textView.setAlpha(alpha);\n    textView.setTextSize(textSize);\n    // Log.d(\"Lyric\", \"alpha: \" + alpha + \" text size: \" + textSize);\n\n    //监听 OnTouch 事件 为了实现\"移动歌词\"功能\n    textView.setOnTouchListener(this);\n\n    int textPositionX;\n    int textPositionY;\n    switch (textX) {\n      case \"CENTER\":\n        textPositionX = Gravity.CENTER;\n        break;\n      case \"RIGHT\":\n        textPositionX = Gravity.END;\n        break;\n      case \"Left\":\n      default:\n        textPositionX = Gravity.START;\n        break;\n    }\n    switch (textY) {\n      case \"CENTER\":\n        textPositionY = Gravity.CENTER;\n        break;\n      case \"BOTTOM\":\n        textPositionY = Gravity.BOTTOM;\n        break;\n      case \"TOP\":\n      default:\n        textPositionY = Gravity.TOP;\n        break;\n    }\n    textView.setGravity(textPositionX | textPositionY);\n\n    if (!isSingleLine) {\n      textView.setMaxLines(maxLineNum);\n    }\n  }\n  private void handleShowLyric() {\n    if (windowManager == null) {\n      windowManager = (WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE);\n      //设置TextView的属性\n      layoutParams = new WindowManager.LayoutParams();\n\n      DisplayMetrics outMetrics = new DisplayMetrics();\n      windowManager.getDefaultDisplay().getMetrics(outMetrics);\n      // winWidth = (int)(outMetrics.widthPixels * 0.92);\n    }\n\n    // 注意，悬浮窗只有一个，而当打开应用的时候才会产生悬浮窗，所以要判断悬浮窗是否已经存在，\n    if (textView != null) {\n      windowManager.removeView(textView);\n    }\n\n    // 使用Application context\n    // 创建UI控件，避免Activity销毁导致上下文出现问题,因为现在的悬浮窗是系统级别的，不依赖与Activity存在\n    //创建自定义的TextView\n    createTextView();\n\n    // layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT | WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;\n    // layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;\n    layoutParams.type = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ?\n      WindowManager.LayoutParams.TYPE_SYSTEM_ALERT :\n      WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;\n\n    // layoutParams.flags = isLock\n    //  ? WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE\n    //  : WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;\n    layoutParams.flags = getLayoutParamsFlags();\n    if (isLock) {\n      textView.setBackgroundColor(Color.TRANSPARENT);\n\n      // 修复 Android 12 的穿透点击问题\n      if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {\n        layoutParams.alpha = 0.8f;\n      }\n    } else {\n      textView.setBackgroundResource(R.drawable.rounded_corner);\n\n      if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {\n        layoutParams.alpha = 1.0f;\n      }\n    }\n\n    // TYPE_SYSTEM_ALERT  系统提示,它总是出现在应用程序窗口之上\n    // TYPE_SYSTEM_OVERLAY   系统顶层窗口。显示在其他一切内容之上。此窗口不能获得输入焦点，否则影响锁屏\n    // FLAG_NOT_FOCUSABLE 悬浮窗口较小时，后面的应用图标由不可长按变为可长按,不设置这个flag的话，home页的划屏会有问题\n    // FLAG_NOT_TOUCH_MODAL不阻塞事件传递到后面的窗口\n    layoutParams.gravity = Gravity.TOP | Gravity.START;  //显示在屏幕上中部\n\n    updateWH();\n\n    //悬浮窗的宽高\n    // layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;\n    // layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;\n    // layoutParams.width= DisplayUtil.dp2px(mContext,55);\n    // layoutParams.height= DisplayUtil.dp2px(mContext,55);\n    layoutParams.width = (int)(maxWidth * widthPercentage);\n    textView.setWidth(layoutParams.width);\n    setLayoutParamsHeight();\n\n    //显示位置与指定位置的相对位置差\n    layoutParams.x = (int)(maxWidth * prevViewPercentageX);\n    layoutParams.y = (int)(maxHeight * prevViewPercentageY);\n\n    fixViewPosition();\n\n    //设置透明\n    layoutParams.format = PixelFormat.TRANSPARENT;\n\n    //添加到window中\n    windowManager.addView(textView, layoutParams);\n  }\n\n  public void setLyric(String text, ArrayList<String> extendedLyrics) {\n    if (text.equals(\"\") && text.equals(currentLyric) && extendedLyrics.size() == 0) return;\n    currentLyric = text;\n    currentExtendedLyrics = extendedLyrics;\n    if (textView == null) return;\n    if (extendedLyrics.size() > 0 && maxLineNum > 1 && !isSingleLine) {\n      int num = maxLineNum - 1;\n      StringBuilder textBuilder = new StringBuilder(text);\n      for (String lrc : extendedLyrics) {\n        textBuilder.append(\"\\n\").append(lrc);\n        if (--num < 1) break;\n      }\n      text = textBuilder.toString();\n    }\n    if (textView == null) return;\n    textView.setText(text);\n  }\n\n  public void setMaxLineNum(int maxLineNum) {\n    this.maxLineNum = maxLineNum;\n    if (textView == null) return;\n    if (!isSingleLine) textView.setMaxLines(maxLineNum);\n    setLayoutParamsHeight();\n\n    int maxY = maxHeight - layoutParams.height;\n    int y = layoutParams.y;\n    if (y < 0) y = 0;\n    else if (y > maxY) y = maxY;\n    if (layoutParams.y != y) layoutParams.y = y;\n\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void setWidth(int width) {\n    if (textView == null) return;\n    widthPercentage = width / 100f;\n    layoutParams.width = (int)(maxWidth * widthPercentage);\n    textView.setWidth(layoutParams.width);\n\n    int maxX = maxWidth - layoutParams.width;\n    int x = layoutParams.x;\n    if (x < 0) x = 0;\n    else if (x > maxX) x = maxX;\n    if (layoutParams.x != x) layoutParams.x = x;\n\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  @Override\n  public boolean onTouch(View v, MotionEvent event) {\n    int maxX = maxWidth - layoutParams.width;\n    int maxY = maxHeight - layoutParams.height;\n\n    switch (event.getAction()){\n      case MotionEvent.ACTION_DOWN:\n        // 获取按下时的X，Y坐标\n        lastX = event.getRawX();\n        lastY = event.getRawY();\n\n        preY = lastY;\n        break;\n      case MotionEvent.ACTION_MOVE:\n        // 获取移动时的X，Y坐标\n        nowX = event.getRawX();\n        nowY = event.getRawY();\n        if (preY == 0){\n          preY = nowY;\n        }\n        // 计算XY坐标偏移量\n        tranX = nowX - lastX;\n        tranY = nowY - lastY;\n\n        int x = layoutParams.x + (int)tranX;\n        if (x < 0) x = 0;\n        else if (x > maxX) x = maxX;\n        int y = layoutParams.y + (int)tranY;\n        if (y < 0) y = 0;\n        else if (y > maxY) y = maxY;\n\n        // 移动悬浮窗\n        layoutParams.x = x;\n        layoutParams.y = y;\n        //更新悬浮窗位置\n        windowManager.updateViewLayout(textView, layoutParams);\n        //记录当前坐标作为下一次计算的上一次移动的位置坐标\n        lastX = nowX;\n        lastY = nowY;\n        break;\n      case MotionEvent.ACTION_UP:\n        // float dy = nowY - preY;\n        // Log.d(\"Lyric\",\"dy: \" + dy);\n        // if (isVibrated){\n        //   if (dy > 10){\n        //     //down\n        //     actions(AppHolder.actions[3]);\n        //   }else if (dy<-10){\n        //     //up\n        //     actions(AppHolder.actions[4]);\n        //   }else {\n        //     //longClick\n        //     actions(AppHolder.actions[2]);\n        //   }\n        //   isVibrated =false;\n        // }\n        //根据移动的位置来判断\n        // dy = 0;\n        tranY = 0;\n        float percentageX = (float)layoutParams.x / (float) maxWidth * 100f;\n        float percentageY = (float)layoutParams.y / (float) maxHeight * 100f;\n        if (percentageX != prevViewPercentageX || percentageY != prevViewPercentageY) {\n          prevViewPercentageX = percentageX / 100f;\n          prevViewPercentageY = percentageY / 100f;\n          sendPositionEvent(percentageX, percentageY);\n        }\n        break;\n    }\n    return true;\n  }\n\n  public void lockView() {\n    isLock = true;\n    if (windowManager == null || textView == null) return;\n    layoutParams.flags = getLayoutParamsFlags();\n\n    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {\n      layoutParams.alpha = 0.8f;\n    }\n    textView.setBackgroundColor(Color.TRANSPARENT);\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void unlockView() {\n    isLock = false;\n    if (windowManager == null || textView == null) return;\n    layoutParams.flags = getLayoutParamsFlags();\n\n    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {\n      layoutParams.alpha = 1.0f;\n    }\n    textView.setBackgroundResource(R.drawable.rounded_corner);\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void setColor(String unplayColor, String playedColor, String shadowColor) {\n    this.unplayColor = unplayColor;\n    this.playedColor = playedColor;\n    this.shadowColor = shadowColor;\n    if (textView == null) return;\n    textView.setTextColor(parseColor(playedColor));\n    textView.setShadowColor(parseColor(shadowColor));\n    // windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void setLyricTextPosition(String textX, String textY) {\n    this.textX = textX;\n    this.textY = textY;\n    if (windowManager == null || textView == null) return;\n    int textPositionX;\n    int textPositionY;\n    // Log.d(\"Lyric\", \"textX: \" + textX + \"  textY: \" + textY);\n    switch (textX) {\n      case \"CENTER\":\n        textPositionX = Gravity.CENTER_HORIZONTAL;\n        break;\n      case \"RIGHT\":\n        textPositionX = Gravity.END;\n        break;\n      case \"LEFT\":\n      default:\n        textPositionX = Gravity.START;\n        break;\n    }\n    switch (textY) {\n      case \"CENTER\":\n        textPositionY = Gravity.CENTER_VERTICAL;\n        break;\n      case \"BOTTOM\":\n        textPositionY = Gravity.BOTTOM;\n        break;\n      case \"TOP\":\n      default:\n        textPositionY = Gravity.TOP;\n        break;\n    }\n    textView.setGravity(textPositionX | textPositionY);\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void setAlpha(float alpha) {\n    this.alpha = alpha;\n    if (textView == null) return;\n    textView.setAlpha(alpha);\n  }\n\n  public void setSingleLine(boolean isSingleLine) {\n    this.isSingleLine = isSingleLine;\n    if (textView == null) return;\n    windowManager.removeView(textView);\n    createTextView();\n    textView.setWidth(layoutParams.width);\n    textView.setHeight(layoutParams.height);\n    windowManager.addView(textView, layoutParams);\n\n    if (isLock) lockView();\n    else unlockView();\n\n    setLyric(currentLyric, currentExtendedLyrics);\n  }\n\n  public void setShowToggleAnima(boolean showToggleAnima) {\n    isShowToggleAnima = showToggleAnima;\n    if (textView == null) return;\n    textView.setShowAnima(showToggleAnima);\n  }\n\n  public void setTextSize(float size) {\n    this.textSize = size;\n    if (windowManager == null || textView == null) return;\n    textView.setTextSize(size);\n    setLayoutParamsHeight();\n    windowManager.updateViewLayout(textView, layoutParams);\n  }\n\n  public void destroyView() {\n    if (textView == null || windowManager == null) return;\n    windowManager.removeView(textView);\n    textView = null;\n    removeOrientationEvent();\n  }\n\n  public void destroy() {\n    destroyView();\n    windowManager = null;\n    layoutParams = null;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/lyric/Utils.java",
    "content": "package cn.toside.music.mobile.lyric;\n\nimport android.os.Handler;\n\npublic class Utils {\n  // https://gist.github.com/mathew-kurian/2bd2b8b3a2f6438d6786\n  public static Object setTimeout(Runnable runnable, long delay) {\n    return new TimeoutEvent(runnable, delay);\n  }\n  public static void clearTimeout(Object timeoutEvent) {\n    if (timeoutEvent instanceof TimeoutEvent) {\n      ((TimeoutEvent) timeoutEvent).cancelTimeout();\n    }\n  }\n  private static class TimeoutEvent {\n    private static final Handler handler = new Handler();\n    private volatile Runnable runnable;\n\n    private TimeoutEvent(Runnable task, long delay) {\n      runnable = task;\n      handler.postDelayed(() -> {\n        if (runnable != null) {\n          runnable.run();\n        }\n      }, delay);\n    }\n\n    private void cancelTimeout() {\n      runnable = null;\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/Console.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.os.Handler;\nimport android.os.Message;\nimport android.util.Log;\n\nimport com.whl.quickjs.wrapper.QuickJSContext;\n\npublic class Console implements QuickJSContext.Console {\n  private final Handler eventHandler;\n\n  Console(Handler eventHandler) {\n    this.eventHandler = eventHandler;\n  }\n\n  private void sendLog(String type, String log) {\n    Message message = this.eventHandler.obtainMessage();\n    message.what = HandlerWhat.LOG;\n    message.obj = new Object[]{type, log};\n    Log.d(\"UserApi Log\", \"[\" + type + \"]\" + log);\n    this.eventHandler.sendMessage(message);\n  }\n\n  @Override\n  public void log(String info) {\n    sendLog(\"log\", info);\n  }\n\n  @Override\n  public void info(String info) {\n    sendLog(\"info\", info);\n  }\n\n  @Override\n  public void warn(String info) {\n    sendLog(\"warn\", info);\n  }\n\n  @Override\n  public void error(String info) {\n    sendLog(\"error\", info);\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/HandlerWhat.java",
    "content": "package cn.toside.music.mobile.userApi;\n\npublic class HandlerWhat {\n  public static final int ACTION = 1000;\n  public static final int INIT = 99;\n  public static final int INIT_FAILED = 500;\n  public static final int INIT_SUCCESS = 200;\n  public static final int DESTROY = 98;\n  public static final int LOG = 1001;\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/JavaScriptThread.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.HandlerThread;\nimport android.os.Message;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport com.facebook.react.bridge.ReactApplicationContext;\n\npublic class JavaScriptThread extends HandlerThread {\n  private Handler handler;\n  private QuickJS javaScriptExecutor;\n  private final ReactApplicationContext reactContext;\n  private final Bundle scriptInfo;\n\n  JavaScriptThread(ReactApplicationContext context, Bundle info) {\n    super(\"JavaScriptThread\");\n    this.reactContext = context;\n    this.scriptInfo = info;\n  }\n\n  public void prepareHandler(final Handler mainHandler) {\n    start();\n    Log.d(\"UserApi [thread]\", \"running 2\");\n    this.handler = new Handler(getLooper()) {\n      @Override\n      public void handleMessage(@NonNull Message message) {\n        if (javaScriptExecutor == null) {\n          javaScriptExecutor = new QuickJS(reactContext, mainHandler);\n          Log.d(\"UserApi [thread]\", \"javaScript executor created\");\n          String result = javaScriptExecutor.loadScript(scriptInfo);\n          if (\"\".equals(result)) {\n            Log.d(\"UserApi [thread]\", \"script loaded\");\n            mainHandler.sendEmptyMessage(HandlerWhat.INIT_SUCCESS);\n          } else {\n            Log.w(\"UserApi [thread]\", \"script load failed: \" + result);\n            mainHandler.sendMessage(mainHandler.obtainMessage(HandlerWhat.INIT_FAILED, result));\n          }\n        }\n        switch (message.what) {\n          case HandlerWhat.INIT: break;\n          case HandlerWhat.ACTION: {\n            Object[] data = (Object[]) message.obj;\n            // Log.d(\"UserApi [handler]\", \"handler action: \" + data[0]);\n            javaScriptExecutor.callJS((String) data[0], data[1]);\n            return;\n          }\n          case HandlerWhat.DESTROY:\n            javaScriptExecutor.destroy();\n            javaScriptExecutor = null;\n            break;\n          default:\n            Log.w(\"UserApi [handler]\", \"Unknown message what: \" + message.what);\n            break;\n        }\n      }\n    };\n  }\n\n  public Handler getHandler() {\n    return this.handler;\n  }\n\n  public void stopThread() {\n    quit();\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/JsHandler.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.os.Message;\nimport android.util.Log;\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.WritableMap;\n\nimport java.util.Objects;\n\npublic class JsHandler extends Handler {\n  private final UtilsEvent utilsEvent;\n\n  JsHandler(Looper looper, UtilsEvent utilsEvent) {\n    super(looper);\n    this.utilsEvent = utilsEvent;\n  }\n\n  private void sendInitFailedEvent(String errorMessage) {\n    WritableMap params = Arguments.createMap();\n    params.putString(\"action\", \"init\");\n    params.putString(\"errorMessage\", errorMessage);\n    params.putString(\"data\", \"{ \\\"info\\\": null, \\\"status\\\": false, \\\"errorMessage\\\": \\\"Create JavaScript Env Failed\\\" }\");\n    this.utilsEvent.sendEvent(utilsEvent.API_ACTION, params);\n    sendLogEvent(new Object[]{\"error\", errorMessage});\n  }\n\n  private void sendLogEvent(Object[] data) {\n    WritableMap params = Arguments.createMap();\n    params.putString(\"action\", \"log\");\n    params.putString(\"type\", (String) data[0]);\n    params.putString(\"log\", (String) data[1]);\n    this.utilsEvent.sendEvent(utilsEvent.API_ACTION, params);\n  }\n\n  private void sendActionEvent(String action, String data) {\n    WritableMap params = Arguments.createMap();\n    params.putString(\"action\", action);\n    params.putString(\"data\", data);\n    this.utilsEvent.sendEvent(utilsEvent.API_ACTION, params);\n  }\n\n  @Override\n  public void handleMessage(Message msg) {\n    switch (msg.what) {\n      case HandlerWhat.INIT_SUCCESS: break;\n      case HandlerWhat.INIT_FAILED:\n        sendInitFailedEvent((String) msg.obj);\n        break;\n      case HandlerWhat.ACTION:\n        Object[] action = (Object[]) msg.obj;\n        sendActionEvent((String) action[0], (String) action[1]);\n        // Log.d(\"UserApi [api call]\", \"action: \" + action[0]);\n        break;\n      case HandlerWhat.LOG:\n        sendLogEvent((Object[]) msg.obj);\n        break;\n      default:\n        Log.w(\"UserApi [api call]\", \"Unknown message what: \" + msg.what);\n        break;\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/QuickJS.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.util.Base64;\nimport android.util.Log;\nimport cn.toside.music.mobile.crypto.AES;\nimport cn.toside.music.mobile.crypto.RSA;\nimport com.facebook.react.bridge.ReactApplicationContext;\n\nimport com.whl.quickjs.android.QuickJSLoader;\nimport com.whl.quickjs.wrapper.QuickJSContext;\nimport java.io.InputStream;\nimport java.net.URLDecoder;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.util.UUID;\n\npublic class QuickJS {\n  private final Handler eventHandler;\n  private String key;\n  private final ReactApplicationContext reactContext;\n  private boolean isInited = false;\n  private QuickJSContext jsContext = null;\n  final Handler timeoutHandler = new Handler();\n  private boolean inited = false;\n\n  public QuickJS(ReactApplicationContext context, Handler eventHandler) {\n    this.reactContext = context;\n    this.eventHandler = eventHandler;\n  }\n\n  private void init() {\n    if (this.isInited) return;\n    QuickJSLoader.init();\n    this.key = UUID.randomUUID().toString();\n    this.isInited = true;\n  }\n\n  private String getPreloadScript() {\n    try {\n      InputStream inputStream = this.reactContext.getAssets().open(\"script/user-api-preload.js\");\n      byte[] buffer = new byte[inputStream.available()];\n      inputStream.read(buffer);\n      inputStream.close();\n      return new String(buffer, StandardCharsets.UTF_8);\n    } catch (Exception e) {\n      return null;\n    }\n  }\n\n  private void createEnvObj(QuickJSContext jsContext) {\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__\", args -> {\n      if (this.key.equals(args[0])) {\n        callNative((String) args[1], (String) args[2]);\n        return null;\n      }\n      return null;\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__utils_str2b64\", args -> {\n      try {\n        return new String(Base64.encode(((String) args[0]).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP));\n      } catch (Exception e) {\n        Log.e(\"UserApi [utils]\", \"utils_str2b64 error: \" + e.getMessage());\n        return \"\";\n      }\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__utils_b642buf\", args -> {\n      try {\n        byte[] byteArray = Base64.decode(((String) args[0]).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);\n        StringBuilder jsonArrayString = new StringBuilder(\"[\");\n        for (int i = 0; i < byteArray.length; i++) {\n          jsonArrayString.append((int) byteArray[i]);\n          if (i < byteArray.length - 1) {\n            jsonArrayString.append(\",\");\n          }\n        }\n        jsonArrayString.append(\"]\");\n        return jsonArrayString.toString();\n      } catch (Exception e) {\n        Log.e(\"UserApi [utils]\", \"utils_b642buf error: \" + e.getMessage());\n        return \"\";\n      }\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__utils_str2md5\", args -> {\n      try {\n        // Log.d(\"UserApi [script call]\", \"utils_str2md5: \" + args[0]);\n        String str;\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n          str = URLDecoder.decode((String) args[0], StandardCharsets.UTF_8);\n        } else {\n          str = URLDecoder.decode((String) args[0], \"UTF-8\");\n        }\n        // Log.d(\"UserApi [script call]\", \"utils_str2md5: \" + str);\n        MessageDigest md = MessageDigest.getInstance(\"MD5\");\n        byte[] md5Bytes = md.digest(str.getBytes(StandardCharsets.UTF_8));\n        StringBuilder md5String = new StringBuilder();\n        for (byte b : md5Bytes) {\n          md5String.append(String.format(\"%02x\", b));\n        }\n        return md5String.toString();\n      } catch (Exception e) {\n        Log.e(\"UserApi [utils]\", \"utils_str2md5 error: \" + e.getMessage());\n        return \"\";\n      }\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__utils_aes_encrypt\", args -> {\n      try {\n        return AES.encrypt((String) args[0], (String) args[1], (String) args[2], (String) args[3]);\n      } catch (Exception e) {\n        Log.e(\"UserApi [utils]\", \"utils_aes_encrypt error: \" + e.getMessage());\n        return \"\";\n      }\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__utils_rsa_encrypt\", args -> {\n      try {\n        return RSA.encryptRSAToString((String) args[0], (String) args[1], (String) args[2]);\n      } catch (Exception e) {\n        Log.e(\"UserApi [utils]\", \"utils_rsa_encrypt error: \" + e.getMessage());\n        return \"\";\n      }\n    });\n    jsContext.getGlobalObject().setProperty(\"__lx_native_call__set_timeout\", args -> {\n      this.timeoutHandler.postDelayed(() -> {\n        callJS(\"__set_timeout__\", args[0]);\n      }, (int) args[1]);\n      return null;\n    });\n  }\n\n  private boolean createJSEnv(String id, String name, String desc, String version, String author, String homepage, String rawScript) {\n    init();\n    QuickJSContext quickJSContext = this.jsContext;\n    if (quickJSContext != null) quickJSContext.destroy();\n    this.jsContext = QuickJSContext.create();\n    this.jsContext.setConsole(new Console(this.eventHandler));\n    String preloadScript = getPreloadScript();\n    if (preloadScript == null) return false;\n    createEnvObj(this.jsContext);\n    this.jsContext.evaluate(preloadScript);\n    this.jsContext.getGlobalObject().getJSFunction(\"lx_setup\").call(this.key, id, name, desc, version, author, homepage, rawScript);\n    return true;\n  }\n\n  private void callNative(String action, String data) {\n    Message message = this.eventHandler.obtainMessage();\n    message.what = HandlerWhat.ACTION;\n    if (\"init\".equals(action)) {\n      if (inited) return;\n      inited = true;\n    }\n    message.obj = new Object[]{action, data};\n    Log.d(\"UserApi [script call]\", \"script call action: \" + action + \" data: \" + data);\n    this.eventHandler.sendMessage(message);\n  }\n\n  public String loadScript(Bundle scriptInfo) {\n    Log.d(\"UserApi\", \"UserApi Thread id: \" + Thread.currentThread().getId());\n    String script = scriptInfo.getString(\"script\", \"\");\n    if (createJSEnv(scriptInfo.getString(\"id\", \"\"),\n      scriptInfo.getString(\"name\", \"Unknown\"),\n      scriptInfo.getString(\"description\", \"\"),\n      scriptInfo.getString(\"version\", \"\"),\n      scriptInfo.getString(\"author\", \"\"),\n      scriptInfo.getString(\"homepage\", \"\"),\n      script)) {\n      try {\n        this.jsContext.evaluate(script);\n        return \"\";\n      } catch (Exception e) {\n        Log.e(\"UserApi\", \"load script error: \" + e.getMessage());\n        try {\n          callJS(\"__run_error__\");\n        } catch (Exception ignored) {}\n        if (inited) return \"\";\n        inited = true;\n        return e.getMessage();\n      }\n    }\n    return \"create JavaScript Env failed\";\n  }\n\n  public Object callJS(String action) {\n    Object[] params = new Object[]{this.key, action};\n    return callJS(params);\n  }\n  public Object callJS(String action, Object... args) {\n    Object[] params = new Object[args.length + 2];\n    params[0] = this.key;\n    params[1] = action;\n    System.arraycopy(args, 0, params, 2, args.length);\n    return callJS(params);\n  }\n  public Object callJS(Object[] params) {\n    try {\n      return this.jsContext.getGlobalObject().getJSFunction(\"__lx_native__\").call(params);\n    } catch (Exception e) {\n      Message message = eventHandler.obtainMessage();\n      message.what = HandlerWhat.LOG;\n      String msg = e.getMessage();\n      if (msg == null) return null;\n      if (msg.length() > 1024) msg = msg.substring(0, 1024) + \"...\";\n      message.obj = new Object[]{\"error\", \"Call script error: \" + msg};\n      eventHandler.sendMessage(message);\n      Log.e(\"UserApi\", \"Call script error: \" + e.getMessage());\n      if (!this.inited) {\n        eventHandler.sendMessage(eventHandler.obtainMessage(HandlerWhat.INIT_FAILED, msg));\n        this.inited = true;\n      }\n      return null;\n    }\n  }\n\n  public void destroy () {\n    this.jsContext.destroy();\n    this.jsContext = null;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/UserApiModule.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.util.Log;\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactContextBaseJavaModule;\nimport com.facebook.react.bridge.ReactMethod;\nimport com.facebook.react.bridge.ReadableMap;\nimport java.lang.Thread;\n\npublic class UserApiModule extends ReactContextBaseJavaModule {\n  private JavaScriptThread javaScriptThread;\n  private final ReactApplicationContext reactContext;\n  private UtilsEvent utilsEvent;\n\n  private int listenerCount = 0;\n\n  UserApiModule(ReactApplicationContext reactContext) {\n    super(reactContext);\n    this.javaScriptThread = null;\n    this.utilsEvent = null;\n    this.reactContext = reactContext;\n  }\n\n  @Override\n  public String getName() {\n    return \"UserApiModule\";\n  }\n\n  @ReactMethod\n  public void addListener(String eventName) {\n    if (listenerCount == 0) {\n      // Set up any upstream listeners or background tasks as necessary\n    }\n\n    listenerCount += 1;\n  }\n\n  @ReactMethod\n  public void removeListeners(Integer count) {\n    listenerCount -= count;\n    if (listenerCount == 0) {\n      // Remove upstream listeners, stop unnecessary background tasks\n    }\n  }\n\n  @ReactMethod\n  public void loadScript(ReadableMap data) {\n    if (this.utilsEvent == null) this.utilsEvent = new UtilsEvent(this.reactContext);\n    if (this.javaScriptThread != null) destroy();\n    Bundle info = Arguments.toBundle(data);\n    this.javaScriptThread = new JavaScriptThread(this.reactContext, info);\n    this.javaScriptThread.prepareHandler(new JsHandler(this.reactContext.getMainLooper(), this.utilsEvent));\n    this.javaScriptThread.getHandler().sendEmptyMessage(HandlerWhat.INIT);\n    this.javaScriptThread.setUncaughtExceptionHandler((thread, ex) -> {\n      Handler jsHandler = javaScriptThread.getHandler();\n      Message message = jsHandler.obtainMessage();\n      message.what = HandlerWhat.LOG;\n      message.obj = new Object[]{\"error\", \"Uncaught exception in JavaScriptThread: \" + ex.getMessage()};\n      jsHandler.sendMessage(message);\n      Log.e(\"JavaScriptThread\", \"Uncaught exception in JavaScriptThread: \" + ex.getMessage());\n    });\n    Log.d(\"UserApi\", \"Module Thread id: \" + Thread.currentThread().getId());\n  }\n\n  @ReactMethod\n  public boolean sendAction(String action, String info) {\n    JavaScriptThread javaScriptThread = this.javaScriptThread;\n    if (javaScriptThread == null) return false;\n    Handler jsHandler = javaScriptThread.getHandler();\n    Message message = jsHandler.obtainMessage();\n    message.what = HandlerWhat.ACTION;\n    message.obj = new Object[]{action, info};\n    jsHandler.sendMessage(message);\n    return true;\n  }\n\n  @ReactMethod\n  public void destroy() {\n    JavaScriptThread javaScriptThread = this.javaScriptThread;\n    if (javaScriptThread == null) return;\n    javaScriptThread.getHandler().sendEmptyMessage(HandlerWhat.DESTROY);\n    javaScriptThread.stopThread();\n    this.javaScriptThread = null;\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/UserApiPackage.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.bridge.NativeModule;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.uimanager.ViewManager;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class UserApiPackage implements ReactPackage {\n  @Override\n  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {\n    return Arrays.asList(new UserApiModule(reactContext));\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/userApi/UtilsEvent.java",
    "content": "package cn.toside.music.mobile.userApi;\n\nimport android.util.Log;\n\nimport androidx.annotation.Nullable;\n\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.WritableMap;\nimport com.facebook.react.modules.core.DeviceEventManagerModule;\n\npublic class UtilsEvent {\n  final String API_ACTION = \"api-action\";\n  private final ReactApplicationContext reactContext;\n\n  UtilsEvent(ReactApplicationContext reactContext) {\n    this.reactContext = reactContext;\n  }\n\n  public void sendEvent(String eventName, @Nullable WritableMap params) {\n    reactContext\n      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)\n      .emit(eventName, params);\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/AsyncTask.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.util.Log;\n\nimport com.facebook.react.bridge.Promise;\n\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\n\npublic class AsyncTask {\n  // https://stackoverflow.com/a/58767934\n  private static class TaskRunner {\n    private final ExecutorService executor = Executors.newSingleThreadExecutor(); // change according to your requirements\n    private final Handler handler = new Handler(Looper.getMainLooper());\n\n    public interface Callback<Object> {\n      void onComplete(Object result);\n    }\n\n    public <Object> void executeAsync(Callable<Object> callable, Callback<Object> callback) {\n      executor.execute(() -> {\n        try {\n          Object result = callable.call();\n          handler.post(() -> callback.onComplete(result));\n        } catch (Exception e) {\n          handler.post(() -> callback.onComplete((Object) e));\n          Log.e(\"TaskRunner\", \"execute error: \" + e.getMessage());\n        }\n      });\n    }\n    public void shutdown() {\n      executor.shutdown();\n    }\n  }\n\n  public static void runTask(Callable<Object> callable, Promise promise) {\n    TaskRunner taskRunner = new TaskRunner();\n    try {\n      taskRunner.executeAsync(callable, (Object result) -> {\n        taskRunner.shutdown();\n        if (result instanceof Exception) {\n          promise.reject(\"-1\", ((Exception) result).getMessage());\n        } else promise.resolve(result);\n      });\n    } catch (Exception err) {\n      promise.reject(\"-1\", err.getMessage());\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/BatteryOptimizationUtil.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.PowerManager;\nimport android.provider.Settings;\nimport android.util.Log;\n\nimport java.util.List;\n\npublic class BatteryOptimizationUtil {\n\n  public static boolean isIgnoringBatteryOptimization(Context context, String packageName) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {\n      PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);\n      if (pm != null) {\n        return pm.isIgnoringBatteryOptimizations(packageName);\n      }\n      return false;\n    }\n    return true; // Android 6.0 以下不需要\n  }\n\n  /**\n   * 请求忽略电池优化\n   * @return true: 已忽略或成功打开系统页面；false: 系统不支持或调用失败\n   */\n  public static boolean requestIgnoreBatteryOptimization(Context context, String packageName) {\n    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n      return true;\n    }\n\n    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);\n    if (pm != null && pm.isIgnoringBatteryOptimizations(packageName)) {\n      return true;\n    }\n\n    @SuppressLint(\"BatteryLife\")\n    Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);\n    intent.setData(Uri.parse(\"package:\" + packageName));\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n\n    PackageManager pmgr = context.getPackageManager();\n    List<ResolveInfo> resolveInfos = pmgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n    if (resolveInfos.isEmpty()) {\n      Log.w(\"BatteryOptimizationUtil\", \"No Activity found to handle ignore battery optimization intent\");\n      return false;\n    }\n\n    try {\n      context.startActivity(intent);\n      return true;\n    } catch (Exception e) {\n      Log.e(\"BatteryOptimizationUtil\", \"Failed to start ignore battery optimization intent\", e);\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/NotificationPermissionUtil.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport android.app.NotificationChannel;\nimport android.app.NotificationManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.content.pm.ResolveInfo;\nimport android.os.Build;\nimport android.provider.Settings;\nimport android.util.Log;\n\nimport androidx.core.app.NotificationManagerCompat;\n\nimport java.util.List;\n\npublic class NotificationPermissionUtil {\n\n  /** 检查通知权限是否开启 */\n  public static boolean isNotificationsEnabled(Context context) {\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n      // Android 8.0 及以上\n      NotificationManager manager =\n        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\n      if (manager == null) return false;\n\n      if (!manager.areNotificationsEnabled()) {\n        return false;\n      }\n\n      // 检查每个通知通道\n      try {\n        List<NotificationChannel> channels = manager.getNotificationChannels();\n        if (channels != null) {\n          for (NotificationChannel channel : channels) {\n            if (channel.getImportance() == NotificationManager.IMPORTANCE_NONE) {\n              return false;\n            }\n          }\n        }\n      } catch (Exception e) {\n        Log.w(\"NotificationUtil\", \"Error reading notification channels\", e);\n      }\n\n      return true;\n    } else {\n      // Android 5.1 - 7.1\n      return NotificationManagerCompat.from(context).areNotificationsEnabled();\n    }\n  }\n\n  /** 安全地打开通知设置页 */\n  public static boolean openNotificationPermissionActivity(Context context) {\n    String packageName = context.getPackageName();\n    Intent intent = new Intent();\n\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n      // Android 8.0 及以上\n      intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);\n      intent.putExtra(\"android.provider.extra.APP_PACKAGE\", packageName);\n    } else {\n      // Android 5.1 - 7.1\n      intent.setAction(\"android.settings.APP_NOTIFICATION_SETTINGS\");\n      intent.putExtra(\"app_package\", packageName);\n      intent.putExtra(\"app_uid\", context.getApplicationInfo().uid);\n    }\n\n    // 加上 NEW_TASK 标志，确保从非 Activity Context 启动不会崩溃\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n\n    // 检查系统是否支持该 Intent（防止某些 ROM 无响应导致卡死）\n    PackageManager pm = context.getPackageManager();\n    List<ResolveInfo> infos = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);\n    if (infos.isEmpty()) {\n      Log.w(\"NotificationUtil\", \"No activity found to handle notification settings intent\");\n      return false;\n    }\n\n    try {\n      context.startActivity(intent);\n      return true;\n    } catch (Exception e) {\n      Log.e(\"NotificationUtil\", \"Failed to start notification settings\", e);\n      return false;\n    }\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/Utils.java",
    "content": "package cn.toside.music.mobile.utils;\n\n\nimport android.content.Context;\nimport android.os.storage.StorageManager;\n\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactMethod;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.lang.reflect.Array;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.Objects;\nimport java.util.concurrent.Callable;\n\npublic class Utils {\n//  public static boolean deletePath(File dir) {\n//    if (dir.isDirectory()) {\n//      String[] children = dir.list();\n//      for (int i=0; i< children.length; i++) {\n//        boolean success = deletePath(new File(dir, children[i]));\n//        if (!success) {\n//          return false;\n//        }\n//      }\n//    }\n//\n//    // The directory is now empty so delete it\n//    return dir.delete();\n//  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/UtilsEvent.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport android.util.Log;\n\nimport androidx.annotation.Nullable;\n\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.WritableMap;\nimport com.facebook.react.modules.core.DeviceEventManagerModule;\n\npublic class UtilsEvent {\n  final String SCREEN_STATE = \"screen-state\";\n  final String SCREEN_SIZE_CHANGED = \"screen-size-changed\";\n\n  private final ReactApplicationContext reactContext;\n  UtilsEvent(ReactApplicationContext reactContext) { this.reactContext = reactContext; }\n\n  public void sendEvent(String eventName, @Nullable WritableMap params) {\n    reactContext\n      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)\n      .emit(eventName, params);\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/UtilsModule.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Rect;\nimport android.net.Uri;\nimport android.net.wifi.WifiInfo;\nimport android.net.wifi.WifiManager;\nimport android.os.Build;\nimport android.util.Log;\nimport android.view.Window;\nimport android.view.WindowManager;\n\nimport androidx.core.app.LocaleManagerCompat;\nimport androidx.core.content.FileProvider;\nimport androidx.core.os.LocaleListCompat;\n\nimport com.facebook.react.bridge.Arguments;\nimport com.facebook.react.bridge.Promise;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.bridge.ReactContextBaseJavaModule;\nimport com.facebook.react.bridge.ReactMethod;\nimport com.facebook.react.bridge.WritableArray;\nimport com.facebook.react.bridge.WritableMap;\nimport com.facebook.react.bridge.WritableNativeArray;\n\nimport java.io.File;\nimport java.util.Locale;\nimport java.util.Objects;\n\npublic class UtilsModule extends ReactContextBaseJavaModule {\n  private final ReactApplicationContext reactContext;\n\n  private int listenerCount = 0;\n\n  UtilsEvent utilsEvent;\n\n  UtilsModule(ReactApplicationContext reactContext) {\n    super(reactContext);\n    this.reactContext = reactContext;\n    utilsEvent = new UtilsEvent(reactContext);\n    registerScreenBroadcastReceiver();\n  }\n\n  @Override\n  public String getName() {\n    return \"UtilsModule\";\n  }\n\n  @ReactMethod\n  public void addListener(String eventName) {\n    if (listenerCount == 0) {\n      // Set up any upstream listeners or background tasks as necessary\n    }\n\n    listenerCount += 1;\n  }\n\n  @ReactMethod\n  public void removeListeners(Integer count) {\n    listenerCount -= count;\n    if (listenerCount == 0) {\n      // Remove upstream listeners, stop unnecessary background tasks\n    }\n  }\n\n  private void registerScreenBroadcastReceiver() {\n    final IntentFilter theFilter = new IntentFilter();\n    /** System Defined Broadcast */\n    theFilter.addAction(Intent.ACTION_SCREEN_ON);\n    theFilter.addAction(Intent.ACTION_SCREEN_OFF);\n\n    BroadcastReceiver screenOnOffReceiver = new BroadcastReceiver() {\n      @Override\n      public void onReceive(Context context, Intent intent) {\n        String strAction = intent.getAction();\n\n        WritableMap params = Arguments.createMap();\n\n        switch (Objects.requireNonNull(strAction)) {\n          case Intent.ACTION_SCREEN_OFF:\n\n            params.putString(\"state\", \"OFF\");\n            utilsEvent.sendEvent(utilsEvent.SCREEN_STATE, params);\n            break;\n          case Intent.ACTION_SCREEN_ON:\n            params.putString(\"state\", \"ON\");\n            utilsEvent.sendEvent(utilsEvent.SCREEN_STATE, params);\n            break;\n        }\n      }\n    };\n\n    reactContext.registerReceiver(screenOnOffReceiver, theFilter);\n  }\n\n  @ReactMethod\n  public void exitApp() {\n    // https://github.com/wumke/react-native-exit-app/blob/master/android/src/main/java/com/github/wumke/RNExitApp/RNExitAppModule.java\n    // android.os.Process.killProcess(android.os.Process.myPid());\n\n    // https://stackoverflow.com/questions/6330200/how-to-quit-android-application-programmatically\n    Activity currentActivity = reactContext.getCurrentActivity();\n    Log.d(\"Utils\", \"Exit app...\");\n    if (currentActivity == null) {\n      Log.d(\"Utils\", \"killProcess\");\n      android.os.Process.killProcess(android.os.Process.myPid());\n    } else {\n      currentActivity.finishAndRemoveTask();\n      System.exit(0);\n    }\n  }\n\n  @ReactMethod\n  public void getSupportedAbis(Promise promise) {\n    // https://github.com/react-native-device-info/react-native-device-info/blob/ff8f672cb08fa39a887567d6e23e2f08778e8340/android/src/main/java/com/learnium/RNDeviceInfo/RNDeviceModule.java#L877\n    WritableArray array = new WritableNativeArray();\n    for (String abi : Build.SUPPORTED_ABIS) {\n      array.pushString(abi);\n    }\n    promise.resolve(array);\n  }\n\n  @ReactMethod\n  public void installApk(String filePath, String fileProviderAuthority, Promise promise) {\n    // https://github.com/mikehardy/react-native-update-apk/blob/master/android/src/main/java/net/mikehardy/rnupdateapk/RNUpdateAPK.java\n    File file = new File(filePath);\n    if (!file.exists()) {\n      Log.e(\"Utils\", \"installApk: file doe snot exist '\" + filePath + \"'\");\n      // FIXME this should take a promise and fail it\n      promise.reject(\"Utils\", \"installApk: file doe snot exist '\" + filePath + \"'\");\n      return;\n    }\n\n    if (Build.VERSION.SDK_INT >= 24) {\n      // API24 and up has a package installer that can handle FileProvider content:// URIs\n      Uri contentUri;\n      try {\n        contentUri = FileProvider.getUriForFile(getReactApplicationContext(), fileProviderAuthority, file);\n      } catch (Exception e) {\n        // FIXME should be a Promise.reject really\n        Log.e(\"Utils\", \"installApk exception with authority name '\" + fileProviderAuthority + \"'\", e);\n        promise.reject(\"Utils\", \"installApk exception with authority name '\" + fileProviderAuthority + \"'\");\n        return;\n        // throw e;\n      }\n      Intent installApp = new Intent(Intent.ACTION_INSTALL_PACKAGE);\n      installApp.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);\n      installApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n      installApp.setData(contentUri);\n      installApp.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, reactContext.getApplicationInfo().packageName);\n      reactContext.startActivity(installApp);\n      promise.resolve(null);\n    } else {\n      // Old APIs do not handle content:// URIs, so use an old file:// style\n      String cmd = \"chmod 777 \" + file;\n      try {\n        Runtime.getRuntime().exec(cmd);\n      } catch (Exception e) {\n        // e.printStackTrace();\n        Log.e(\"Utils\", \"installApk exception : \" + e.getMessage(), e);\n        promise.reject(\"Utils\", e.getMessage());\n      }\n      Intent intent = new Intent(Intent.ACTION_VIEW);\n      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);\n      intent.setDataAndType(Uri.parse(\"file://\" + file), \"application/vnd.android.package-archive\");\n      reactContext.startActivity(intent);\n      promise.resolve(null);\n    }\n  }\n\n  @ReactMethod\n  public void screenkeepAwake() {\n    // https://github.com/corbt/react-native-keep-awake/blob/master/android/src/main/java/com/corbt/keepawake/KCKeepAwake.java\n    final Activity activity = getCurrentActivity();\n\n    if (activity != null) {\n      activity.runOnUiThread(() -> {\n        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n      });\n    }\n  }\n\n  @ReactMethod\n  public void screenUnkeepAwake() {\n    // https://github.com/corbt/react-native-keep-awake/blob/master/android/src/main/java/com/corbt/keepawake/KCKeepAwake.java\n    final Activity activity = getCurrentActivity();\n\n    if (activity != null) {\n      activity.runOnUiThread(() -> {\n        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n      });\n    }\n  }\n\n  /**\n   Gets the device's WiFi interface IP address\n   @return device's WiFi IP if connected to WiFi, else '0.0.0.0'\n   */\n  @ReactMethod\n  public void getWIFIIPV4Address(final Promise promise) throws Exception {\n    // https://github.com/pusherman/react-native-network-info/blob/master/android/src/main/java/com/pusherman/networkinfo/RNNetworkInfo.java\n    WifiManager wifi = (WifiManager) reactContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE);\n    new Thread(new Runnable() {\n      public void run() {\n        try {\n          WifiInfo info = wifi.getConnectionInfo();\n          int ipAddress = info.getIpAddress();\n          @SuppressLint(\"DefaultLocale\") String stringip = String.format(\"%d.%d.%d.%d\", (ipAddress & 0xff), (ipAddress >> 8 & 0xff),\n            (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));\n          promise.resolve(stringip);\n        }catch (Exception e) {\n          promise.resolve(null);\n        }\n      }\n    }).start();\n  }\n\n  // https://stackoverflow.com/a/26117646\n  @ReactMethod\n  public void getDeviceName(final Promise promise) {\n    String manufacturer = Build.MANUFACTURER;\n    String model = Build.MODEL;\n    if (model.startsWith(manufacturer)) {\n      promise.resolve(capitalize(model));\n    } else {\n      promise.resolve(capitalize(manufacturer) + \" \" + model);\n    }\n  }\n  private String capitalize(String s) {\n    if (s == null || s.isEmpty()) {\n      return \"\";\n    }\n    char first = s.charAt(0);\n    if (Character.isUpperCase(first)) {\n      return s;\n    } else {\n      return Character.toUpperCase(first) + s.substring(1);\n    }\n  }\n\n  @ReactMethod\n  public void isNotificationsEnabled(final Promise promise) {\n    new Thread(() -> {\n      boolean enabled = NotificationPermissionUtil.isNotificationsEnabled(\n        reactContext.getApplicationContext());\n      promise.resolve(enabled);\n    }).start();\n  }\n\n  @ReactMethod\n  public void openNotificationPermissionActivity(Promise promise) {\n    new Thread(() -> {\n      boolean result = NotificationPermissionUtil.openNotificationPermissionActivity(\n        reactContext.getApplicationContext());\n      promise.resolve(result);\n    }).start();\n  }\n\n  @ReactMethod\n  public void shareText(String shareTitle, String title, String text) {\n    Intent shareIntent = new Intent(Intent.ACTION_SEND);\n    shareIntent.setType(\"text/plain\");\n    shareIntent.putExtra(Intent.EXTRA_TEXT,text);\n    shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);\n    Objects.requireNonNull(reactContext.getCurrentActivity()).startActivity(Intent.createChooser(shareIntent, shareTitle));\n  }\n\n  // https://stackoverflow.com/questions/73463341/in-per-app-language-how-to-get-app-locale-in-api-33-if-system-locale-is-diffe\n  @ReactMethod\n  public void getSystemLocales(Promise promise) {\n    Locale locale = null;\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n      LocaleListCompat list = LocaleManagerCompat.getSystemLocales(reactContext);\n      if (!list.isEmpty()) {\n        locale = list.get(0);\n      } else {\n        promise.resolve(null);\n        return;\n      }\n    } else {\n      locale = Locale.getDefault();\n    }\n    if (locale == null) {\n      promise.resolve(\"\");\n      return;\n    }\n\n    // 格式化成 zh_cn、en_us 等\n    String language = locale.getLanguage(); // zh, en\n    String country = locale.getCountry();   // CN, US\n    String localeStr;\n\n    if (!country.isEmpty()) {\n      localeStr = language.toLowerCase() + \"_\" + country.toLowerCase();\n    } else {\n      localeStr = language.toLowerCase();\n    }\n\n    promise.resolve(localeStr);\n  }\n\n  // https://github.com/Anthonyzou/react-native-full-screen/blob/master/android/src/main/java/com/rn/full/screen/FullScreen.java\n  //  @ReactMethod\n  //  public void onFullScreen() {\n  //    UiThreadUtil.runOnUiThread(() -> {\n  //      Activity currentActivity = reactContext.getCurrentActivity();\n  //      if (currentActivity == null) return;\n  //      currentActivity.getWindow().getDecorView().setSystemUiVisibility(\n  //        View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n  //          | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n  //          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n  //          | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar\n  //          | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar\n  //          | View.SYSTEM_UI_FLAG_IMMERSIVE\n  //      );\n  //    });\n  //  }\n  //  @ReactMethod\n  //  public void offFullScreen() {\n  //    UiThreadUtil.runOnUiThread(() -> {\n  //      Activity currentActivity = reactContext.getCurrentActivity();\n  //      if (currentActivity == null) return;\n  //      currentActivity.getWindow().getDecorView().setSystemUiVisibility(\n  //        View.SYSTEM_UI_FLAG_LAYOUT_STABLE\n  //          | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION\n  //          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN\n  //      );\n  //    });\n  //  }\n\n  @ReactMethod\n  public void getWindowSize(Promise promise) {\n    WritableMap params = Arguments.createMap();\n\n    Activity currentActivity = reactContext.getCurrentActivity();\n    if (currentActivity == null) {\n      params.putInt(\"width\", 0);\n      params.putInt(\"height\", 0);\n      promise.resolve(params);\n      return;\n    }\n    // 获取当前应用可用区域大小\n    Window window = currentActivity.getWindow();\n    Rect rect = new Rect();\n    window.getDecorView().getWindowVisibleDisplayFrame(rect);\n    // View decorView = window.getDecorView();\n    // int width = decorView.getMeasuredWidth();\n    // int height = decorView.getMeasuredHeight();\n    params.putInt(\"width\", rect.width());\n    params.putInt(\"height\", rect.height());\n    promise.resolve(params);\n  }\n\n  @ReactMethod\n  public void isIgnoringBatteryOptimization(Promise promise) {\n    new Thread(() -> {\n      boolean result = BatteryOptimizationUtil.isIgnoringBatteryOptimization(\n        reactContext.getApplicationContext(),\n        reactContext.getPackageName()\n      );\n      promise.resolve(result);\n    }).start();\n  }\n\n  @ReactMethod\n  public void requestIgnoreBatteryOptimization(Promise promise) {\n    new Thread(() -> {\n      try {\n        boolean result = BatteryOptimizationUtil.requestIgnoreBatteryOptimization(\n          reactContext.getApplicationContext(),\n          reactContext.getPackageName()\n        );\n        promise.resolve(result);\n      } catch (Exception e) {\n        promise.reject(\"ERROR\", e);\n      }\n    }).start();\n  }\n}\n\n"
  },
  {
    "path": "android/app/src/main/java/cn/toside/music/mobile/utils/UtilsPackage.java",
    "content": "package cn.toside.music.mobile.utils;\n\nimport com.facebook.react.ReactPackage;\nimport com.facebook.react.bridge.NativeModule;\nimport com.facebook.react.bridge.ReactApplicationContext;\nimport com.facebook.react.uimanager.ViewManager;\n\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\n\npublic class UtilsPackage implements ReactPackage {\n\n  @Override\n  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {\n    return Collections.emptyList();\n  }\n\n  @Override\n  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {\n    return Arrays.<NativeModule>asList(new UtilsModule(reactContext));\n  }\n}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"447.94\"\n    android:viewportHeight=\"447.94\">\n  <group android:scaleX=\"0.48\"\n      android:scaleY=\"0.48\"\n      android:translateX=\"116.4644\"\n      android:translateY=\"116.4644\">\n    <path\n        android:pathData=\"M203.81,0.48c-19.67,-3.35 -35.76,11.14 -35.76,31.09v206.17c-11.64,-4.27 -24.17,-6.72 -37.28,-6.72c-59.9,0 -108.47,48.57 -108.47,108.47c0,59.9 48.56,108.46 108.47,108.46c34.14,0 64.54,-15.82 84.41,-40.48l-49.66,-49.66c-15.12,-15.11 -11.71,-28.9 -9.54,-34.14c2.17,-5.23 9.51,-17.4 30.88,-17.4h18.08v-56.88c0,-21.13 14.62,-38.86 34.27,-43.74c0.03,-44.37 0.03,-81.81 0.03,-81.81c140.15,0 131.72,83.97 115.32,132.2c-6.42,18.88 -2.6,22.05 10.89,7.35C536.47,77.11 298.38,16.57 203.81,0.48z\"\n        android:fillColor=\"#5ed698\"/>\n    <path\n        android:pathData=\"M301.06,223.88h-50.99c-3.91,0 -7.57,0.95 -10.89,2.52c-8.62,4.09 -14.61,12.8 -14.61,22.97v76.51h-37.71c-14.08,0 -17.43,8.07 -7.47,18.03l46.89,46.9l31.25,31.25c4.98,4.99 11.51,7.47 18.03,7.47c6.52,0 13.05,-2.48 18.03,-7.47l78.15,-78.14c9.95,-9.96 6.61,-18.03 -7.47,-18.03h-37.71v-76.51C326.57,235.29 315.15,223.88 301.06,223.88z\"\n        android:fillColor=\"#4daf7c\"/>\n  </group>\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/rn_edit_text_material.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Copyright (C) 2014 The Android Open Source Project\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-->\n<inset xmlns:android=\"http://schemas.android.com/apk/res/android\"\n       android:insetLeft=\"@dimen/abc_edit_text_inset_horizontal_material\"\n       android:insetRight=\"@dimen/abc_edit_text_inset_horizontal_material\"\n       android:insetTop=\"@dimen/abc_edit_text_inset_top_material\"\n       android:insetBottom=\"@dimen/abc_edit_text_inset_bottom_material\">\n\n    <selector>\n        <!--\n          This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).\n          The item below with state_pressed=\"false\" and state_focused=\"false\" causes a NullPointerException.\n          NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'\n\n          <item android:state_pressed=\"false\" android:state_focused=\"false\" android:drawable=\"@drawable/abc_textfield_default_mtrl_alpha\"/>\n\n          For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.\n        -->\n        <item android:state_enabled=\"false\" android:drawable=\"@drawable/abc_textfield_default_mtrl_alpha\"/>\n        <item android:drawable=\"@drawable/abc_textfield_activated_mtrl_alpha\"/>\n    </selector>\n\n</inset>\n"
  },
  {
    "path": "android/app/src/main/res/drawable/rounded_corner.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n  <solid android:color=\"#29000000\" />\n\n  <corners android:radius=\"2dp\" />\n</shape>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">LX Music</string>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"android:editTextBackground\">@drawable/rn_edit_text_material</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/xml/file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n\n  <!-- Select one of the following based on your apk location -->\n\n  <!-- cache dir is always available and requires no permissions, but space may be limited -->\n  <cache-path name=\"cache\" path=\"/\" />\n  <!-- <files-path name=\"name\" path=\"/\" />  -->\n\n  <!-- External cache dir is maybe user-friendly for downloaded APKs, but you must be careful. -->\n  <!-- 1) in API <19 (KitKat) this requires WRITE_EXTERNAL_STORAGE permission. >=19, no permission -->\n  <!-- 2) this directory may not be available, check Environment.isExternalStorageEmulated(file) to see -->\n  <!-- 3) there may be no beneifit versus cache-path if external storage is emulated. Check Environment.isExternalStorageEmulated(File) to verify -->\n  <!-- 4) the path will change for each app 'com.example' must be replaced by your application package -->\n  <!-- <external-cache-path name=\"external-cache\" path=\"/data/user/0/com.example/cache\" /> -->\n\n  <!-- Note that these external paths require WRITE_EXTERNAL_STORAGE permission -->\n  <!-- <external-path name=\"some_external_path\" path=\"put-your-specific-external-path-here\" />  -->\n  <!-- <external-files-path name=\"external-files\" path=\"/data/user/0/com.example/cache\" />  -->\n  <!-- <external-media-path name=\"external-media\" path=\"put-your-path-to-media-here\" />  -->\n</paths>\n"
  },
  {
    "path": "android/app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?> \n<network-security-config> \n    <base-config cleartextTrafficPermitted=\"true\" /> \n</network-security-config>\n"
  },
  {
    "path": "android/app/src/release/java/cn/toside/music/ReactNativeFlipper.java",
    "content": "/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * <p>This source code is licensed under the MIT license found in the LICENSE file in the root\n * directory of this source tree.\n */\npackage cn.toside.music.mobile;\n\nimport android.content.Context;\nimport com.facebook.react.ReactInstanceManager;\n\n/**\n * Class responsible of loading Flipper inside your React Native application. This is the release\n * flavor of it so it's empty as we don't want to load Flipper.\n */\npublic class ReactNativeFlipper {\n  public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {\n    // Do nothing as we don't want to initialize Flipper on Release.\n  }\n}\n"
  },
  {
    "path": "android/build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    ext {\n        buildToolsVersion = \"35.0.0\"\n        minSdkVersion = 21\n        compileSdkVersion = 36\n        targetSdkVersion = 29\n\n        ndkVersion = \"26.1.10909125\"\n        kotlinVersion = \"1.9.24\" // Or any version above 1.3.x\n        RNNKotlinVersion = kotlinVersion\n\n        // https://github.com/dream-horizon-org/react-native-fast-image/blob/main/docs/app-glide-module.md\n        excludeAppGlideModule = true\n    }\n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath('com.android.tools.build:gradle:8.6.1')\n        classpath(\"com.facebook.react:react-native-gradle-plugin\")\n        classpath(\"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion\")\n    }\n}\n\napply plugin: \"com.facebook.react.rootproject\"\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.8-all.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m\norg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=2048m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\n\n# Use this property to specify which architecture you want to build.\n# You can also override it from the CLI using\n# ./gradlew <task> -PreactNativeArchitectures=x86_64\nreactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64\n\n# Use this property to enable support to the new architecture.\n# This will allow you to use TurboModules and the Fabric render in\n# your application. You should enable this flag either if you want\n# to write custom TurboModules/Fabric components OR use libraries that\n# are providing them.\nnewArchEnabled=false\n\n# Use this property to enable or disable the Hermes JS engine.\n# If set to false, you will be using JSC instead.\nhermesEnabled=true\n\n\n# org.gradle.daemon=true\n# org.gradle.configureondemand=true\n\n\n# AsyncStorage_dedicatedExecutor = true\n# https://react-native-async-storage.github.io/async-storage/docs/advanced/next\nAsyncStorage_useNextStorage=true\nAsyncStorage_kotlinVersion=1.8.10\n# https://developer.android.com/jetpack/androidx/releases/room\nAsyncStorage_next_roomVersion=2.4.3\n\n# https://github.com/wix/react-native-navigation/issues/7403\n# android.jetifier.blacklist = bcprov-jdk15on\n"
  },
  {
    "path": "android/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\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#      https://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#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd \"${APP_HOME:-./}\" > /dev/null && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\"==\"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\n@rem This is normally unused\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif %ERRORLEVEL% equ 0 goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho. 1>&2\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\necho. 1>&2\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\necho location of your Java installation. 1>&2\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif %ERRORLEVEL% equ 0 goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nset EXIT_CODE=%ERRORLEVEL%\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\nexit /b %EXIT_CODE%\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "rootProject.name = 'cn.toside.music.mobile'\napply from: file(\"../node_modules/@react-native-community/cli-platform-android/native_modules.gradle\"); applyNativeModulesSettingsGradle(settings)\ninclude ':app'\nincludeBuild('../node_modules/@react-native/gradle-plugin')\n"
  },
  {
    "path": "app.json",
    "content": "{\n  \"name\": \"LX Music\",\n  \"displayName\": \"洛雪音乐助手\"\n}\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  presets: ['module:@react-native/babel-preset'],\n  plugins: [\n    '@babel/plugin-proposal-export-namespace-from',\n    [\n      'module-resolver',\n      {\n        root: ['.'],\n        extensions: [\n          '.android.ts',\n          '.ios.ts',\n          '.android.tsx',\n          '.ios.tsx',\n          '.tsx',\n          '.ts',\n          '.android.js',\n          '.ios.js',\n          '.android.jsx',\n          '.ios.jsx',\n          '.jsx',\n          '.js',\n          '.json',\n        ],\n        alias: {\n          '@': './src',\n          // '@config': './src/config',\n          // '@store': './src/store',\n          // '@components': './src/components',\n          // '@navigation': './src/navigation',\n          // '@screens': './src/screens',\n          // '@theme': './src/theme',\n        },\n      },\n    ],\n  ],\n}\n"
  },
  {
    "path": "dependencies-patch.js",
    "content": "// 修补依赖源码以使构建的依赖恢复正常工作\n\nconst fs = require('node:fs')\nconst path = require('node:path')\n\nconst rootPath = path.join(__dirname, './')\n\nconst patchs = [\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": "index.js",
    "content": "/**\n * @format\n */\nimport './shim'\nimport './src/app'\n// import './test'\n// import '@/utils/errorHandle'\n// import { Navigation } from 'react-native-navigation'\n// import App from './App'\n\n// Navigation.registerComponent('com.myApp.WelcomeScreen', () => App)\n// Navigation.events().registerAppLaunchedListener(() => {\n//   Navigation.setRoot({\n//     root: {\n//       stack: {\n//         children: [\n//           {\n//             component: {\n//               name: 'com.myApp.WelcomeScreen',\n//             },\n//           },\n//         ],\n//       },\n//     },\n//   })\n// })\n"
  },
  {
    "path": "ios/.xcode.env",
    "content": "# This `.xcode.env` file is versioned and is used to source the environment\n# used when running script phases inside Xcode.\n# To customize your local environment, you can create an `.xcode.env.local`\n# file that is not versioned.\n\n# NODE_BINARY variable contains the PATH to the node executable.\n#\n# Customize the NODE_BINARY variable here.\n# For example, to use nvm with brew, add the following line\n# . \"$(brew --prefix nvm)/nvm.sh\" --no-use\nexport NODE_BINARY=$(command -v node)\n"
  },
  {
    "path": "ios/LxMusicMobile/AppDelegate.h",
    "content": "#import <RCTAppDelegate.h>\n#import <UIKit/UIKit.h>\n\n@interface AppDelegate : RCTAppDelegate\n\n@end\n"
  },
  {
    "path": "ios/LxMusicMobile/AppDelegate.mm",
    "content": "#import \"AppDelegate.h\"\n#import <ReactNativeNavigation/ReactNativeNavigation.h>\n\n#import <React/RCTBundleURLProvider.h>\n\n@implementation AppDelegate\n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions\n{\n  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];\n  [ReactNativeNavigation bootstrapWithBridge:bridge];\n  // You can add your custom initial props in the dictionary below.\n  // They will be passed down to the ViewController used by React Native.\n  self.initialProps = @{};\n\n  return YES;\n}\n\n- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge {\n  return [ReactNativeNavigation extraModulesForBridge:bridge];\n}\n\n- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge\n{\n  return [self getBundleURL];\n}\n- (NSURL *)getBundleURL\n{\n#if DEBUG\n  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@\"index\"];\n#else\n  return [[NSBundle mainBundle] URLForResource:@\"main\" withExtension:@\"jsbundle\"];\n#endif\n}\n\n@end\n"
  },
  {
    "path": "ios/LxMusicMobile/Images.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"29x29\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"40x40\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"3x\",\n      \"size\" : \"60x60\"\n    },\n    {\n      \"idiom\" : \"ios-marketing\",\n      \"scale\" : \"1x\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "ios/LxMusicMobile/Images.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"version\" : 1,\n    \"author\" : \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/LxMusicMobile/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>LxMusicMobile</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>NSAppTransportSecurity</key>\n\t<dict>\n\t  <!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->\n\t\t<key>NSAllowsArbitraryLoads</key>\n\t\t<false/>\n\t\t<key>NSAllowsLocalNetworking</key>\n\t\t<true/>\n\t</dict>\n\t<key>NSLocationWhenInUseUsageDescription</key>\n\t<string></string>\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/LxMusicMobile/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3.0\" toolsVersion=\"15702\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" useAutolayout=\"YES\" launchScreen=\"YES\" useTraitCollections=\"YES\" useSafeAreas=\"YES\" colorMatched=\"YES\" initialViewController=\"01J-lp-oVM\">\n    <device id=\"retina4_7\" orientation=\"portrait\" appearance=\"light\"/>\n    <dependencies>\n        <deployment identifier=\"iOS\"/>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"15704\"/>\n        <capability name=\"Safe area layout guides\" minToolsVersion=\"9.0\"/>\n        <capability name=\"documents saved in the Xcode 8 format\" minToolsVersion=\"8.0\"/>\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <view key=\"view\" contentMode=\"scaleToFill\" id=\"Ze5-6b-2t3\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"375\" height=\"667\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <subviews>\n                            <label opaque=\"NO\" clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"LxMusicMobile\" textAlignment=\"center\" lineBreakMode=\"middleTruncation\" baselineAdjustment=\"alignBaselines\" minimumFontSize=\"18\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"GJd-Yh-RWb\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"202\" width=\"375\" height=\"43\"/>\n                                <fontDescription key=\"fontDescription\" type=\"boldSystem\" pointSize=\"36\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                            <label opaque=\"NO\" clipsSubviews=\"YES\" userInteractionEnabled=\"NO\" contentMode=\"left\" horizontalHuggingPriority=\"251\" verticalHuggingPriority=\"251\" text=\"Powered by React Native\" textAlignment=\"center\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" minimumFontSize=\"9\" translatesAutoresizingMaskIntoConstraints=\"NO\" id=\"MN2-I3-ftu\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"626\" width=\"375\" height=\"21\"/>\n                                <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"17\"/>\n                                <nil key=\"highlightedColor\"/>\n                            </label>\n                        </subviews>\n                        <color key=\"backgroundColor\" systemColor=\"systemBackgroundColor\" cocoaTouchSystemColor=\"whiteColor\"/>\n                        <constraints>\n                            <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"bottom\" secondItem=\"MN2-I3-ftu\" secondAttribute=\"bottom\" constant=\"20\" id=\"OZV-Vh-mqD\"/>\n                            <constraint firstItem=\"Bcu-3y-fUS\" firstAttribute=\"centerX\" secondItem=\"GJd-Yh-RWb\" secondAttribute=\"centerX\" id=\"Q3B-4B-g5h\"/>\n                            <constraint firstItem=\"MN2-I3-ftu\" firstAttribute=\"centerX\" secondItem=\"Bcu-3y-fUS\" secondAttribute=\"centerX\" id=\"akx-eg-2ui\"/>\n                            <constraint firstItem=\"MN2-I3-ftu\" firstAttribute=\"leading\" secondItem=\"Bcu-3y-fUS\" secondAttribute=\"leading\" id=\"i1E-0Y-4RG\"/>\n                            <constraint firstItem=\"GJd-Yh-RWb\" firstAttribute=\"centerY\" secondItem=\"Ze5-6b-2t3\" secondAttribute=\"bottom\" multiplier=\"1/3\" constant=\"1\" id=\"moa-c2-u7t\"/>\n                            <constraint firstItem=\"GJd-Yh-RWb\" firstAttribute=\"leading\" secondItem=\"Bcu-3y-fUS\" secondAttribute=\"leading\" symbolic=\"YES\" id=\"x7j-FC-K8j\"/>\n                        </constraints>\n                        <viewLayoutGuide key=\"safeArea\" id=\"Bcu-3y-fUS\"/>\n                    </view>\n                </viewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"iYj-Kq-Ea1\" userLabel=\"First Responder\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"52.173913043478265\" y=\"375\"/>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/LxMusicMobile/main.m",
    "content": "#import <UIKit/UIKit.h>\n\n#import \"AppDelegate.h\"\n\nint main(int argc, char *argv[])\n{\n  @autoreleasepool {\n    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));\n  }\n}\n"
  },
  {
    "path": "ios/LxMusicMobile.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 54;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t00E356F31AD99517003FC87E /* LxMusicMobileTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* LxMusicMobileTests.m */; };\n\t\t0C80B921A6F3F58F76C31292 /* libPods-LxMusicMobile.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-LxMusicMobile.a */; };\n\t\t13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };\n\t\t13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };\n\t\t13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };\n\t\t7699B88040F8A987B510C191 /* libPods-LxMusicMobile-LxMusicMobileTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 19F6CBCC0A4E27FBF8BF4A61 /* libPods-LxMusicMobile-LxMusicMobileTests.a */; };\n\t\t81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 13B07F861A680F5B00A75B9A;\n\t\t\tremoteInfo = LxMusicMobile;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXFileReference section */\n\t\t00E356EE1AD99517003FC87E /* LxMusicMobileTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LxMusicMobileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t00E356F21AD99517003FC87E /* LxMusicMobileTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LxMusicMobileTests.m; sourceTree = \"<group>\"; };\n\t\t13B07F961A680F5B00A75B9A /* LxMusicMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LxMusicMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = LxMusicMobile/AppDelegate.h; sourceTree = \"<group>\"; };\n\t\t13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = LxMusicMobile/AppDelegate.mm; sourceTree = \"<group>\"; };\n\t\t13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = LxMusicMobile/Images.xcassets; sourceTree = \"<group>\"; };\n\t\t13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = LxMusicMobile/Info.plist; sourceTree = \"<group>\"; };\n\t\t13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = LxMusicMobile/main.m; sourceTree = \"<group>\"; };\n\t\t19F6CBCC0A4E27FBF8BF4A61 /* libPods-LxMusicMobile-LxMusicMobileTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-LxMusicMobile-LxMusicMobileTests.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t3B4392A12AC88292D35C810B /* Pods-LxMusicMobile.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-LxMusicMobile.debug.xcconfig\"; path = \"Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t5709B34CF0A7D63546082F79 /* Pods-LxMusicMobile.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-LxMusicMobile.release.xcconfig\"; path = \"Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t5B7EB9410499542E8C5724F5 /* Pods-LxMusicMobile-LxMusicMobileTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-LxMusicMobile-LxMusicMobileTests.debug.xcconfig\"; path = \"Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests.debug.xcconfig\"; sourceTree = \"<group>\"; };\n\t\t5DCACB8F33CDC322A6C60F78 /* libPods-LxMusicMobile.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = \"libPods-LxMusicMobile.a\"; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = LxMusicMobile/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t89C6BE57DB24E9ADA2F236DE /* Pods-LxMusicMobile-LxMusicMobileTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = \"Pods-LxMusicMobile-LxMusicMobileTests.release.xcconfig\"; path = \"Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests.release.xcconfig\"; sourceTree = \"<group>\"; };\n\t\tED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t00E356EB1AD99517003FC87E /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t7699B88040F8A987B510C191 /* libPods-LxMusicMobile-LxMusicMobileTests.a in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t13B07F8C1A680F5B00A75B9A /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t0C80B921A6F3F58F76C31292 /* libPods-LxMusicMobile.a in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t00E356EF1AD99517003FC87E /* LxMusicMobileTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t00E356F21AD99517003FC87E /* LxMusicMobileTests.m */,\n\t\t\t\t00E356F01AD99517003FC87E /* Supporting Files */,\n\t\t\t);\n\t\t\tpath = LxMusicMobileTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t00E356F01AD99517003FC87E /* Supporting Files */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t00E356F11AD99517003FC87E /* Info.plist */,\n\t\t\t);\n\t\t\tname = \"Supporting Files\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t13B07FAE1A68108700A75B9A /* LxMusicMobile */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t13B07FAF1A68108700A75B9A /* AppDelegate.h */,\n\t\t\t\t13B07FB01A68108700A75B9A /* AppDelegate.mm */,\n\t\t\t\t13B07FB51A68108700A75B9A /* Images.xcassets */,\n\t\t\t\t13B07FB61A68108700A75B9A /* Info.plist */,\n\t\t\t\t81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,\n\t\t\t\t13B07FB71A68108700A75B9A /* main.m */,\n\t\t\t);\n\t\t\tname = LxMusicMobile;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t2D16E6871FA4F8E400B85C8A /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tED297162215061F000B7C4FE /* JavaScriptCore.framework */,\n\t\t\t\t5DCACB8F33CDC322A6C60F78 /* libPods-LxMusicMobile.a */,\n\t\t\t\t19F6CBCC0A4E27FBF8BF4A61 /* libPods-LxMusicMobile-LxMusicMobileTests.a */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t832341AE1AAA6A7D00B99B32 /* Libraries */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t);\n\t\t\tname = Libraries;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t83CBB9F61A601CBA00E9B192 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t13B07FAE1A68108700A75B9A /* LxMusicMobile */,\n\t\t\t\t832341AE1AAA6A7D00B99B32 /* Libraries */,\n\t\t\t\t00E356EF1AD99517003FC87E /* LxMusicMobileTests */,\n\t\t\t\t83CBBA001A601CBA00E9B192 /* Products */,\n\t\t\t\t2D16E6871FA4F8E400B85C8A /* Frameworks */,\n\t\t\t\tBBD78D7AC51CEA395F1C20DB /* Pods */,\n\t\t\t);\n\t\t\tindentWidth = 2;\n\t\t\tsourceTree = \"<group>\";\n\t\t\ttabWidth = 2;\n\t\t\tusesTabs = 0;\n\t\t};\n\t\t83CBBA001A601CBA00E9B192 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t13B07F961A680F5B00A75B9A /* LxMusicMobile.app */,\n\t\t\t\t00E356EE1AD99517003FC87E /* LxMusicMobileTests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\tBBD78D7AC51CEA395F1C20DB /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t3B4392A12AC88292D35C810B /* Pods-LxMusicMobile.debug.xcconfig */,\n\t\t\t\t5709B34CF0A7D63546082F79 /* Pods-LxMusicMobile.release.xcconfig */,\n\t\t\t\t5B7EB9410499542E8C5724F5 /* Pods-LxMusicMobile-LxMusicMobileTests.debug.xcconfig */,\n\t\t\t\t89C6BE57DB24E9ADA2F236DE /* Pods-LxMusicMobile-LxMusicMobileTests.release.xcconfig */,\n\t\t\t);\n\t\t\tpath = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t00E356ED1AD99517003FC87E /* LxMusicMobileTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget \"LxMusicMobileTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tA55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t00E356EA1AD99517003FC87E /* Sources */,\n\t\t\t\t00E356EB1AD99517003FC87E /* Frameworks */,\n\t\t\t\t00E356EC1AD99517003FC87E /* Resources */,\n\t\t\t\tC59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */,\n\t\t\t\tF6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t00E356F51AD99517003FC87E /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = LxMusicMobileTests;\n\t\t\tproductName = LxMusicMobileTests;\n\t\t\tproductReference = 00E356EE1AD99517003FC87E /* LxMusicMobileTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t13B07F861A680F5B00A75B9A /* LxMusicMobile */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget \"LxMusicMobile\" */;\n\t\t\tbuildPhases = (\n\t\t\t\tC38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t13B07F871A680F5B00A75B9A /* Sources */,\n\t\t\t\t13B07F8C1A680F5B00A75B9A /* Frameworks */,\n\t\t\t\t13B07F8E1A680F5B00A75B9A /* Resources */,\n\t\t\t\t00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,\n\t\t\t\t00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */,\n\t\t\t\tE235C05ADACE081382539298 /* [CP] Copy Pods Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = LxMusicMobile;\n\t\t\tproductName = LxMusicMobile;\n\t\t\tproductReference = 13B07F961A680F5B00A75B9A /* LxMusicMobile.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t83CBB9F71A601CBA00E9B192 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 1210;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t00E356ED1AD99517003FC87E = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 6.2;\n\t\t\t\t\t\tTestTargetID = 13B07F861A680F5B00A75B9A;\n\t\t\t\t\t};\n\t\t\t\t\t13B07F861A680F5B00A75B9A = {\n\t\t\t\t\t\tLastSwiftMigration = 1120;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject \"LxMusicMobile\" */;\n\t\t\tcompatibilityVersion = \"Xcode 12.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 83CBB9F61A601CBA00E9B192;\n\t\t\tproductRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t13B07F861A680F5B00A75B9A /* LxMusicMobile */,\n\t\t\t\t00E356ED1AD99517003FC87E /* LxMusicMobileTests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t00E356EC1AD99517003FC87E /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t13B07F8E1A680F5B00A75B9A /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"$(SRCROOT)/.xcode.env.local\",\n\t\t\t\t\"$(SRCROOT)/.xcode.env\",\n\t\t\t);\n\t\t\tname = \"Bundle React Native code and images\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"set -e\\n\\nWITH_ENVIRONMENT=\\\"../node_modules/react-native/scripts/xcode/with-environment.sh\\\"\\nREACT_NATIVE_XCODE=\\\"../node_modules/react-native/scripts/react-native-xcode.sh\\\"\\n\\n/bin/sh -c \\\"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\\\"\\n\";\n\t\t};\n\t\t00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tA55EABD7B0C7F3A422A6CC61 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-LxMusicMobile-LxMusicMobileTests-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tC38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-LxMusicMobile-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"diff \\\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\\\" \\\"${PODS_ROOT}/Manifest.lock\\\" > /dev/null\\nif [ $? != 0 ] ; then\\n    # print error to STDERR\\n    echo \\\"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\\\" >&2\\n    exit 1\\nfi\\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\\necho \\\"SUCCESS\\\" > \\\"${SCRIPT_OUTPUT_FILE_0}\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tC59DA0FBD6956966B86A3779 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-frameworks-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-frameworks-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tE235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-resources-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-resources-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile/Pods-LxMusicMobile-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\tF6A41C54EA430FDDC6A6ED99 /* [CP] Copy Pods Resources */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-resources-${CONFIGURATION}-input-files.xcfilelist\",\n\t\t\t);\n\t\t\tname = \"[CP] Copy Pods Resources\";\n\t\t\toutputFileListPaths = (\n\t\t\t\t\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-resources-${CONFIGURATION}-output-files.xcfilelist\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-LxMusicMobile-LxMusicMobileTests/Pods-LxMusicMobile-LxMusicMobileTests-resources.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t00E356EA1AD99517003FC87E /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t00E356F31AD99517003FC87E /* LxMusicMobileTests.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t13B07F871A680F5B00A75B9A /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,\n\t\t\t\t13B07FC11A68108700A75B9A /* main.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t00E356F51AD99517003FC87E /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 13B07F861A680F5B00A75B9A /* LxMusicMobile */;\n\t\t\ttargetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\t00E356F61AD99517003FC87E /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-LxMusicMobile-LxMusicMobileTests.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tINFOPLIST_FILE = LxMusicMobileTests/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.4;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-ObjC\",\n\t\t\t\t\t\"-lc++\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/LxMusicMobile.app/LxMusicMobile\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t00E356F71AD99517003FC87E /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 89C6BE57DB24E9ADA2F236DE /* Pods-LxMusicMobile-LxMusicMobileTests.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tINFOPLIST_FILE = LxMusicMobileTests/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.4;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t\t\"@loader_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"-ObjC\",\n\t\t\t\t\t\"-lc++\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/LxMusicMobile.app/LxMusicMobile\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t13B07F941A680F5B00A75B9A /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-LxMusicMobile.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tENABLE_BITCODE = NO;\n\t\t\t\tINFOPLIST_FILE = LxMusicMobile/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"-ObjC\",\n\t\t\t\t\t\"-lc++\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = LxMusicMobile;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t13B07F951A680F5B00A75B9A /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-LxMusicMobile.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tINFOPLIST_FILE = LxMusicMobile/Info.plist;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tOTHER_LDFLAGS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"-ObjC\",\n\t\t\t\t\t\"-lc++\",\n\t\t\t\t);\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = \"org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)\";\n\t\t\t\tPRODUCT_NAME = LxMusicMobile;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tVERSIONING_SYSTEM = \"apple-generic\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t83CBBA201A601CBA00E9B192 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"c++20\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphonesimulator*]\" = \"\";\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_SYMBOLS_PRIVATE_EXTERN = NO;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.4;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t/usr/lib/swift,\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"\\\"$(SDKROOT)/usr/lib/swift\\\"\",\n\t\t\t\t\t\"\\\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\\\"\",\n\t\t\t\t\t\"\\\"$(inherited)\\\"\",\n\t\t\t\t);\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tOTHER_CPLUSPLUSFLAGS = (\n\t\t\t\t\t\"$(OTHER_CFLAGS)\",\n\t\t\t\t\t\"-DFOLLY_NO_CONFIG\",\n\t\t\t\t\t\"-DFOLLY_MOBILE=1\",\n\t\t\t\t\t\"-DFOLLY_USE_LIBCPP=1\",\n\t\t\t\t\t\"-DFOLLY_CFG_NO_COROUTINES=1\",\n\t\t\t\t);\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t83CBBA211A601CBA00E9B192 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"c++20\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = YES;\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\t\"EXCLUDED_ARCHS[sdk=iphonesimulator*]\" = \"\";\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.4;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t/usr/lib/swift,\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tLIBRARY_SEARCH_PATHS = (\n\t\t\t\t\t\"\\\"$(SDKROOT)/usr/lib/swift\\\"\",\n\t\t\t\t\t\"\\\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\\\"\",\n\t\t\t\t\t\"\\\"$(inherited)\\\"\",\n\t\t\t\t);\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tOTHER_CPLUSPLUSFLAGS = (\n\t\t\t\t\t\"$(OTHER_CFLAGS)\",\n\t\t\t\t\t\"-DFOLLY_NO_CONFIG\",\n\t\t\t\t\t\"-DFOLLY_MOBILE=1\",\n\t\t\t\t\t\"-DFOLLY_USE_LIBCPP=1\",\n\t\t\t\t\t\"-DFOLLY_CFG_NO_COROUTINES=1\",\n\t\t\t\t);\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget \"LxMusicMobileTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t00E356F61AD99517003FC87E /* Debug */,\n\t\t\t\t00E356F71AD99517003FC87E /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget \"LxMusicMobile\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t13B07F941A680F5B00A75B9A /* Debug */,\n\t\t\t\t13B07F951A680F5B00A75B9A /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject \"LxMusicMobile\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t83CBBA201A601CBA00E9B192 /* Debug */,\n\t\t\t\t83CBBA211A601CBA00E9B192 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;\n}\n"
  },
  {
    "path": "ios/LxMusicMobile.xcodeproj/xcshareddata/xcschemes/LxMusicMobile.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"1210\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"13B07F861A680F5B00A75B9A\"\n               BuildableName = \"LxMusicMobile.app\"\n               BlueprintName = \"LxMusicMobile\"\n               ReferencedContainer = \"container:LxMusicMobile.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\">\n      <Testables>\n         <TestableReference\n            skipped = \"NO\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"00E356ED1AD99517003FC87E\"\n               BuildableName = \"LxMusicMobileTests.xctest\"\n               BlueprintName = \"LxMusicMobileTests\"\n               ReferencedContainer = \"container:LxMusicMobile.xcodeproj\">\n            </BuildableReference>\n         </TestableReference>\n      </Testables>\n   </TestAction>\n   <LaunchAction\n      buildConfiguration = \"Debug\"\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      debugServiceExtension = \"internal\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"13B07F861A680F5B00A75B9A\"\n            BuildableName = \"LxMusicMobile.app\"\n            BlueprintName = \"LxMusicMobile\"\n            ReferencedContainer = \"container:LxMusicMobile.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </LaunchAction>\n   <ProfileAction\n      buildConfiguration = \"Release\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable\n         runnableDebuggingMode = \"0\">\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"13B07F861A680F5B00A75B9A\"\n            BuildableName = \"LxMusicMobile.app\"\n            BlueprintName = \"LxMusicMobile\"\n            ReferencedContainer = \"container:LxMusicMobile.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ios/LxMusicMobileTests/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>BNDL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/LxMusicMobileTests/LxMusicMobileTests.m",
    "content": "#import <UIKit/UIKit.h>\n#import <XCTest/XCTest.h>\n\n#import <React/RCTLog.h>\n#import <React/RCTRootView.h>\n\n#define TIMEOUT_SECONDS 600\n#define TEXT_TO_LOOK_FOR @\"Welcome to React\"\n\n@interface LxMusicMobileTests : XCTestCase\n\n@end\n\n@implementation LxMusicMobileTests\n\n- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test\n{\n  if (test(view)) {\n    return YES;\n  }\n  for (UIView *subview in [view subviews]) {\n    if ([self findSubviewInView:subview matching:test]) {\n      return YES;\n    }\n  }\n  return NO;\n}\n\n- (void)testRendersWelcomeScreen\n{\n  UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];\n  NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];\n  BOOL foundElement = NO;\n\n  __block NSString *redboxError = nil;\n#ifdef DEBUG\n  RCTSetLogFunction(\n      ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {\n        if (level >= RCTLogLevelError) {\n          redboxError = message;\n        }\n      });\n#endif\n\n  while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {\n    [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];\n    [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];\n\n    foundElement = [self findSubviewInView:vc.view\n                                  matching:^BOOL(UIView *view) {\n                                    if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {\n                                      return YES;\n                                    }\n                                    return NO;\n                                  }];\n  }\n\n#ifdef DEBUG\n  RCTSetLogFunction(RCTDefaultLogFunction);\n#endif\n\n  XCTAssertNil(redboxError, @\"RedBox error: %@\", redboxError);\n  XCTAssertTrue(foundElement, @\"Couldn't find element with text '%@' in %d seconds\", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);\n}\n\n@end\n"
  },
  {
    "path": "ios/Podfile",
    "content": "# Resolve react_native_pods.rb with node to allow for hoisting\nrequire Pod::Executable.execute_command('node', ['-p',\n  'require.resolve(\n    \"react-native/scripts/react_native_pods.rb\",\n    {paths: [process.argv[1]]},\n  )', __dir__]).strip\n\nplatform :ios, min_ios_version_supported\nprepare_react_native_project!\n\n# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.\n# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded\n#\n# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`\n# ```js\n# module.exports = {\n#   dependencies: {\n#     ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),\n# ```\nflipper_config = ENV['NO_FLIPPER'] == \"1\" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled\n\nlinkage = ENV['USE_FRAMEWORKS']\nif linkage != nil\n  Pod::UI.puts \"Configuring Pod with #{linkage}ally linked Frameworks\".green\n  use_frameworks! :linkage => linkage.to_sym\nend\n\ntarget 'LxMusicMobile' do\n  config = use_native_modules!\n\n  use_react_native!(\n    :path => config[:reactNativePath],\n    # Enables Flipper.\n    #\n    # Note that if you have use_frameworks! enabled, Flipper will not work and\n    # you should disable the next line.\n    :flipper_configuration => flipper_config,\n    # An absolute path to your application root.\n    :app_path => \"#{Pod::Config.instance.installation_root}/..\"\n  )\n\n  target 'LxMusicMobileTests' do\n    inherit! :complete\n    # Pods for testing\n  end\n\n  post_install do |installer|\n    # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202\n    react_native_post_install(\n      installer,\n      config[:reactNativePath],\n      :mac_catalyst_enabled => false\n    )\n  end\nend\n"
  },
  {
    "path": "metro.config.js",
    "content": "const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config')\n\n/**\n * Metro configuration\n * https://facebook.github.io/metro/docs/configuration\n *\n * @type {import('metro-config').MetroConfig}\n */\nconst config = {\n  resolver: {\n    extraNodeModules: {\n      // crypto: require.resolve('react-native-quick-crypto'),\n      // stream: require.resolve('stream-browserify'),\n      buffer: require.resolve('@craftzdog/react-native-buffer'),\n    },\n  },\n}\n\nmodule.exports = mergeConfig(getDefaultConfig(__dirname), config)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"lx-music-mobile\",\n  \"version\": \"1.8.1\",\n  \"versionCode\": 73,\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"react-native run-android --active-arch-only\",\n    \"ios\": \"react-native run-ios\",\n    \"start\": \"react-native start\",\n    \"sc\": \"react-native start --reset-cache\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx\",\n    \"lint:fix\": \"eslint . --ext .js,.jsx,.ts,.tsx --fix\",\n    \"rd\": \"react-devtools\",\n    \"menu\": \"adb shell input keyevent 82\",\n    \"bundle-android\": \"react-native bundle --platform android --dev true --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res\",\n    \"build-test\": \"react-native bundle --platform android --dev true --entry-file index.js --bundle-output index.android.bundle --assets-dest res\",\n    \"pack:android:debug\": \"./gradlew assembleDebug\",\n    \"pack\": \"npm run pack:android\",\n    \"pack:android\": \"cd android && gradlew.bat assembleRelease\",\n    \"clear\": \"cd android && gradlew.bat clean\",\n    \"clear:full\": \"git clean -fdx -e android/keystore.properties -e android/app/*.keystore\",\n    \"build:theme\": \"node src/theme/themes/createThemes.js\",\n    \"publish\": \"node publish\"\n  },\n  \"engines\": {\n    \"node\": \">= 18\",\n    \"npm\": \">= 8.5.2\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/lyswhut/lx-music-mobile.git\"\n  },\n  \"keywords\": [\n    \"music-player\",\n    \"react-native-app\"\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-mobile/issues\"\n  },\n  \"homepage\": \"https://github.com/lyswhut/lx-music-mobile#readme\",\n  \"dependencies\": {\n    \"@craftzdog/react-native-buffer\": \"^6.1.1\",\n    \"@d11/react-native-fast-image\": \"^8.13.0\",\n    \"@react-native-async-storage/async-storage\": \"^2.1.2\",\n    \"@react-native-clipboard/clipboard\": \"^1.16.3\",\n    \"@react-native-community/slider\": \"^4.5.7\",\n    \"he\": \"^1.2.0\",\n    \"iconv-lite\": \"^0.7.2\",\n    \"lrc-file-parser\": \"^2.4.1\",\n    \"message2call\": \"^0.1.3\",\n    \"pako\": \"^2.1.0\",\n    \"react\": \"18.2.0\",\n    \"react-native\": \"0.73.11\",\n    \"react-native-background-timer\": \"github:lyswhut/react-native-background-timer#55ecaa80880e9cec1fff81f3ce10e6250ab3c40c\",\n    \"react-native-exception-handler\": \"^2.10.10\",\n    \"react-native-file-system\": \"github:lyswhut/react-native-file-system#fcb0e6f55af684ac60598c720c77cfe0502f2c65\",\n    \"react-native-fs\": \"^2.20.0\",\n    \"react-native-local-media-metadata\": \"github:lyswhut/react-native-local-media-metadata#1b5be310d112afce0df598d3e9c7521c3311049c\",\n    \"react-native-navigation\": \"7.39.2\",\n    \"react-native-pager-view\": \"6.7.1\",\n    \"react-native-quick-base64\": \"^2.2.2\",\n    \"react-native-quick-md5\": \"^3.0.9\",\n    \"react-native-track-player\": \"github:lyswhut/react-native-track-player#d4a062f76ba3d0c408e0f0827b721cc4bf46dffd\",\n    \"react-native-vector-icons\": \"^10.2.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.29.0\",\n    \"@babel/eslint-parser\": \"^7.28.6\",\n    \"@babel/plugin-proposal-export-namespace-from\": \"^7.18.9\",\n    \"@babel/preset-env\": \"^7.29.0\",\n    \"@babel/runtime\": \"^7.28.6\",\n    \"@react-native/babel-preset\": \"^0.74.89\",\n    \"@react-native/metro-config\": \"^0.74.89\",\n    \"@react-native/typescript-config\": \"^0.74.89\",\n    \"@tsconfig/react-native\": \"^3.0.9\",\n    \"@types/he\": \"^1.2.3\",\n    \"@types/react\": \"^18.3.27\",\n    \"@types/react-native\": \"^0.72.8\",\n    \"@types/react-native-background-timer\": \"^2.0.2\",\n    \"@types/react-native-vector-icons\": \"^6.4.18\",\n    \"babel-plugin-module-resolver\": \"^5.0.2\",\n    \"changelog-parser\": \"^3.0.1\",\n    \"eslint-config-standard\": \"^17.1.0\",\n    \"eslint-config-standard-with-typescript\": \"^43.0.1\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^5.2.0\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"overrides\": {\n    \"@react-native-community/cli-platform-android\": {\n      \"fast-xml-parser\": \"^5.3.4\"\n    },\n    \"@react-native-community/cli-platform-ios\": {\n      \"fast-xml-parser\": \"^5.3.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "publish/changeLog.md",
    "content": "我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\n\n### 优化\n\n- 启动APP时将最后播放的歌曲信息初始化到播放器（显示到系统通知栏，可用媒体键控制播放）\n- 优化图片组件性能\n- 优化版本检查提示，使用 toast 显示未知版本信息（#946）\n\n### 修复\n\n- 修复在某些Android设备上字体显示异常的问题（#926, #718）\n"
  },
  {
    "path": "publish/index.js",
    "content": "// const fs = require('fs')\n// const path = require('path')\nconst chalk = require('chalk')\n// const 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  await updateVersionFile(process.argv.slice(2)[0])\n  console.log(chalk.green('日志更新完成~'))\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  // } 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/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 => {\n    reject(err)\n  })\n  const wr = fs.createWriteStream(target)\n  wr.on('error', err => {\n    reject(err)\n  })\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/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\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(/(## [?0.1.1]?)/, log + '\\n$1'), 'utf-8')\n}\n\n\nmodule.exports = async newVerNum => {\n  if (!newVerNum) newVerNum = pkg.version\n  const newMDChangeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8')\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  pkg.versionCode = pkg.versionCode + 1\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  }\n}\n\n"
  },
  {
    "path": "publish/version.json",
    "content": "{\"version\":\"1.8.1\",\"desc\":\"我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\\n\\n优化\\n- 启动APP时将最后播放的歌曲信息初始化到播放器（显示到系统通知栏，可用媒体键控制播放）\\n- 优化图片组件性能\\n- 优化版本检查提示，使用 toast 显示未知版本信息（#946）\\n\\n修复\\n- 修复在某些Android设备上字体显示异常的问题（#926, #718）\",\"history\":[{\"version\":\"1.8.0\",\"desc\":\"我们很高兴地宣布新项目 Any Listen 的桌面版已发布，目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能，更多功能仍在积极开发中，桌面版与Web版将同步更新。\\n对于有播放本地音乐或播放服务器上音乐需求的人可以试试，若遇到任何问题可以发 issue 反馈。\\n\\n新增\\n- 新增「设置 → 基本设置 → 允许通过底栏进度条调整播放进度」设置（#778）\\n   *默认为原来的启用状态，若你觉得在进入播放详情页时会误触调整进度，则可以将其关闭*\\n- 新增 Any Listen 歌词标签数据读取与播放\\n- 编辑本地歌曲的标签信息时，添加 Any Listen 歌词标签数据生成（用于支持已下载歌曲的歌词在桌面版逐字播放）\\n\\n优化\\n- 设置-备份与恢复 导入列表数据时，增加二次确认提示（#809）\\n\\n修复\\n- 修复切歌时，偶现歌词不播放的问题\\n- 修复TX源搜索失败 (@Folltoshe)\\n- 修复MG源歌单加载失败（#913）\\n- 修复MG源评论加载失败（#914）\\n\\n其他\\n- 更新 Media3 到 v1.8.0\"},{\"version\":\"1.7.1\",\"desc\":\"修复\\n- 修复 tx 歌单搜索名字、描述出现乱码的问题\\n- 修复解析某些本地歌词文件时出现乱码的问题（#694）\\n- 修复 Android 5.1 下添加本地歌曲时报错的问题（#730）\\n- 修复 kw 歌单详情出现打开失败的问题\\n- 修复 kg 热门评论无法获取的问题\\n- 修复 kg 歌单打开失败的问题（thanks @Folltoshe）\\n\\n优化\\n- 优化软件文案编排（#701, #703, @3gf8jv4dv）\\n\\n变更\\n- 我的列表-歌曲菜单中的 歌曲换源 功能从之前的类似软连接的形式改成替换歌曲的形式，也就是说，现在该功能相当于快速在线搜索歌曲，确认换源后将自动将原来的歌曲删除再将选择的歌曲插入被删除歌曲的位置。\\n\\n其他\\n- 更新项目文档（@3gf8jv4dv）\"},{\"version\":\"1.7.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*为了防止歌曲缓存被第三方软件当做垃圾意外清理，歌曲缓存不再存储到缓存目录。若想清理缓存，需去「设置 → 其他 → 资源缓存管理」清理。*\\n\\n*更新到该版本后首次播放歌曲时，会将之前的歌曲缓存迁移到新位置，需要等待的时间取决于你已缓存资源的大小。*\\n\\n*感谢 @3gf8jv4dv 对 LX 系列项目翻译、文档等文案的大幅修订优化。*\\n\\n新增\\n- 新增蓝牙歌词支持，可以通过「设置 → 播放设置 → 显示蓝牙歌词」启用（#615）\\n- 新增繁体中文语言（#659, @3gf8jv4dv）\\n- 将 LX Music 设置为「音乐应用」类，允许将 LX Music 设置为系统默认音乐播放器\\n- 支持在程序外使用 LX Music 打开常见音乐文件及 `.js`、`.json`、`.lxmc` 等文件\\n\\n优化\\n- 防止歌曲缓存被第三方软件当做垃圾意外清理\\n- 优化正常播放结束时的下一首歌曲播放衔接度，在歌曲即将结束播放时将预获取下一首歌曲的播放链接，减少自动切歌时的等待时间\\n- 优化歌曲换源机制，提升换源正确率\\n- 首次使用的提示窗口可以通过点击背景或者返回键关闭（#577）\\n- 上移 Toast 位置避免遮挡播放模式图标（#603, @sibojia）\\n- 优化简体中文文案编排，大幅修订英语文案编排（#658, #660 等, @3gf8jv4dv）\\n- 优化应用图标质量，在高版本系统上使用矢量图标\\n\\n修复\\n- 修复导出文件到范围存储类型的目录时，扩展名丢失的问题\\n- 修复切换列表播放歌曲时可能会出现播放的歌曲不对应的问题\\n- 修复内置列表名称硬编码和语言切换显示的问题（#662）\\n- 修复某些情况下进播放详情页时，详情页不显示或应用界面无响应的问题\\n- 修复低版本 Android 在某些情况下对 Emoji 字符编码的处理问题\\n\\n变更\\n- 歌曲缓存不再存储到缓存目录\\n- 不再长期缓存换源歌曲信息\\n\\n其他\\n- 更新 Readme 文档，优化文案编排（#651, Thanks @3gf8jv4dv）\\n- 更新 Issue 模板（#652, @3gf8jv4dv）\\n- 更新项目文档（@3gf8jv4dv）\\n- 更新 React Native 到 v0.73.11\"},{\"version\":\"1.6.0\",\"desc\":\"新增\\n- 新增 我的列表-歌曲右击菜单-歌曲换源 功能，换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲，该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题\\n- 新增 Scheme URL 调用支持，调用传参格式与PC端一致，详情看文档说明： https://lyswhut.github.io/lx-music-doc/mobile/scheme-url\"},{\"version\":\"1.5.0\",\"desc\":\"我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\\n\\n新增\\n- 新增重复歌曲列表，可以方便移除我的列表中的重复歌曲，此列表会列出目标列表里歌曲名相同的歌曲，可在“我的列表”里的列表名菜单中使用（注：该功能与PC端的区别是可以点击歌曲名多选删除）\\n- 新增打开当前歌曲详情页菜单，可以在歌曲菜单中使用\\n\\n修复\\n- 修复潜在桌面歌词导致的崩溃问题\\n\\n其他\\n- 更新 React native 到 v0.73.9\\n- 更新 exoplayer 到 v1.4.0\"},{\"version\":\"1.4.2\",\"desc\":\"我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\\n\\n修复\\n- 修复数据存储管理在移除数据时可能出现移除失败的问题\"},{\"version\":\"1.4.1\",\"desc\":\"我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\\n\\n修复\\n- 修复播放详情页歌词滚动问题（#518）\"},{\"version\":\"1.4.0\",\"desc\":\"我们发布了关于 LX Music 项目发展调整与新项目计划的说明，\\n详情看： https://github.com/lyswhut/lx-music-desktop/issues/1912\\n\\n新增\\n- 新增 设置-基本设置-启动后打开播放详情界面 设置，默认关闭（#502 @mingcc7）\\n\\n修复\\n- 修复重复的数据初始化调用\\n- 修复导入歌单时可能会导致歌单数据存储异常的问题（#500）\\n\\n变更\\n- 设置-播放设置-优先播放320k音质选项改为“优先播放的音质”，允许选择更高优先播放的音质，如果歌曲及音源支持的话（#487）\\n\\n其他\\n- 更新 React native 到 v0.73.8\"},{\"version\":\"1.3.0\",\"desc\":\"新增\\n- 新增棕色主题“泥牛入海”\\n- 新增设置-基本设置-总是保留状态栏高度设置，如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况，可以启用该设置以始终为系统状态栏保留空间\\n- 新增在线自定义源导入功能，允许通过http/https链接导入自定义源\\n\\n优化\\n- 不再丢弃kg源逐行歌词（@helloplhm-qwq）\\n- 支持kw源排行榜显示大小（revert @Folltoshe #1460）\\n- 优化本地歌曲换源匹配机制\\n\\n修复\\n- 修复mg歌词在某些情况下获取失败的问题\\n- 修复mg歌单搜索（@helloplhm-qwq）\\n- 修复kg最新评论无法获取的问题（@helloplhm-qwq）\\n\\n其他\\n- 更新 React native 到 v0.73.6\"},{\"version\":\"1.2.0\",\"desc\":\"提前祝大家新年快乐！\\n\\n新增\\n- 新增自定义源（实验性功能），调用方式与PC端一致，但需要注意的是，移动端自定义源的环境与PC端不同，某些环境API不可用，详情看自定义说明文档\\n- 新增长按收藏列表名自动跳转列表顶部的功能\\n- 新增实验性的添加本地歌曲到我的收藏支持，与PC端类似，在我的收藏的列表菜单中选择歌曲目录，将添加所选目录下的所有歌曲，目前支持mp3/flac/ogg/wav等格式\\n- 新增歌曲标签编辑功能，允许编辑本地源且文件存在的歌曲标签信息\\n- 新增动态背景，启用后将使用当前播放歌曲封面做APP背景，默认关闭，可到设置-主题设置启用\\n- 新增APP全局字体阴影，默认关闭，可到设置-主题设置启用\\n- 新增启用竖屏首页横向滚动设置，默认开启（原来的行为），如果你不想要竖屏的首页左右滑动则可以关闭此设置（#397）\\n- 新增“使用系统文件选择器”设置，默认启用，启用该选项后，导入备份文件、自定义源等操作将不需要申请存储权限，但可能在某些系统上不可用\\n- 播放详情页新增桌面歌词显示/隐藏切换按钮，长按可切换歌词锁定状态\\n- 我的列表菜单列表新增“新建列表”菜单\\n- 我的列表菜单列表新增“排序歌曲”菜单，可以排序所选列表内的歌曲，排序功能与PC一致\\n- 添加 墨·状态栏特别版（版本号包含`sl`）的 release 构建\\n\\n优化\\n- 添加是否忽略电池优化检查，用于提醒用户添加白名单，确保APP后台播放稳定性\\n- 在设置界面返回时，不再直接返回桌面，将回到进入设置界面前的界面，在非设置界面返回时才会返回桌面\\n- 更新播放栏进度条样式，进度条允许拖动调整进度\\n- 优化播放详情页歌曲封面、控制按钮对各尺寸屏幕的适配，修改横屏下的控制栏按钮布局\\n- 优化横竖屏界面的展示判断，现在趋于方屏的屏幕按竖屏的方式显示，横屏下的播放栏添加上一曲切歌按钮\\n- 添加对wy源某些歌曲有问题的歌词进行修复（#370）\\n- 文件选择器允许选择外置存储设备上的路径，添加SD卡、USB存储等外置存储设备的读写支持\\n- 图片显示改用第三方的图片组件，支持gif类型的图片显示，尝试解决某些设备上图片过多导致的应用崩溃问题\\n- 歌曲评论内容过长时自动折叠，需手动展开\\n- 改进本地音乐在线信息的匹配机制\\n- 移除播放服务唤醒锁，解决APP在空闲时仍然处于唤醒状态的问题\\n- 添加创建同名列表时的二次确认\\n\\n修复\\n- 修复主题背景覆盖不全的问题\\n- 修复清理缓存后查看日志时会导致APP崩溃的问题\\n- 修复临时列表变更会意外触发同步的问题\\n\\n变更\\n- 在更低版本的安卓上启用跟随系统亮暗主题功能（#317）\\n- 由于歌曲评论的图片太大占用较多资源，评论图片不再直接加载，需要点击图片区域后再加载\\n- 导入文件（歌单备份、自定义源文件等）默认不再需要设备存储权限，但如果这导致在你的设备上无法选择文件，则可以关闭基本设置的“使用系统文件选择器”设置，回退到原来的文件选择方式\\n\\n其他\\n- 移除所有内置源，由于收到腾讯投诉要求停止提供软件内置的连接到他们平台的在线播放及下载服务，所以从即日（2023年10月18日）起LX本身不再提供上述服务\\n- 更新许可协议的排版，使其看起来更加清晰明了，更新数据来源原理说明\\n- 更新 React native 到 v0.73.3\\n- 核心播放器从 ExoPlayer 迁移到 media3 v1.2.1\"},{\"version\":\"1.1.1\",\"desc\":\"落雪提前祝大家中秋快乐~🥮😘！\\n\\n优化\\n- 通过歌曲菜单添加不喜欢歌曲时需要二次确认防止手抖\\n- 减慢歌词详情页歌词滚动速度\\n- 更改应用窗口大小获取方式，尝试解决在某些设备上的背景、弹出菜单显示问题\\n- 优化同步功能错误消息提示，因同步服务版本不匹配导致的连接失败现在将区分提示\\n\\n修复\\n- 修复横屏状态下的歌词滚动位置计算问题\\n- 修复切歌时歌词激活行的重置问题\\n- 修复更新翻译歌词、罗马音歌词设置后需重启应用才生效的问题，现在更新设置后会立即生效\\n\\n其他\\n- 更新 React native 到 v0.72.5\"},{\"version\":\"1.1.0\",\"desc\":\"目前本项目的原始发布地址只有 **GitHub** 及 **蓝奏网盘** （在设置-关于有说明），其他渠道均为第三方转载发布，可信度请自行鉴别。\\n\\n本项目无微信公众号之类的官方账号，也未在小米、华为、vivo等应用商店发布应用，商店内的“LX Music”、“洛雪音乐”相关的应用全部属于假冒应用，谨防被骗。\\n\\n本软件完全无广告且无引流（如需要加群、关注公众号之类才能使用或者升级）的行为，若你使用过程中遇到广告或者引流的信息，则表明你当前运行的软件是第三方修改版。\\n\\n若在升级新版本时提示签名不一致，则表明你手机上的旧版本或者将要安装的新版本中有一方是第三方修改版。\\n\\n若在升级新版本时提示无法降级安装，则表明你使用的是universal（通用）版安装包（安装包大小20M+），尝试使用arm64-v8a版安装包或者到GitHub下载其他版本安装包。\\n\\n该版本针对一加、OPPO、Pixel无法播放歌曲（提示音频加载出错，5 秒后切换下一首）或者无法完整播放歌曲的问题做了处理，但如果你使用该版本后问题依然存在，临时的解决方案是去设置-播放设置关闭“音频卸载”选项后完全重启应用\\n\\n不兼容性变更\\n该版本修改了同步协议逻辑，同步功能至少需要PC端v2.4.0或移动端v1.1.0或同步服务v2.0.0版本才能连接使用\\n\\n新增\\n- 新增列表设置-是否显示歌曲专辑名，默认关闭\\n- 新增列表设置-是否显示歌曲时长，默认开启\\n- 新增是否允许通过歌词调整播放进度功能，默认关闭，可到播放详情页右上角设置开启\\n- 新增“不喜欢歌曲”功能，可以在我的列表或者在线列表内歌曲的右击菜单使用，还可以去“设置-其他”手动编辑不喜欢规则，注：“上一曲”、“下一曲”功能将跳过符合“不喜欢歌曲”规则的歌曲，但你仍可以手动播放这些歌曲\\n- 新增同步功能对“不喜欢歌曲”列表的同步\\n- 新增设置-播放设置-是否启用音频卸载，该设置之前默认是启用的，现在添加开关允许将其关闭，若出现播放器问题可尝试将其关闭\\n- 新增设置-播放设置-自动清空已播放列表选项，默认关闭\\n\\n优化\\n- 优化歌单列表歌单封面大小计算方式\\n- 调整竖屏下的排行榜布局\\n- 调整歌曲列表信息布局\\n- 调整横屏下的歌曲列表为两列\\n- 调整桌面歌词主题配色，增强歌词字体阴影（#276）\\n- 优化数据传输逻辑，列表同步指令使用队列机制，保证列表同步操作的顺序\\n- 暂停播放时播放详情歌词页不要自动滚动歌词回播放位置\\n- 播放详情页歌词添加延迟滚动及着色动画\\n- 优化息屏下的逻辑处理，尽量减少电量消耗\\n\\n修复\\n- 修复wy歌单分类切换无效的问题\\n- 修复因插入数字类型的ID导致其意外在末尾追加 .0 导致列表数据异常的问题，同时也可能导致同步数据丢失的问题（此问题会影响PC端，要完全修复这个问题还需要同时将PC端、同步服务更新到最新版本）\\n- 修复在线列表、我的列表内的歌曲批量操作后，没有自动取消选择的问题\\n- 修复tx热门评论昵称被错误切割的问题 (By: @helloplhm-qwq, @Folltoshe)\\n- 修复wy源热搜词失效的问题（@Folltoshe）\\n- 修复mg歌单搜索歌单播放数量显示问题\\n- 修复搜索提示功能失效的问题（@Folltoshe）\\n- 修复潜在导致列表数据不同步的问题\\n- 修复kg无评论时的加载处理问题\\n- 修复顺序播放时播放完列表的最后一首歌播放按钮状态没有更新的问题（#300）\\n\\n变更\\n- 随机模式下，通过点击与播放列表相同的列表切歌时，将不再清空已播放列表，即已播放的歌曲不再重新参与随机，若想恢复之前的行为可以去设置-播放设置启用清空已播放列表选项\\n\\n其他\\n- 更新 React native 到 v0.72.4\"},{\"version\":\"1.0.6\",\"desc\":\"修复\\n- 修复wy歌单分类切换无效的问题\"},{\"version\":\"1.0.5\",\"desc\":\"优化\\n- 增加kg歌单歌曲flac24bit显示（@helloplhm-qwq）\\n- 增加tx源热门评论图片显示（@Folltoshe）\\n- 支持wy热门评论翻页\\n- 微调排行榜列表宽度及字体大小\\n\\n修复\\n- 修复wy我喜欢列表使用token的方式导入，现在移动端可以使用token的方式导入我喜欢列表的音乐了，这意味着从PC端同步过来的歌单也可以在移动端上更新\\n- 修复在线列表的多选问题\\n- 修复mg搜索不显示时长的问题（@Folltoshe）\\n- 修复mg评论加载失败的问题（@Folltoshe）\\n- 修复在Android 5.1下报错的问题\\n- 修复对存在错误时间标签的歌词的解析\\n- 修复聚合搜索时未显示源名称的问题\\n- 修复更改音源的列表歌曲颜色的实时更新问题\\n\\n其他\\n- 更新kg、tx、wy等平台排行榜列表\\n- 更新react native到v0.71.7\"},{\"version\":\"1.0.4\",\"desc\":\"新增\\n- 隐藏黑色主题背景设置，默认关闭，可以去设置-主题设置更改\\n\\n优化\\n- 添加歌单分类、排行榜激活指示器\\n- 调整设置界面竖屏下的UI布局\\n\\n修复\\n- 修复歌单排序列表滚动重置问题\\n- 修复搜索提示列表的显示时机问题\\n- 就放tx源歌词获取失败的问题\\n- 修复将播放速率调整为0.6后，再次打开设置面板将会导致app崩溃的问题\\n- 修复播放详情页设置面板当前音量显示格式问题\\n\\n其他\\n- 升级 React Native 到 v0.71.5\"},{\"version\":\"1.0.3\",\"desc\":\"修复\\n- 修复歌单详情页内歌曲最多只加载30首的问题\"},{\"version\":\"1.0.2\",\"desc\":\"优化\\n- 竖屏下的首页允许滑动切换页面（恢复v0.x.x的切页操作）\\n- 优化更新语言、主题设置时的流畅度\\n\\n其他\\n- 启用新架构\"},{\"version\":\"1.0.1\",\"desc\":\"修复\\n- 修复在线列表翻页问题\"},{\"version\":\"1.0.0\",\"desc\":\"从v1.0.0起，我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme)，如果你有服务器，可以将其部署到服务器上作为私人多端同步服务使用，详情看该项目说明\\n\\n由于该版本涉及旧版数据迁移，建议更新前先到设置-备份与恢复备份歌单\\n\\n不兼容性变更说明\\n- 同步功能，该功能不支持与PC端v2.2.0之前的版本使用\\n\\n新增\\n- 新增聚合搜索，注：由于这个方式需要对各个源的结果进行排序，所以需要以“歌曲名 歌手”的顺序输入（例如：突然的自我 伍佰），否则排序后的结果可能不是你想要的\\n- 新增歌单搜索功能\\n- 新增热门搜索显示，默认关闭，需要到设置-搜索设置开启\\n- 新增搜索历史记录，默认关闭，需要到设置-搜索设置开启\\n- 启动软件时自动回到上次的界面，例如上次退出软件时在我的收藏，下次启动软件时会自动进入我的收藏\\n- 新增PC端所拥有的内置皮肤\\n- 新增界面字体大小设置\\n- 新增播放器音量大小设置，可以去播放详情页-播放器设置-音量大小更改\\n- 新增播放器播放速率设置，可以去播放详情页-播放器设置-播放速率更改\\n- 新增播放详情页歌词对齐方式设置，可以去播放详情页-播放器设置-歌词对齐方式更改\\n- 新增是否在左侧导航栏显示返回桌面按钮设置，默认关闭，可以去设置-基本设置-是否显示返回桌面按钮开启\\n- 新增是否在左侧导航栏显示退出应用按钮设置，默认关闭，可以去设置-基本设置-是否显示退出应用按钮开启\\n- 支持wy源flac hires歌曲类型的显示\\n- 添加kg源评论图片展示（@helloplhm-qwq）\\n- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示（@helloplhm-qwq, @Folltoshe）\\n\\n优化（界面/交互/功能）\\n- 调整了首页的界面布局\\n- 优化大屏幕下的字体大小及界面布局显示\\n- 支持wy源flac hires歌曲类型的显示\\n- 优化列表数据导入导出的性能，现在进行这些操作应该可以一下子完成且不会再冻结UI了\\n- 支持kg源搜索列表flac hires歌曲类型的显示（@helloplhm-qwq）\\n\\n优化（程序）\\n- 优化程序启动性能，优化与程序交互的流畅度\\n- 重构整个程序，重新梳理了程序逻辑，使其更容易扩展及维护，将大部分代码从JavaScript迁移到TypeScript\\n- 重写配置管理、列表管理功能，使其与PC端同步，更容易复用PC端的代码\\n\\n修复\\n- 修复使用酷狗码无法打开某些类型的歌单的问题\\n- 修复tx源某些歌单无法打开的问题\\n\\n变更\\n- 原来播放详情页的歌词字体大小设置改为播放器设置\\n\\n其他\\n- 升级React Native到v0.71.4\"},{\"version\":\"0.15.5\",\"desc\":\"修复\\n- 修复导入PC端v2列表文件歌曲信息转换丢失的问题\\n- 修复上面问题导致的tx源评论加载失败的问题\"},{\"version\":\"0.15.4\",\"desc\":\"修复\\n- 修复播放详情页歌词翻译、罗马音歌词匹配问题\"},{\"version\":\"0.15.3\",\"desc\":\"修复\\n- 修复鸿蒙系统下的崩溃问题\"},{\"version\":\"0.15.2\",\"desc\":\"修复\\n- 修复潜在的歌词解析导致应用崩溃问题\"},{\"version\":\"0.15.1\",\"desc\":\"修复\\n- 修复某些歌曲的桌面歌词翻译或罗马音没有显示的问题\\n- 修复kg某些歌单链接无法打开的问题\"},{\"version\":\"0.15.0\",\"desc\":\"新增\\n- 支持导入PC端v2版本的列表数据\\n- 添加kg源罗马音歌词的支持\\n- 支持打开波点音乐歌单（需在酷我源打开）\\n\\n修复\\n- 支持单行多时间标签歌词解析，修复某些歌词会出现时间标签的问题\\n- 修复某些类型的kg歌单无法导入的问题\\n- 修复异常歌单、歌曲数据导致的崩溃问题（#157）\\n\\n其他\\n- 升级react-native到 v0.68.5\"},{\"version\":\"0.14.3\",\"desc\":\"修复\\n- 修复因音源的域名到期导致的音源失效的问题\"},{\"version\":\"0.14.2\",\"desc\":\"优化\\n- 为tx、kw源添加 Flac 24bit 音质显示，注：由于之前没有记录此音质，所以之前收藏的歌曲信息中不包含它\\n\\n修复\\n- 修复排行榜在旋转屏幕后，选中的榜单被重置回第一个的问题\\n- 修复企鹅音乐搜索失效的问题\"},{\"version\":\"0.14.1\",\"desc\":\"优化\\n- 添加“弹出键盘时自动隐藏播放栏”设置，默认启用（原来的行为），若在某些设备上播放栏无法显示时则可以关闭此设置\\n- 优化切歌时桌面歌词的切换动画显示\\n- 暂停播放时自动隐藏桌面歌词\\n- 在我的列表-列表名左侧添加了一个图标，以表示此处可以点击切换列表\\n\\n修复\\n- 修复tx源搜索失效的问题\"},{\"version\":\"0.14.0\",\"desc\":\"新增\\n- 新增设置-桌面歌词-单行歌词设置，默认关闭，启用后只显示一行歌词，超出窗口宽度自动滚动到末尾\\n- 新增设置-桌面歌词-显示歌词切换动画，默认启用，如果你觉得切换动画影响视觉可以将其关闭\\n- 新增设置-基本设置-启动后自动播放音乐，默认关闭\\n\\n优化\\n- 支持mg源的歌词翻译（之前添加的歌曲需要去设置清空缓存才会刷新歌词）\\n- 添加歌曲列表更新操作的二次确认\\n- 添加导入文件错误时的指引提示\\n\\n修复\\n- 修复桌面歌词转繁体设置不立即生效的问题\\n- 修复搜索、歌单、排行榜列表可能在切换新内容后出现上次列表内容的残留问题（#118）\\n- 修复在某些系统上播放音乐会导致应用崩溃的问题（#129）\\n- 修复停止播放后的播放器状态清理问题\\n\\n文档\\n移动版文档已迁移到：<https://lyswhut.github.io/lx-music-doc/mobile>\"},{\"version\":\"0.13.0\",\"desc\":\"从这个版本起，你可以将桌面歌词拖动到状态栏上，然后将歌词字体调小后配合新增的歌词窗口宽度、行数设置，模拟出类似状态栏歌词的效果。\\n\\n如果你的设备装有Xposed框架，可以使用状态栏版（详情看GitHub置顶issue），它通过调用第三方Xposed模块【墨•状态栏歌词】的API支持来状态栏歌词（感谢@ftevxk）。\\n但考虑到要依赖第三方应用，并且是Xposed模块，预计用的人会比较少，所以暂不考虑将此特性包含在正式版中。\\n\\n新增\\n- 新增设置-播放设置-显示歌词罗马音，默认关闭，注：目前只有网易源能获取到罗马音歌词（得益于 Binaryify/NeteaseCloudMusicApi/pull/1523），如果你知道其他源的歌词罗马音获取方式，欢迎PR或开issue交流！\\n- 新增黑、白桌面歌词主题\\n- 桌面歌词新增窗口宽度百分比、最大歌词行数调整设置，允许将歌词拖动到刘海屏状态栏上。提示：有了这组功能你就可以模拟状态栏歌词了\\n- 新增设置-播放设置-将播放的歌词转繁体功能（#114）\\n\\n优化\\n- 允许桌面歌词拖动到状态栏上（感谢@ftevxk）\\n- 允许选择更新日志弹窗里的文本内容\\n- 桌面歌词的最大字体大小允许调整到500（#107）\\n\\n修复\\n- 修复潜在的桌面歌词导致应用崩溃问题\\n\\n文档\\n- 将歌曲添加“稍后播放”后，它们会被放在一个优先级最高的特殊队列中，点击“下一曲”时会消耗该队列中的歌曲，并且无法通过“上一曲”功能播放该队列的上一首歌曲\\n- 在切歌时若不是通过“上一曲”、“下一曲”功能切歌（例如直接点击“排行榜列表”、“我的列表”中的歌曲切歌），“稍后播放”队列将会被清空\\n\\n其他\\n- 升级React native到v0.68.2\"},{\"version\":\"0.12.0\",\"desc\":\"新增\\n- 为搜索、歌单、排行榜的歌曲菜单添加分享“分享歌曲”按钮\\n- 新增设置-基本设置-分享设置，它用于控制歌曲菜单的分享行为，默认使用系统分享\\n- 新增是否在通知栏显示歌曲图片设置，默认开启（原来的行为）\\n- 新增黑色皮肤“黑灯瞎火”\\n- 新增设置-基本设置-主题颜色-跟随系统亮、暗模式切换主题设置，注：此设置需要android 10或ios 13及以上的版本才支持\\n\\n优化\\n- 现在即使切歌模式处于单曲循环、顺序播放、禁用时，手动切歌将会按照列表循环的规则处理（#69）\\n- 添加定时退出计时结束后的提示\\n\\n修复\\n- 修复wy源搜索某些歌曲时第一页之后的歌曲无法加载的问题\\n- 每次启动时过滤无效的歌曲\\n- 修复换源失败时的处理问题\\n- 修复非循环模式下播放结束后的状态显示问题及无法重新播放的问题（#104）\\n- 修复定时退出可能导致崩溃的问题\\n- 修复播放详情页歌词界面在把应用切到后台再切回来会导致屏幕常亮失效的问题\\n\\n变更\\n- 歌曲菜单的“复制歌曲名”改为“分享歌曲”，点击后可以选择第三方应用分享歌曲详情页链接\\n- 已存在目录列表的歌曲再次添加时将不会变成移除\\n\\n其他\\n- 升级react-native到 v0.68.1\"},{\"version\":\"0.11.1\",\"desc\":\"修复\\n- 修复播放栏在某些设备不显示的问题\"},{\"version\":\"0.11.0\",\"desc\":\"新增\\n- 新增“点击列表里的歌曲时自动切换到当前列表播放”设置，此功能仅对歌单、排行榜有效，默认关闭\\n- 添加试听接口，这是测试接口、临时接口都不可用时最后的选择...\\n\\n优化\\n- 过滤tx源某些不支持播放的歌曲，解决播放此类内容会导致意外的问题\\n- 备份与恢复兼容单个列表文件的导入\\n- 添加通知权限的检查提醒，点击“不再提示”后，将会在设置-清空缓存后才会恢复提示\\n\\n修复\\n- 修复Android 12下的桌面歌词锁定后还是无法在应用外点击歌词后面下面的内容的问题\\n\\n其他\\n- 升级React native到v0.67.4\"},{\"version\":\"0.10.3\",\"desc\":\"优化\\n- 优化kw源英文与翻译歌词的匹配\\n\\n修复\\n- 修复桌面歌词播放器会导致应用崩溃的问题\"},{\"version\":\"0.10.2\",\"desc\":\"修复\\n- 修复某些系统下的虚拟导航栏会导致播放栏隐藏的问题（react-native v0.67.x导致的）\\n\\n其他\\n- 降级react-native到 v0.66.4\"},{\"version\":\"0.10.1\",\"desc\":\"优化\\n- 优化通知栏的更新机制，尝试修复魅族的通知栏图片不显示的问题\\n- 我的列表-列表名的右击菜单更新已收藏的在线列表时，将始终重新加载，不再使用缓存，解决在原平台更新歌单后，在LX点击更新可能看到的还是在原平台更新前的歌单的问题\\n\\n修复\\n- 修复tx源无搜索结果的问题\\n- 修复小米等设备下面的手势提示线背景颜色为黑色的问题\\n\\n其他\\n- 升级React native到v0.67.1\"},{\"version\":\"0.10.0\",\"desc\":\"新增\\n- 同步功能新增对列表位置调整的支持（需v1.15.3以上的PC端版本才支持）\\n- 新增播放详情页歌词字体大小调整设置，可在详情页右上角的按钮进行调整\\n- 新增同步服务地址历史列表功能\\n- 横屏播放详情页新增评论入口\\n- 我的列表歌曲三个点的菜单新增复制歌曲名\\n\\n优化\\n- 修改对播放模块的调用，杜绝应用显示正在播放的歌曲与实际播放歌曲不一致的问题（这是播放模块歌曲队列与应用内歌曲队列在某些情况下出现不一致时导致的）\\n- 支持PC端同步功能添加对列表顺序调整的控制，确保手动调整位置后的列表与不同的电脑同步时，列表位置不会被还原\\n- 调整横屏下的导航栏、播放详情页布局，提高屏幕空间利用率并使其更易操作\\n- 调整歌单类别、我的列表弹出层界面\\n- 播放栏移除上一曲按钮，将多出来的空间加给播放、下一曲按钮\\n- 现在点击、长按播放栏歌曲标题也可以进入详情页、定位当前播放歌曲了\\n\\n修复\\n- 修复kw源某些歌曲的歌词提取异常的问题\\n\\n其他\\n- 升级react-native到v0.66.4\"},{\"version\":\"0.9.2\",\"desc\":\"优化\\n- 添加应用初始化出错时的错误捕获输出\\n- 优化歌词自动换源机制\\n\\n修复\\n- 修复因kw源歌词接口停用导致该源歌词获取失败的问题\\n\\n其他\\n- 更新react-native到v0.66.3\\n- 更新Exoplayer到v2.16.0\"},{\"version\":\"0.9.1\",\"desc\":\"修复\\n- 修复删除列表时会导致应用崩溃的问题\\n- 修复原生代码导致的错误日志记录\"},{\"version\":\"0.9.0\",\"desc\":\"新增\\n- 新增歌曲评论显示，可在播放详情页进入。（与PC端一样，目前仅支持显示部分评论）\\n- 新增播放、收藏整个排行榜功能，可长按排行榜名字后在弹出的菜单中操作\\n- 新增单个列表导入/导出功能，可以方便分享歌曲列表，可在点击“我的列表”里的列表名右侧的按钮后弹出的菜单中使用\\n- 新增删除列表前的确认弹窗，防止误删列表\\n\\n优化\\n- 添加更多同步功能的日志记录\\n\\n修复\\n- 修复kg源的歌单链接无法打开的问题\\n- 修复同一首歌的URL、歌词等同时需要换源时的处理问题\\n- 修复在排行榜页面无法时无法通过点击我的列表图标切换到我的列表的问题\\n\\n其他\\n- 更新react-native到v0.66.1\"},{\"version\":\"0.8.3\",\"desc\":\"修复\\n- 修复我的列表搜索无法搜索小括号、中括号等字符，并会导致应用崩溃的问题\\n- 修复使用同步功能同步完成后，列表没有被保存，导致下次再连接同步时被同步新增的歌曲被移除的问题（此问题由v0.8.2的存储切片改造引入的）\\n\\n其他\\n- 更新React native到v0.66.0\"},{\"version\":\"0.8.2\",\"desc\":\"优化\\n- 缓冲进度条颜色\\n- 优化数据存储，若需要存储的数据过大时会将数据切片后存储，现在存储大列表不会导致列表丢失了\\n\\n修复\\n- 修复随机播放模式下在同列表切其他歌曲不会清空已播放列表的问题\\n- 修复歌曲播放出错时的URL刷新问题\"},{\"version\":\"0.8.1\",\"desc\":\"优化\\n- 添加更多错误信息的记录\\n\\n修复\\n- 修复潜在的获取缓存大小报错问题\\n- 修复mg排行榜无法加载的问题\\n- 修复列表导出失败时的提示信息缺失翻译的问题\\n- 修复 Android 11 导入列表时，不显示备份文件的问题\\n- 修复其他应用播放声音时，软件临时暂停播放后通知栏的状态仍显示正在播放的问题\"},{\"version\":\"0.8.0\",\"desc\":\"新增\\n- 添加对通知栏歌曲进度条的支持\\n\\n修复\\n- 修复某些情况下桌面歌词会导致APP崩溃的问题\\n- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题\\n\\n其他\\n- 升级React native到v0.65.1\\n- 升级播放模块`react-native-track-player`到v2版本，优化通知栏歌曲信息显示逻辑\"},{\"version\":\"0.7.1\",\"desc\":\"修复\\n- 修复无法从歌单界面打开网易歌单详情的问题\"},{\"version\":\"0.7.0\",\"desc\":\"如果你喜欢并经常使用洛雪音乐，并想要第一时间尝鲜洛雪的新功能，可以加入测试企鹅群768786588，\\n注意：测试版的功可能会不稳定，打算潜水的勿加。\\n\\n新增\\n- 新增横屏状态下的播放详情页\\n- 新增橙、粉、灰主题色\\n- 新增桌面歌词的字体大小、透明度设置\\n- 新增我的列表内歌曲搜索定位功能\\n\\n调整\\n- 为了与搜索、歌单操作栏位置统一，现将我的列表-收藏的列表操作栏由底部挪到顶部\\n\\n修复\\n- 修复tx源的歌词无法显示的问题\\n- 修复随机播放模式下使用稍后播放功能会导致歌曲单曲循环的问题\\n- 修复某些情况下桌面歌词会导致APP崩溃的问题\"},{\"version\":\"0.6.2\",\"desc\":\"优化\\n- 优化设置界面的输入框输入机制，现在只要键盘收起即可自动保存输入的内容\\n- 添加在启用桌面歌词时对悬浮层权限的检查，这应该可以修复某些设备上点击启用桌面歌词时不显示无权限弹窗也不显示桌面歌词的情况\\n\\n变更\\n- 不再自动聚焦定时退出、调整位置弹窗内的输入框，这应该可以修复某些设备无法在这两个地方弹出键盘的问题\\n\\n修复\\n- 修复启用桌面歌词时的权限提示弹窗会导致应用报错的问题\\n- 修复我的列表无法更新从收藏的排行榜的问题\"},{\"version\":\"0.6.1\",\"desc\":\"修复\\n- 修复随机播放下无法切歌的问题\"},{\"version\":\"0.6.0\",\"desc\":\"新增\\n- 新增局域网同步功能（实验性，首次使用前建议先备份一次列表），此功能需要配合PC端使用，移动端与PC端处在同一个局域网（路由器的网络）下时，可以多端实时同步歌曲列表，使用问题请看\\\"常见问题\\\"。\\n- 新增桌面歌词\\n\\n优化\\n- 优化退出应用的机制，现在在需要退出应用的场景将会完全退出应用\\n\\n修复\\n- 修复某些情况下出现恢复播放信息失败的问题\\n- 修复删除列表中正在播放的歌曲时会自动跳到第一首的问题\\n- 修复因其他应用需要播放声音而暂停播放音乐时歌词不会暂停播放导致恢复播放后歌词与播放进度不一致的问题\"},{\"version\":\"0.5.3\",\"desc\":\"修复\\n- 修复歌曲缓存失效的问题\"},{\"version\":\"0.5.2\",\"desc\":\"优化\\n- 优化mg源打开歌单的链接兼容\\n\\n修复\\n- 修复单曲循环播放时循环次数为偶数时歌词不重新播放的问题\\n- 添加针对进入歌词界面时某些情况下会弹出`scrollToIndex out of range: requested index ...`崩溃错误弹窗的处理\\n- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注：现在可以加载1000+首歌曲的歌单，但出于未知原因会导致部分歌曲无法加载（可能是无版权导致的），目前酷狗码仍然最多只能加载500首歌\"},{\"version\":\"0.5.1\",\"desc\":\"优化\\n- 添加切换播放模式时的文字提示\\n- 优化单首歌曲的添加弹窗操作，当选择当前歌曲已存在目标列表时（列表名灰色显示），会将当前歌曲从目标列表移除，否则将当前歌曲添加到目标列表，添加在弹窗内对歌曲的添加、移动、删除操作时的文字提示\\n\\n修复\\n- 修复mg源搜索失效的问题\\n\\n移除\\n- 因wy源的歌单列表已没有“最新”排序的选项，所以现跟随移除wy源歌单列表按“最新”排序的按钮\"},{\"version\":\"0.5.0\",\"desc\":\"新增\\n- 新增“其他应用播放声音时，自动暂停播放”设置，默认开启\\n- 新增“添加歌曲到列表时的位置”设置，可选项为列表的“顶部”与“底部”\\n- 新增“显示歌词翻译设置”，默认关闭\\n\\n变更\\n- 添加歌曲到列表时从原来的底部改为顶部，若想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项\"},{\"version\":\"0.4.2\",\"desc\":\"优化\\n- 优化wy源歌单导入匹配，现在存在链接外的其他字符也可以打开歌单了\\n\\n修复\\n- 修复定时播放开启歌曲播放完毕再停止时，若倒计时已结束会导致无法播放歌曲的问题\\n- 修复打开歌单失败时会导致应用崩溃的问题\\n- 修复打开kw歌单失败时会无限重试的问题\\n- 尝试修复弹出菜单、列表位置不正确的问题\\n- 修复打开kg源歌单链接失败的问题\\n- 尝试修复有时候进入播放详情歌词界面时会导致应用UI被冻结的问题\\n- 修复有时候进入播放详情页时歌曲封面大小显示不正确的问题\"},{\"version\":\"0.4.1\",\"desc\":\"修复\\n- 修复定时播放开启歌曲播放完毕再停止时，若倒计时已结束会导致无法播放歌曲的问题\"},{\"version\":\"0.4.0\",\"desc\":\"新增\\n- 新增我的列表中已收藏的在线列表的更新功能。注意：这将会覆盖本地的目标列表，歌曲将被替换成最新的在线列表（与PC端的同步一样）\\n- 歌曲添加、移动弹窗新增创建新列表功能\\n- 新增定时退出播放\\n\\n优化\\n- 优化应用布局对手机系统字体大小的适配\\n- 调整歌单详情页，现在在歌单详情页按手机上的返回键将会返回歌单列表，而不是直接退出APP\\n- 优化进入播放详情页、歌单详情页的动画效果\\n\\n修复\\n- 尝试修复某些情况下进播放详情歌词界面时报错的问题\"},{\"version\":\"0.3.3\",\"desc\":\"修复\\n- 尝试修复软件启动时恢复上一次播放的歌曲可能导致软件崩溃的问题\\n- 尝试修复播放详情页歌词导致UI冻结的问题\\n- 修复企鹅音乐搜索歌曲没有结果的问题\\n\\n其他\\n- 整合日志记录\\n- 更新 exoPlayer 到 2.14.0\"},{\"version\":\"0.3.2\",\"desc\":\"修复\\n- 修复手机分享的wy歌单、某些tx、kg歌单无法打开的问题\\n- 修复打开空的歌单时，点击播放全部会导致应用崩溃的问题\\n- 修复企鹅音乐搜索歌曲没有结果的问题\"},{\"version\":\"0.3.1\",\"desc\":\"修复\\n- 修复进入播放详情歌词界面后的屏幕常亮不会被取消的问题\"},{\"version\":\"0.3.0\",\"desc\":\"新增\\n- 新增通过歌单链接打开歌单的功能\\n\\n优化\\n- 切换到播放详情歌词界面时将阻止屏幕息屏\\n\\n修复\\n- 修复一个导致崩溃日志写入文件前会导致APP崩溃的莫名其妙问题\"},{\"version\":\"0.2.0\",\"desc\":\"新增\\n- 新增竖屏下的播放详情页\"},{\"version\":\"0.1.7\",\"desc\":\"优化\\n- 修改歌单导入流程，添加对歌单导入错误的捕获\\n\\n修复\\n- 修复在系统暗主题下，应用内文字输入框的字体会变成白色的问题\"},{\"version\":\"0.1.6\",\"desc\":\"优化\\n- 改进软件错误处理，添加对软件崩溃的错误日志记录，可在设置-其他查看错误日志历史。注：清理缓存时日志也将会被清理\\n\\n修复\\n- 修复显示版本更新弹窗会导致应用崩溃的问题\"},{\"version\":\"0.1.5\",\"desc\":\"修复\\n- 修复修复协议弹窗可以被绕过的问题\\n- 修复从在线列表使用稍后播放功能播放歌曲时，歌曲封面不显示的问题\\n- 修复正在播放“稍后播放”的歌曲时，对“稍后播放”前播放的列表进行添加、删除操作会导致切歌的问题\"},{\"version\":\"0.1.4\",\"desc\":\"修复\\n- 修复获取在线列表时快速切换会导致APP闪退的问题\"},{\"version\":\"0.1.3\",\"desc\":\"优化\\n- 添加导入提示，兼容从PC端“全部数据”类型的备份文件中导入歌单\\n- 添加全局异常错误捕获，现在一般情况下APP崩溃前会弹窗提示错误信息。\"},{\"version\":\"0.1.2\",\"desc\":\"优化\\n- 在搜索、歌单、排行榜列表多选音乐后点菜单中的播放将会把已选的歌曲添加到试听列表播放\\n\\n修复\\n- 修复播放模块没拉取最新代码导致播放器存在无法从通知栏停止等问题\"},{\"version\":\"0.1.1\",\"desc\":\"lx-music移动端v0.1.1版本发布 🎊 🎉\"}]}\n"
  },
  {
    "path": "shim.js",
    "content": "global.Buffer = require('buffer').Buffer\n"
  },
  {
    "path": "src/app.ts",
    "content": "import '@/utils/errorHandle'\nimport { init as initLog } from '@/utils/log'\nimport { bootLog, getBootLog } from '@/utils/bootLog'\nimport '@/config/globalData'\nimport { getFontSize } from '@/utils/data'\nimport { exitApp } from './utils/nativeModules/utils'\nimport { windowSizeTools } from './utils/windowSizeTools'\nimport { listenLaunchEvent } from './navigation/regLaunchedEvent'\nimport { tipDialog } from './utils/tools'\n\nconsole.log('starting app...')\nlistenLaunchEvent()\n\nvoid Promise.all([getFontSize(), windowSizeTools.init()]).then(async([fontSize]) => {\n  global.lx.fontSize = fontSize\n  bootLog('Font size setting loaded.')\n\n  let isInited = false\n  let handlePushedHomeScreen: () => void | Promise<void>\n\n  const tryGetBootLog = () => {\n    try {\n      return getBootLog()\n    } catch (err) {\n      return 'Get boot log failed.'\n    }\n  }\n\n  const handleInit = async() => {\n    if (isInited) return\n    void initLog()\n    const { default: init } = await import('@/core/init')\n    try {\n      handlePushedHomeScreen = await init()\n    } catch (err: any) {\n      void tipDialog({\n        title: '初始化失败 (Init Failed)',\n        message: `Boot Log:\\n${tryGetBootLog()}\\n\\n${(err.stack ?? err.message) as string}`,\n        btnText: 'Exit',\n        bgClose: false,\n      }).then(() => {\n        exitApp()\n      })\n      return\n    }\n    isInited ||= true\n  }\n  const { init: initNavigation, navigations } = await import('@/navigation')\n\n  initNavigation(async() => {\n    await handleInit()\n    if (!isInited) return\n    // import('@/utils/nativeModules/cryptoTest')\n\n    await navigations.pushHomeScreen().then(() => {\n      void handlePushedHomeScreen()\n    }).catch((err: any) => {\n      void tipDialog({\n        title: 'Error',\n        message: err.message,\n        btnText: 'Exit',\n        bgClose: false,\n      }).then(() => {\n        exitApp()\n      })\n    })\n  })\n}).catch((err) => {\n  void tipDialog({\n    title: '初始化失败 (Init Failed)',\n    message: `Boot Log:\\n\\n${(err.stack ?? err.message) as string}`,\n    btnText: 'Exit',\n    bgClose: false,\n  }).then(() => {\n    exitApp()\n  })\n})\n"
  },
  {
    "path": "src/components/DesktopLyricEnable.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\n\nimport { toast } from '@/utils/tools'\n\nimport { useI18n } from '@/lang'\nimport { checkDesktopLyricOverlayPermission, hideDesktopLyric, openDesktopLyricOverlayPermissionActivity, showDesktopLyric } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\nexport interface DesktopLyricEnableType {\n  setEnabled: (enabled: boolean) => void\n}\n\nexport default forwardRef<DesktopLyricEnableType, {}>((props, ref) => {\n  const t = useI18n()\n  const [visible, setVisible] = useState(false)\n  // const setIsShowDesktopLyric = useDispatch('common', 'setIsShowDesktopLyric')\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setEnabled(enabled) {\n      void handleChangeEnableDesktopLyric(enabled)\n    },\n  }))\n\n  const handleShowModal = () => {\n    if (visible) confirmAlertRef.current?.setVisible(true)\n    else {\n      setVisible(true)\n      requestAnimationFrame(() => {\n        confirmAlertRef.current?.setVisible(true)\n      })\n    }\n  }\n  const handleChangeEnableDesktopLyric = async(isEnable: boolean) => {\n    if (isEnable) {\n      try {\n        await checkDesktopLyricOverlayPermission()\n        await showDesktopLyric()\n      } catch (err) {\n        console.log(err)\n        handleShowModal()\n        // return false\n      }\n    } else await hideDesktopLyric()\n    // return true\n    updateSetting({ 'desktopLyric.enable': isEnable })\n  }\n\n  const handleTipsCancel = () => {\n    updateSetting({ 'desktopLyric.enable': false })\n    toast(t('disagree_tip'), 'long')\n  }\n  const handleTipsConfirm = () => {\n    confirmAlertRef.current?.setVisible(false)\n    void openDesktopLyricOverlayPermissionActivity()\n  }\n\n  return (\n    visible\n      ? (\n          <ConfirmAlert\n            ref={confirmAlertRef}\n            onCancel={handleTipsCancel}\n            onConfirm={handleTipsConfirm}\n            bgHide={false}\n            closeBtn={false}\n            cancelText={t('disagree')}\n            confirmText={t('agree_go')}\n            text={t('setting_lyric_desktop_permission_tip')} />\n        )\n      : null\n  )\n})\n"
  },
  {
    "path": "src/components/MetadataEditModal/InputItem.tsx",
    "content": "import { memo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\nimport type { InputProps } from '@/components/common/Input'\nimport Input from '@/components/common/Input'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\n\n\nexport interface InputItemProps extends InputProps {\n  value: string\n  label: string\n  onChanged: (text: string) => void\n}\n\nexport default memo(({ value, label, onChanged, ...props }: InputItemProps) => {\n  const theme = useTheme()\n  return (\n    <View style={styles.container}>\n      <Text style={styles.label} size={14}>{label}</Text>\n      <Input\n        value={value}\n        onChangeText={onChanged}\n        style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n        {...props}\n       />\n    </View>\n  )\n})\n\nconst styles = StyleSheet.create({\n  container: {\n    // paddingLeft: 25,\n    marginBottom: 15,\n  },\n  label: {\n    marginBottom: 2,\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // borderRadius: 4,\n    // paddingTop: 3,\n    // paddingBottom: 3,\n    // maxWidth: 300,\n  },\n})\n"
  },
  {
    "path": "src/components/MetadataEditModal/MetadataForm.tsx",
    "content": "import { useImperativeHandle, forwardRef, useState, useCallback, useRef } from 'react'\nimport { View } from 'react-native'\nimport { TEMP_FILE_PATH, createStyle, toast } from '@/utils/tools'\nimport InputItem from './InputItem'\nimport { useI18n } from '@/lang'\nimport TextAreaItem from './TextAreaItem'\nimport PicItem from './PicItem'\nimport { useTheme } from '@/store/theme/hook'\nimport ParseName from './ParseName'\nimport { downloadFile, mkdir, stat } from '@/utils/fs'\nimport { useUnmounted } from '@/utils/hooks'\nimport { getLyricInfo, getPicUrl } from '@/core/music/local'\nimport settingState from '@/store/setting/state'\nimport { buildLyrics } from '@/utils/lrcTools'\n\nexport interface Metadata {\n  name: string // 歌曲名\n  singer: string // 艺术家名\n  albumName: string // 歌曲专辑名称\n  pic: string\n  lyric: string\n  interval: string\n}\nexport const defaultData = {\n  name: '',\n  singer: '',\n  albumName: '',\n  pic: '',\n  lyric: '',\n  interval: '',\n}\n\nexport interface MetadataFormType {\n  setForm: (path: string, metadata: Metadata) => void\n  getForm: () => Metadata\n}\n\nconst matcheingPic = new Set<string>()\nconst matcheingLrc = new Set<string>()\nexport default forwardRef<MetadataFormType, {}>((props, ref) => {\n  const t = useI18n()\n  const [fileName, setFileName] = useState('')\n  const filePath = useRef('')\n  const [data, setData] = useState({ ...defaultData })\n  const theme = useTheme()\n  const isUnmounted = useUnmounted()\n\n  useImperativeHandle(ref, () => ({\n    setForm(path, data) {\n      filePath.current = path\n      // setPath(path)\n      void stat(path).then(info => {\n        if (isUnmounted.current) return\n        setFileName(info.name)\n      })\n      setData(data)\n    },\n    getForm() {\n      return {\n        ...data,\n        name: data.name.trim(),\n        singer: data.singer.trim(),\n        albumName: data.albumName.trim(),\n      }\n    },\n  }))\n\n  const handleUpdateName = useCallback((name: string) => {\n    if (name.length > 150) name = name.substring(0, 150)\n    setData(data => {\n      return { ...data, name }\n    })\n  }, [])\n  const handleUpdateSinger = useCallback((singer: string) => {\n    if (singer.length > 150) singer = singer.substring(0, 150)\n    setData(data => {\n      return { ...data, singer }\n    })\n  }, [])\n  const handleUpdateAlbumName = useCallback((albumName: string) => {\n    if (albumName.length > 150) albumName = albumName.substring(0, 150)\n    setData(data => {\n      return { ...data, albumName }\n    })\n  }, [])\n  const handleOnlineMatchPic = useCallback(() => {\n    let path = filePath.current\n    if (matcheingPic.has(path)) return\n    matcheingPic.add(path)\n    void getPicUrl({\n      skipFilePic: true,\n      musicInfo: {\n        id: path,\n        interval: data.interval,\n        meta: {\n          albumName: data.albumName,\n          ext: '',\n          filePath: path,\n          songId: path,\n        },\n        name: data.name,\n        singer: data.singer,\n        source: 'local',\n      },\n      isRefresh: false,\n    }).then(async(pic) => {\n      if (isUnmounted.current || path != filePath.current) return\n      let ext = pic.split('?')[0]\n      ext = ext.substring(ext.lastIndexOf('.') + 1)\n      if (ext.length > 5) ext = 'jpeg'\n      await mkdir(TEMP_FILE_PATH)\n      const picPath = `${TEMP_FILE_PATH}/${Math.random().toString().substring(5)}.${ext}`\n      return downloadFile(pic, picPath, {\n        connectionTimeout: 10000,\n        readTimeout: 10000,\n      }).promise.then((res) => {\n        if (isUnmounted.current || path != filePath.current) return\n        toast(t('metadata_edit_modal_form_match_pic_success'))\n        setData(data => {\n          return { ...data, pic: picPath }\n        })\n      })\n    }).catch((err) => {\n      console.log(err)\n      if (isUnmounted.current || path != filePath.current) return\n      toast(t('metadata_edit_modal_form_match_pic_failed'))\n    }).finally(() => {\n      matcheingPic.delete(path)\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data.albumName, data.name, data.singer, t])\n  const handleOnlineMatchLyric = useCallback(() => {\n    let path = filePath.current\n    if (matcheingLrc.has(path)) return\n    matcheingLrc.add(path)\n    void getLyricInfo({\n      skipFileLyric: true,\n      musicInfo: {\n        id: path,\n        interval: data.interval,\n        meta: {\n          albumName: data.albumName,\n          ext: '',\n          filePath: path,\n          songId: path,\n        },\n        name: data.name,\n        singer: data.singer,\n        source: 'local',\n      },\n      isRefresh: false,\n    }).then(async(lrcData) => {\n      if (isUnmounted.current || path != filePath.current) return\n      toast(t('metadata_edit_modal_form_match_lyric_success'))\n      setData(data => {\n        return { ...data, lyric: buildLyrics(lrcData, true, settingState.setting['player.isShowLyricTranslation'], settingState.setting['player.isShowLyricRoma']) }\n      })\n    }).catch(() => {\n      if (isUnmounted.current || path != filePath.current) return\n      toast(t('metadata_edit_modal_form_match_lyric_failed'))\n    }).finally(() => {\n      matcheingLrc.delete(path)\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [data.albumName, data.name, data.singer, t])\n  const handleUpdatePic = useCallback((path: string) => {\n    setData(data => {\n      return { ...data, pic: path }\n    })\n  }, [])\n  const handleUpdateLyric = useCallback((lyric: string) => {\n    setData(data => {\n      return { ...data, lyric }\n    })\n  }, [])\n\n  return (\n    <View style={styles.container}>\n      <TextAreaItem\n        value={fileName}\n        label={global.i18n.t('metadata_edit_modal_file_name')}\n        numberOfLines={2}\n        scrollEnabled\n        style={{ ...styles.pathText, color: theme['c-primary-font'] }}\n      />\n\n      <InputItem\n        value={data.name}\n        label={t('metadata_edit_modal_form_name')}\n        onChanged={handleUpdateName}\n        keyboardType=\"name-phone-pad\" />\n      <InputItem\n        value={data.singer}\n        label={t('metadata_edit_modal_form_singer')}\n        onChanged={handleUpdateSinger}\n        keyboardType=\"name-phone-pad\" />\n      <ParseName\n        fileName={fileName}\n        onNameChanged={handleUpdateName}\n        onSingerChanged={handleUpdateSinger}\n      />\n      <InputItem\n        value={data.albumName}\n        label={t('metadata_edit_modal_form_album_name')}\n        onChanged={handleUpdateAlbumName}\n        keyboardType=\"name-phone-pad\" />\n\n      <PicItem\n        value={data.pic}\n        label={t('metadata_edit_modal_form_pic')}\n        onOnlineMatch={handleOnlineMatchPic}\n        onChanged={handleUpdatePic} />\n      <TextAreaItem\n        value={data.lyric}\n        label={t('metadata_edit_modal_form_lyric')}\n        onOnlineMatch={handleOnlineMatchLyric}\n        onChanged={handleUpdateLyric}\n        numberOfLines={6}\n        keyboardType=\"default\" />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n    width: 360,\n    maxWidth: '100%',\n  },\n  pathText: {\n    height: 60,\n  },\n})\n\n\n"
  },
  {
    "path": "src/components/MetadataEditModal/ParseName.tsx",
    "content": "import { memo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport ButtonPrimary from '@/components/common/ButtonPrimary'\nimport { useI18n } from '@/lang'\n\n\nexport interface ParseNameProps {\n  fileName: string\n  onNameChanged: (text: string) => void\n  onSingerChanged: (text: string) => void\n}\n\nconst parsePath = (fileName: string) => {\n  return fileName.substring(0, fileName.lastIndexOf('.')).split('-').map(name => name.trim())\n}\n\nexport default memo(({ fileName, onNameChanged, onSingerChanged }: ParseNameProps) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const handleParseNameSinger = () => {\n    const [name, singer] = parsePath(fileName)\n    onNameChanged(name)\n    if (singer) onSingerChanged(singer)\n  }\n  const handleParseSingerName = () => {\n    const [singer, name] = parsePath(fileName)\n    onSingerChanged(singer)\n    if (name) onNameChanged(name)\n  }\n  return (\n    <View style={styles.container}>\n      <Text style={styles.label} size={14}>{t('metadata_edit_modal_form_parse_name')}</Text>\n      <View style={styles.btns}>\n        <ButtonPrimary style={{ backgroundColor: theme['c-button-background'] }} onPress={handleParseNameSinger}>\n          <Text color={theme['c-button-font']} size={13}>{t('metadata_edit_modal_form_parse_name_singer')}</Text>\n        </ButtonPrimary>\n        <ButtonPrimary style={{ backgroundColor: theme['c-button-background'] }} onPress={handleParseSingerName}>\n          <Text color={theme['c-button-font']} size={13}>{t('metadata_edit_modal_form_parse_singer_name')}</Text>\n        </ButtonPrimary>\n      </View>\n    </View>\n  )\n})\n\nconst styles = StyleSheet.create({\n  container: {\n    // paddingLeft: 25,\n    marginBottom: 15,\n  },\n  label: {\n    marginBottom: 2,\n  },\n  btns: {\n    marginTop: 5,\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/components/MetadataEditModal/PicItem.tsx",
    "content": "import { memo, useCallback, useRef } from 'react'\n\nimport { TouchableOpacity, View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport Image from '@/components/common/Image'\nimport FileSelect, { type FileSelectType } from '@/components/common/FileSelect'\nimport { BorderWidths } from '@/theme'\n\n\nexport interface PicItemProps {\n  value: string\n  label: string\n  onOnlineMatch: () => void\n  onChanged: (text: string) => void\n}\n\nexport default memo(({ value, label, onOnlineMatch, onChanged }: PicItemProps) => {\n  const theme = useTheme()\n  const fileSelectRef = useRef<FileSelectType>(null)\n  const handleRemoveFile = useCallback(() => {\n    onChanged('')\n  }, [onChanged])\n  const handleShowSelectFile = useCallback(() => {\n    fileSelectRef.current?.show({\n      title: global.i18n.t('metadata_edit_modal_form_select_pic_title'),\n      dirOnly: false,\n      filter: ['jpg', 'jpeg', 'png'],\n    }, (path) => {\n      onChanged(path)\n    })\n  }, [onChanged])\n  return (\n    <View style={styles.container}>\n      <View style={styles.header}>\n        <Text style={styles.label} size={14}>{label}</Text>\n        <View style={styles.btns}>\n          <TouchableOpacity onPress={handleRemoveFile}>\n            <Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_remove_pic')}</Text>\n          </TouchableOpacity>\n          <TouchableOpacity onPress={onOnlineMatch}>\n            <Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_match_pic')}</Text>\n          </TouchableOpacity>\n          <TouchableOpacity onPress={handleShowSelectFile}>\n            <Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_select_pic')}</Text>\n          </TouchableOpacity>\n        </View>\n      </View>\n      <View style={styles.picContent}>\n        <Image\n          url={value}\n          cache={false}\n          style={{ ...styles.pic, borderColor: theme['c-border-background'] }}\n        />\n      </View>\n      <FileSelect ref={fileSelectRef} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    // paddingLeft: 25,\n    marginBottom: 15,\n  },\n  header: {\n    flexDirection: 'row',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n    gap: 30,\n  },\n  label: {\n    marginBottom: 2,\n  },\n  btns: {\n    flexDirection: 'row',\n    gap: 15,\n  },\n  picContent: {\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    marginTop: 5,\n    position: 'relative',\n  },\n  pic: {\n    width: 180,\n    height: 180,\n    borderWidth: BorderWidths.normal,\n    borderStyle: 'dashed',\n  },\n})\n"
  },
  {
    "path": "src/components/MetadataEditModal/TextAreaItem.tsx",
    "content": "import { memo, useCallback } from 'react'\n\nimport { StyleSheet, TouchableOpacity, View } from 'react-native'\nimport type { InputProps } from '@/components/common/Input'\nimport Input from '@/components/common/Input'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\n\n\nexport interface TextAreaItemProps extends InputProps {\n  value: string\n  label: string\n  onOnlineMatch?: () => void\n  onChanged?: (text: string) => void\n}\n\nexport default memo(({ value, label, onOnlineMatch, onChanged, style, ...props }: TextAreaItemProps) => {\n  const theme = useTheme()\n  const handleRemove = useCallback(() => {\n    onChanged?.('')\n  }, [onChanged])\n\n  return (\n    <View style={styles.container}>\n      <View style={styles.header}>\n        <Text style={styles.label} size={14}>{label}</Text>\n        {\n          onChanged ? (\n            <View style={styles.btns}>\n              <TouchableOpacity onPress={handleRemove}>\n                <Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_remove_lyric')}</Text>\n              </TouchableOpacity>\n              <TouchableOpacity onPress={onOnlineMatch}>\n                <Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_match_lyric')}</Text>\n              </TouchableOpacity>\n            </View>\n          ) : null\n        }\n      </View>\n      <Input\n        value={value}\n        onChangeText={onChanged}\n        scrollEnabled={false}\n        textAlignVertical='top'\n        multiline\n        style={StyleSheet.compose({ ...styles.textarea, backgroundColor: theme['c-primary-input-background'] }, style)}\n        {...props}\n       />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    // paddingLeft: 25,\n    marginBottom: 15,\n  },\n  header: {\n    flexDirection: 'row',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n    gap: 30,\n  },\n  label: {\n    marginBottom: 2,\n  },\n  btns: {\n    flexDirection: 'row',\n    gap: 15,\n  },\n  textarea: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingTop: 3,\n    paddingBottom: 3,\n    height: 'auto',\n    // height: 300,\n    // maxWidth: 300,\n  },\n})\n"
  },
  {
    "path": "src/components/MetadataEditModal/index.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { View } from 'react-native'\nimport { TEMP_FILE_PATH, createStyle, toast } from '@/utils/tools'\nimport {\n  readMetadata,\n  readPic,\n  readLyric,\n  writeMetadata,\n  writePic,\n  writeLyric,\n} from '@/utils/localMediaMetadata'\nimport { useUnmounted } from '@/utils/hooks'\nimport MetadataForm, { defaultData, type Metadata, type MetadataFormType } from './MetadataForm'\nimport { log } from '@/utils/log'\nimport { formatPlayTime2 } from '@/utils'\nimport { unlink } from '@/utils/fs'\n\nexport type {\n  Metadata,\n}\n\nexport interface MetadataEditType {\n  show: (filePath: string) => void\n}\nexport interface MetadataEditProps {\n  onUpdate: (info: Metadata) => void\n}\n\n\nexport default forwardRef<MetadataEditType, MetadataEditProps>((props, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const metadataFormRef = useRef<MetadataFormType>(null)\n  const filePath = useRef<string>('')\n  const metadata = useRef<Metadata>({ ...defaultData })\n  const [visible, setVisible] = useState(false)\n  const [processing, setProcessing] = useState(false)\n  const isUnmounted = useUnmounted()\n\n  const handleShow = (filePath: string) => {\n    alertRef.current?.setVisible(true)\n    void Promise.all([\n      readMetadata(filePath),\n      readPic(filePath).catch(() => ''),\n      readLyric(filePath, false).catch(() => ''),\n    ]).then(async([_metadata, pic, lyric]) => {\n      if (!_metadata) return\n      if (isUnmounted.current) return\n      metadata.current = {\n        name: _metadata.name,\n        singer: _metadata.singer,\n        albumName: _metadata.albumName,\n        pic,\n        interval: formatPlayTime2(_metadata.interval),\n        lyric,\n      }\n      requestAnimationFrame(() => {\n        metadataFormRef.current?.setForm(filePath, metadata.current)\n      })\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show(path) {\n      filePath.current = path\n      if (visible) handleShow(path)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow(path)\n        })\n      }\n    },\n  }))\n\n  const handleUpdate = async() => {\n    if (!metadataFormRef.current) return\n    let _metadata = metadataFormRef.current.getForm()\n    if (!_metadata.name) {\n      toast(global.i18n.t('metadata_edit_modal_tip'), 'long')\n      return\n    }\n    setProcessing(true)\n    let isUpdated = false\n    try {\n      if (\n        _metadata.name != metadata.current.name ||\n        _metadata.singer != metadata.current.singer ||\n        _metadata.albumName != metadata.current.albumName\n      ) {\n        isUpdated ||= true\n        await writeMetadata(filePath.current, {\n          name: _metadata.name,\n          singer: _metadata.singer,\n          albumName: _metadata.albumName,\n        })\n      }\n      if (_metadata.pic != metadata.current.pic) {\n        isUpdated ||= true\n        await writePic(filePath.current, _metadata.pic)\n        if (_metadata.pic.startsWith(TEMP_FILE_PATH)) void unlink(_metadata.pic)\n      }\n      if (_metadata.lyric != metadata.current.lyric) {\n        isUpdated ||= true\n        await writeLyric(filePath.current, _metadata.lyric)\n      }\n    } catch (err: any) {\n      log.error(`save (${filePath.current}) metadata failed: \\n${err.message}`)\n      toast(global.i18n.t('metadata_edit_modal_failed'), 'long')\n      return\n    } finally {\n      setProcessing(false)\n    }\n    if (isUpdated) toast(global.i18n.t('metadata_edit_modal_success'), 'long')\n    alertRef.current?.setVisible(false)\n    props.onUpdate(_metadata)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleUpdate}\n          confirmText={processing ? global.i18n.t('metadata_edit_modal_processing') : global.i18n.t('metadata_edit_modal_confirm')}\n          disabledConfirm={processing}\n        >\n          <View style={styles.renameContent} onStartShouldSetResponder={() => true}>\n            <Text style={styles.title}>{global.i18n.t('metadata_edit_modal_title')}</Text>\n            <MetadataForm ref={metadataFormRef} />\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  renameContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  title: {\n    marginBottom: 20,\n    textAlign: 'center',\n  },\n})\n\n\n"
  },
  {
    "path": "src/components/MusicAddModal/CreateUserList.tsx",
    "content": "import { useState, useRef, useEffect } from 'react'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { confirmDialog, createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { createUserList } from '@/core/list'\nimport listState from '@/store/list/state'\n\nexport default ({ isEdit, onHide }: {\n  isEdit: boolean\n  onHide: () => void\n}) => {\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n  const t = useI18n()\n\n  useEffect(() => {\n    if (isEdit) {\n      setText('')\n      requestAnimationFrame(() => {\n        inputRef.current?.focus()\n      })\n    }\n  }, [isEdit])\n\n  const handleSubmitEditing = async() => {\n    onHide()\n    const name = text.trim()\n    if (!name.length || (listState.userList.some(l => l.name == name) && !(await confirmDialog({\n      message: global.i18n.t('list_duplicate_tip'),\n    })))) return\n    void createUserList(listState.userList.length, [{ id: `userlist_${Date.now()}`, name, locationUpdateTime: null }])\n  }\n\n  return isEdit\n    ? (\n      <View style={styles.imputContainer}>\n        <Input\n          placeholder={t('list_create_input_placeholder')}\n          value={text}\n          onChangeText={setText}\n          ref={inputRef}\n          onBlur={handleSubmitEditing}\n          onSubmitEditing={handleSubmitEditing}\n          style={styles.input}\n        />\n      </View>\n      )\n    : null\n}\n\nconst styles = createStyle({\n  imputContainer: {\n    position: 'absolute',\n    top: 0,\n    left: 0,\n    width: '100%',\n    height: '100%',\n    paddingBottom: 10,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  input: {\n    flex: 1,\n    fontSize: 14,\n    borderRadius: 4,\n    textAlign: 'center',\n    height: '100%',\n  },\n})\n"
  },
  {
    "path": "src/components/MusicAddModal/List.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport { ScrollView, TouchableOpacity, View } from 'react-native'\n\nimport Text from '@/components/common/Text'\nimport { useMyList } from '@/store/list/hook'\nimport ListItem, { styles as listStyles } from './ListItem'\nimport CreateUserList from './CreateUserList'\nimport { useWindowSize } from '@/utils/hooks'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nconst styles = createStyle({\n  list: {\n    paddingLeft: 15,\n    paddingRight: 2,\n    paddingBottom: 5,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    // backgroundColor: 'rgba(0,0,0,0.2)'\n    // justifyContent: 'center',\n  },\n})\nconst MIN_WIDTH = scaleSizeW(150)\nconst PADDING = styles.list.paddingLeft + styles.list.paddingRight\n\n\nconst EditListItem = ({ itemWidth }: {\n  itemWidth: number\n}) => {\n  const [isEdit, setEdit] = useState(false)\n  const theme = useTheme()\n  const t = useI18n()\n\n  return (\n    <View style={{ ...listStyles.listItem, width: itemWidth }}>\n      <TouchableOpacity\n        style={{ ...listStyles.button, borderColor: theme['c-primary-light-200-alpha-700'], borderStyle: 'dashed' }}\n        onPress={() => { setEdit(true) }}\n      >\n        <Text style={{ opacity: isEdit ? 0 : 1 }} numberOfLines={1} size={14} color={theme['c-button-font']}>{t('list_create')}</Text>\n      </TouchableOpacity>\n      {\n        isEdit\n          ? <CreateUserList isEdit={isEdit} onHide={() => { setEdit(false) }} />\n          : null\n      }\n    </View>\n  )\n}\n\nexport default ({ musicInfo, onPress }: {\n  musicInfo: LX.Music.MusicInfo\n  onPress: (listInfo: LX.List.MyListInfo) => void\n}) => {\n  const windowSize = useWindowSize()\n  const allList = useMyList()\n  const itemWidth = useMemo(() => {\n    let w = Math.floor(windowSize.width * 0.9 - PADDING)\n    let n = Math.floor(w / MIN_WIDTH)\n    if (n > 10) n = 10\n    return Math.floor((w - 1) / n)\n  }, [windowSize])\n\n  return (\n    <ScrollView style={{ flexGrow: 0 }}>\n      <View style={styles.list} onStartShouldSetResponder={() => true}>\n        { allList.map(info => <ListItem key={info.id} listInfo={info} musicInfo={musicInfo} onPress={onPress} width={itemWidth} />) }\n        <EditListItem itemWidth={itemWidth} />\n      </View>\n    </ScrollView>\n  )\n}\n"
  },
  {
    "path": "src/components/MusicAddModal/ListItem.tsx",
    "content": "import { View } from 'react-native'\nimport Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { BorderWidths } from '@/theme'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useMusicExistsList } from '@/store/list/hook'\n\nexport default ({ listInfo, onPress, musicInfo, width }: {\n  listInfo: LX.List.MyListInfo\n  onPress: (listInfo: LX.List.MyListInfo) => void\n  musicInfo: LX.Music.MusicInfo\n  width: number\n}) => {\n  const theme = useTheme()\n  const isExists = useMusicExistsList(listInfo, musicInfo)\n\n  const handlePress = () => {\n    if (isExists) {\n      toast(global.i18n.t('list_add_tip_exists'))\n      return\n    }\n    onPress(listInfo)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, width }}>\n      <Button\n        style={{ ...styles.button, backgroundColor: theme['c-button-background'], borderColor: theme['c-primary-light-400-alpha-300'], opacity: isExists ? 0.4 : 1 }}\n        onPress={handlePress}\n      >\n        <Text numberOfLines={1} size={14} color={theme['c-button-font']}>{listInfo.name}</Text>\n      </Button>\n    </View>\n  )\n}\n\nexport const styles = createStyle({\n  listItem: {\n    // width: '50%',\n    paddingRight: 13,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  button: {\n    height: 36,\n    paddingLeft: 10,\n    paddingRight: 10,\n    marginRight: 10,\n    marginBottom: 10,\n    borderRadius: 4,\n    width: '100%',\n    alignItems: 'center',\n    justifyContent: 'center',\n    borderWidth: BorderWidths.normal1,\n  },\n})\n"
  },
  {
    "path": "src/components/MusicAddModal/MusicAddModal.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport { toast } from '@/utils/tools'\nimport Title from './Title'\nimport List from './List'\nimport { useI18n } from '@/lang'\nimport { addListMusics, moveListMusics } from '@/core/list'\nimport settingState from '@/store/setting/state'\n\nexport interface SelectInfo {\n  musicInfo: LX.Music.MusicInfo | null\n  listId: string\n  isMove: boolean\n  // single: boolean\n}\nconst initSelectInfo = {}\n\nexport interface MusicAddModalProps {\n  onAdded?: () => void\n  // onRename: (listInfo: LX.List.UserListInfo) => void\n  // onImport: (listInfo: LX.List.MyListInfo, index: number) => void\n  // onExport: (listInfo: LX.List.MyListInfo, index: number) => void\n  // onSync: (listInfo: LX.List.UserListInfo) => void\n  // onRemove: (listInfo: LX.List.UserListInfo) => void\n}\nexport interface MusicAddModalType {\n  show: (info: SelectInfo) => void\n}\n\nexport default forwardRef<MusicAddModalType, MusicAddModalProps>(({ onAdded }, ref) => {\n  const t = useI18n()\n  const dialogRef = useRef<DialogType>(null)\n  const [selectInfo, setSelectInfo] = useState<SelectInfo>(initSelectInfo as SelectInfo)\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo) {\n      setSelectInfo(selectInfo)\n\n      requestAnimationFrame(() => {\n        dialogRef.current?.setVisible(true)\n      })\n    },\n  }))\n\n  const handleHide = () => {\n    requestAnimationFrame(() => {\n      setSelectInfo({ ...selectInfo, musicInfo: null })\n    })\n  }\n\n  const handleSelect = (listInfo: LX.List.MyListInfo) => {\n    dialogRef.current?.setVisible(false)\n    if (selectInfo.isMove) {\n      void moveListMusics(selectInfo.listId, listInfo.id,\n        [selectInfo.musicInfo!],\n        settingState.setting['list.addMusicLocationType'],\n      ).then(() => {\n        onAdded?.()\n        toast(t('list_edit_action_tip_move_success'))\n      }).catch(() => {\n        toast(t('list_edit_action_tip_move_failed'))\n      })\n    } else {\n      void addListMusics(listInfo.id,\n        [selectInfo.musicInfo!],\n        settingState.setting['list.addMusicLocationType'],\n      ).then(() => {\n        onAdded?.()\n        toast(t('list_edit_action_tip_add_success'))\n      }).catch(() => {\n        toast(t('list_edit_action_tip_add_failed'))\n      })\n    }\n  }\n\n  return (\n    <Dialog ref={dialogRef} onHide={handleHide}>\n      {\n        selectInfo.musicInfo\n          ? (<>\n              <Title musicInfo={selectInfo.musicInfo} isMove={selectInfo.isMove} />\n              <List musicInfo={selectInfo.musicInfo} onPress={handleSelect} />\n            </>)\n          : null\n      }\n    </Dialog>\n  )\n})\n\n"
  },
  {
    "path": "src/components/MusicAddModal/Title.tsx",
    "content": "import Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\n\nexport default ({ musicInfo, isMove }: {\n  musicInfo: LX.Music.MusicInfo\n  isMove: boolean\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  return (\n    <Text style={styles.title}>\n      {t(isMove ? 'list_add_title_first_move' : 'list_add_title_first_add')} <Text color={theme['c-primary-font']}>{musicInfo.name}</Text> {t('list_add_title_last')}\n    </Text>\n  )\n}\n\nconst styles = createStyle({\n  title: {\n    textAlign: 'center',\n    paddingTop: 15,\n    paddingBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/components/MusicAddModal/index.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport Modal, { type MusicAddModalType as ModalType, type MusicAddModalProps as ModalProps, type SelectInfo } from './MusicAddModal'\n\nexport interface MusicAddModalProps {\n  onAdded?: ModalProps['onAdded']\n}\nexport interface MusicAddModalType {\n  show: (info: SelectInfo) => void\n}\n\nexport default forwardRef<MusicAddModalType, MusicAddModalProps>(({ onAdded }, ref) => {\n  const musicAddModalRef = useRef<ModalType>(null)\n  const [visible, setVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(listInfo) {\n      if (visible) musicAddModalRef.current?.show(listInfo)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          musicAddModalRef.current?.show(listInfo)\n        })\n      }\n    },\n  }))\n\n  return (\n    visible\n      ? <Modal ref={musicAddModalRef} onAdded={onAdded} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/components/MusicMultiAddModal/List.tsx",
    "content": "import { useMemo, useState } from 'react'\nimport { ScrollView, TouchableOpacity, View } from 'react-native'\n\nimport Text from '@/components/common/Text'\nimport { useMyList } from '@/store/list/hook'\nimport ListItem, { styles as listStyles } from './ListItem'\nimport CreateUserList from '../MusicAddModal/CreateUserList'\nimport { useWindowSize } from '@/utils/hooks'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nconst styles = createStyle({\n  list: {\n    paddingLeft: 15,\n    paddingRight: 2,\n    paddingBottom: 5,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    // backgroundColor: 'rgba(0,0,0,0.2)'\n  },\n})\nconst MIN_WIDTH = scaleSizeW(140)\nconst PADDING = styles.list.paddingLeft + styles.list.paddingRight\n\nconst EditListItem = ({ itemWidth }: {\n  itemWidth: number\n}) => {\n  const [isEdit, setEdit] = useState(false)\n  const theme = useTheme()\n  const t = useI18n()\n\n  return (\n    <View style={{ ...listStyles.listItem, width: itemWidth }}>\n      <TouchableOpacity\n        style={{ ...listStyles.button, borderColor: theme['c-primary-light-200-alpha-700'], borderStyle: 'dashed' }}\n        onPress={() => { setEdit(true) }}\n      >\n        <Text style={{ opacity: isEdit ? 0 : 1 }} numberOfLines={1} size={14} color={theme['c-button-font']}>{t('list_create')}</Text>\n      </TouchableOpacity>\n      {\n        isEdit\n          ? <CreateUserList isEdit={isEdit} onHide={() => { setEdit(false) }} />\n          : null\n      }\n    </View>\n  )\n}\n\nexport default ({ listId, onPress }: {\n  listId: string\n  onPress: (listInfo: LX.List.MyListInfo) => void\n}) => {\n  const windowSize = useWindowSize()\n  const allList = useMyList().filter(l => l.id != listId)\n  const itemWidth = useMemo(() => {\n    let w = Math.floor(windowSize.width * 0.9 - PADDING)\n    let n = Math.floor(w / MIN_WIDTH)\n    if (n > 10) n = 10\n    return Math.floor((w - 1) / n)\n  }, [windowSize])\n\n  return (\n    <ScrollView style={{ flexGrow: 0 }}>\n      <View style={{ ...styles.list }} onStartShouldSetResponder={() => true}>\n        { allList.map(info => <ListItem key={info.id} listInfo={info} onPress={onPress} width={itemWidth} />) }\n        <EditListItem itemWidth={itemWidth} />\n      </View>\n    </ScrollView>\n  )\n}\n"
  },
  {
    "path": "src/components/MusicMultiAddModal/ListItem.tsx",
    "content": "import { View } from 'react-native'\nimport Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { BorderWidths } from '@/theme'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\n\nexport default ({ listInfo, onPress, width }: {\n  listInfo: LX.List.MyListInfo\n  onPress: (listInfo: LX.List.MyListInfo) => void\n  width: number\n}) => {\n  const theme = useTheme()\n\n  const handlePress = () => {\n    onPress(listInfo)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, width }}>\n      <Button\n        style={{ ...styles.button, backgroundColor: theme['c-button-background'], borderColor: theme['c-primary-light-200-alpha-700'] }}\n        onPress={handlePress}\n      >\n        <Text numberOfLines={1} size={14} color={theme['c-button-font']}>{listInfo.name}</Text>\n      </Button>\n    </View>\n  )\n}\n\nexport const styles = createStyle({\n  listItem: {\n    // width: '50%',\n    paddingRight: 13,\n  },\n  button: {\n    height: 36,\n    paddingLeft: 10,\n    paddingRight: 10,\n    marginRight: 10,\n    marginBottom: 10,\n    borderRadius: 4,\n    width: '100%',\n    alignItems: 'center',\n    justifyContent: 'center',\n    borderWidth: BorderWidths.normal1,\n  },\n})\n"
  },
  {
    "path": "src/components/MusicMultiAddModal/MusicMultiAddModal.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport { toast } from '@/utils/tools'\nimport Title from './Title'\nimport List from './List'\nimport { useI18n } from '@/lang'\nimport { addListMusics, moveListMusics } from '@/core/list'\nimport settingState from '@/store/setting/state'\n\nexport interface SelectInfo {\n  selectedList: LX.Music.MusicInfo[]\n  listId: string\n  isMove: boolean\n  // single: boolean\n}\nconst initSelectInfo = { selectedList: [], listId: '', isMove: false }\n\nexport interface MusicMultiAddModalProps {\n  onAdded?: () => void\n  // onRename: (listInfo: LX.List.UserListInfo) => void\n  // onImport: (listInfo: LX.List.MyListInfo, index: number) => void\n  // onExport: (listInfo: LX.List.MyListInfo, index: number) => void\n  // onSync: (listInfo: LX.List.UserListInfo) => void\n  // onRemove: (listInfo: LX.List.UserListInfo) => void\n}\nexport interface MusicMultiAddModalType {\n  show: (info: SelectInfo) => void\n}\n\nexport default forwardRef<MusicMultiAddModalType, MusicMultiAddModalProps>(({ onAdded }, ref) => {\n  const t = useI18n()\n  const dialogRef = useRef<DialogType>(null)\n  const [selectInfo, setSelectInfo] = useState<SelectInfo>(initSelectInfo)\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo) {\n      setSelectInfo(selectInfo)\n\n      requestAnimationFrame(() => {\n        dialogRef.current?.setVisible(true)\n      })\n    },\n  }))\n\n  const handleHide = () => {\n    requestAnimationFrame(() => {\n      setSelectInfo({ ...selectInfo, selectedList: [] })\n    })\n  }\n\n  const handleSelect = (listInfo: LX.List.MyListInfo) => {\n    dialogRef.current?.setVisible(false)\n    if (selectInfo.isMove) {\n      void moveListMusics(selectInfo.listId, listInfo.id,\n        [...selectInfo.selectedList],\n        settingState.setting['list.addMusicLocationType'],\n      ).then(() => {\n        onAdded?.()\n        toast(t('list_edit_action_tip_move_success'))\n      }).catch(() => {\n        toast(t('list_edit_action_tip_move_failed'))\n      })\n    } else {\n      void addListMusics(listInfo.id,\n        [...selectInfo.selectedList],\n        settingState.setting['list.addMusicLocationType'],\n      ).then(() => {\n        onAdded?.()\n        toast(t('list_edit_action_tip_add_success'))\n      }).catch(() => {\n        toast(t('list_edit_action_tip_add_failed'))\n      })\n    }\n  }\n\n  return (\n    <Dialog ref={dialogRef} onHide={handleHide}>\n      {\n        selectInfo.selectedList.length\n          ? (<>\n              <Title selectedList={selectInfo.selectedList} isMove={selectInfo.isMove} />\n              <List listId={selectInfo.listId} onPress={handleSelect} />\n            </>)\n          : null\n      }\n    </Dialog>\n  )\n})\n\n"
  },
  {
    "path": "src/components/MusicMultiAddModal/Title.tsx",
    "content": "import Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\n\nexport default ({ selectedList, isMove }: {\n  selectedList: LX.Music.MusicInfo[]\n  isMove: boolean\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  return (\n    <Text style={styles.title} size={16}>\n      {t(isMove ? 'list_multi_add_title_first_move' : 'list_multi_add_title_first_add')} <Text color={theme['c-primary-font']} size={16}>{selectedList.length}</Text> {t('list_multi_add_title_last')}\n    </Text>\n  )\n}\n\nconst styles = createStyle({\n  title: {\n    textAlign: 'center',\n    paddingTop: 15,\n    paddingBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/components/MusicMultiAddModal/index.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport Modal, { type MusicMultiAddModalType as ModalType, type MusicMultiAddModalProps as ModalProps, type SelectInfo } from './MusicMultiAddModal'\n\nexport interface MusicAddModalProps {\n  onAdded?: ModalProps['onAdded']\n}\nexport interface MusicMultiAddModalType {\n  show: (info: SelectInfo) => void\n}\n\nexport default forwardRef<MusicMultiAddModalType, MusicAddModalProps>(({ onAdded }, ref) => {\n  const musicMultiAddModalRef = useRef<ModalType>(null)\n  const [visible, setVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(listInfo) {\n      if (visible) musicMultiAddModalRef.current?.show(listInfo)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          musicMultiAddModalRef.current?.show(listInfo)\n        })\n      }\n    },\n  }))\n\n  return (\n    visible\n      ? <Modal ref={musicMultiAddModalRef} onAdded={onAdded} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/components/OnlineList/List.tsx",
    "content": "import { useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react'\nimport { FlatList, type FlatListProps, RefreshControl, View } from 'react-native'\n\n// import { useMusicList } from '@/store/list/hook'\nimport ListItem, { ITEM_HEIGHT } from './ListItem'\nimport { createStyle, getRowInfo, type RowInfoType } from '@/utils/tools'\nimport type { Position } from './ListMenu'\nimport type { SelectMode } from './MultipleModeBar'\nimport { useTheme } from '@/store/theme/hook'\nimport settingState from '@/store/setting/state'\nimport { MULTI_SELECT_BAR_HEIGHT } from './MultipleModeBar'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { handlePlay } from './listAction'\nimport { useSettingValue } from '@/store/setting/hook'\n\ntype FlatListType = FlatListProps<LX.Music.MusicInfoOnline>\n\nexport type {\n  RowInfoType,\n}\n\nexport interface ListProps {\n  onShowMenu: (musicInfo: LX.Music.MusicInfoOnline, index: number, position: Position) => void\n  onMuiltSelectMode: () => void\n  onSelectAll: (isAll: boolean) => void\n  onRefresh: () => void\n  onLoadMore: () => void\n  onPlayList?: (index: number) => void\n  progressViewOffset?: number\n  ListHeaderComponent?: FlatListType['ListEmptyComponent']\n  checkHomePagerIdle: boolean\n  rowType?: RowInfoType\n}\nexport interface ListType {\n  setList: (list: LX.Music.MusicInfoOnline[], isAppend: boolean, showSource: boolean) => void\n  setIsMultiSelectMode: (isMultiSelectMode: boolean) => void\n  setSelectMode: (mode: SelectMode) => void\n  selectAll: (isAll: boolean) => void\n  getSelectedList: () => LX.Music.MusicInfoOnline[]\n  getList: () => LX.Music.MusicInfoOnline[]\n  setStatus: (val: Status) => void\n}\nexport type Status = 'loading' | 'refreshing' | 'end' | 'error' | 'idle'\n\n\nconst List = forwardRef<ListType, ListProps>(({\n  onShowMenu,\n  onMuiltSelectMode,\n  onSelectAll,\n  onRefresh,\n  onLoadMore,\n  onPlayList,\n  progressViewOffset,\n  ListHeaderComponent,\n  checkHomePagerIdle,\n  rowType,\n}, ref) => {\n  // const t = useI18n()\n  const theme = useTheme()\n  const flatListRef = useRef<FlatList>(null)\n  const [currentList, setList] = useState<LX.Music.MusicInfoOnline[]>([])\n  const [showSource, setShowSource] = useState(false)\n  const isMultiSelectModeRef = useRef(false)\n  const selectModeRef = useRef<SelectMode>('single')\n  const prevSelectIndexRef = useRef(-1)\n  const [selectedList, setSelectedList] = useState<LX.Music.MusicInfoOnline[]>([])\n  const selectedListRef = useRef<LX.Music.MusicInfoOnline[]>([])\n  const [visibleMultiSelect, setVisibleMultiSelect] = useState(false)\n  const [status, setStatus] = useState<Status>('idle')\n  const rowInfo = useRef(getRowInfo(rowType))\n  const isShowAlbumName = useSettingValue('list.isShowAlbumName')\n  const isShowInterval = useSettingValue('list.isShowInterval')\n  // const currentListIdRef = useRef('')\n  // console.log('render music list')\n\n  useImperativeHandle(ref, () => ({\n    setList(list, isAppend, showSource) {\n      setList(list)\n      setShowSource(showSource)\n      if (!isAppend && selectedListRef.current.length) setSelectedList(selectedListRef.current = [])\n    },\n    setIsMultiSelectMode(isMultiSelectMode) {\n      isMultiSelectModeRef.current = isMultiSelectMode\n      if (!isMultiSelectMode) {\n        prevSelectIndexRef.current = -1\n        handleUpdateSelectedList([])\n      }\n      setVisibleMultiSelect(isMultiSelectMode)\n    },\n    setSelectMode(mode) {\n      selectModeRef.current = mode\n    },\n    selectAll(isAll) {\n      let list: LX.Music.MusicInfoOnline[]\n      if (isAll) {\n        list = [...currentList]\n      } else {\n        list = []\n      }\n      selectedListRef.current = list\n      setSelectedList(list)\n    },\n    getSelectedList() {\n      return selectedListRef.current\n    },\n    getList() {\n      return currentList\n    },\n    setStatus(val) {\n      setStatus(val)\n    },\n  }))\n\n\n  const handleUpdateSelectedList = (newList: LX.Music.MusicInfoOnline[]) => {\n    if (selectedListRef.current.length && newList.length == currentList.length) onSelectAll(true)\n    else if (selectedListRef.current.length == currentList.length) onSelectAll(false)\n    selectedListRef.current = newList\n    setSelectedList(newList)\n  }\n  const handleSelect = (item: LX.Music.MusicInfoOnline, pressIndex: number) => {\n    let newList: LX.Music.MusicInfoOnline[]\n    if (selectModeRef.current == 'single') {\n      prevSelectIndexRef.current = pressIndex\n      const index = selectedListRef.current.indexOf(item)\n      if (index < 0) {\n        newList = [...selectedListRef.current, item]\n      } else {\n        newList = [...selectedListRef.current]\n        newList.splice(index, 1)\n      }\n    } else {\n      if (selectedListRef.current.length) {\n        const prevIndex = prevSelectIndexRef.current\n        const currentIndex = pressIndex\n        if (prevIndex == currentIndex) {\n          newList = []\n        } else if (currentIndex > prevIndex) {\n          newList = currentList.slice(prevIndex, currentIndex + 1)\n        } else {\n          newList = currentList.slice(currentIndex, prevIndex + 1)\n          newList.reverse()\n        }\n      } else {\n        newList = [item]\n        prevSelectIndexRef.current = pressIndex\n      }\n    }\n\n    handleUpdateSelectedList(newList)\n  }\n\n  const handlePress = (item: LX.Music.MusicInfoOnline, index: number) => {\n    requestAnimationFrame(() => {\n      if (checkHomePagerIdle && !global.lx.homePagerIdle) return\n      if (isMultiSelectModeRef.current) {\n        handleSelect(item, index)\n      } else {\n        if (settingState.setting['list.isClickPlayList'] && onPlayList != null) {\n          onPlayList(index)\n        } else {\n          // console.log(currentList[index])\n          handlePlay(currentList[index])\n        }\n      }\n    })\n  }\n\n  const handleLongPress = (item: LX.Music.MusicInfoOnline, index: number) => {\n    if (isMultiSelectModeRef.current) return\n    prevSelectIndexRef.current = index\n    handleUpdateSelectedList([item])\n    onMuiltSelectMode()\n  }\n\n  const handleLoadMore = () => {\n    if (status != 'idle') return\n    onLoadMore()\n  }\n\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => (\n    <ListItem\n      item={item}\n      index={index}\n      showSource={showSource}\n      onPress={handlePress}\n      onLongPress={handleLongPress}\n      onShowMenu={onShowMenu}\n      selectedList={selectedList}\n      rowInfo={rowInfo.current}\n      isShowAlbumName={isShowAlbumName}\n      isShowInterval={isShowInterval}\n    />\n  )\n  const getkey: FlatListType['keyExtractor'] = item => item.id\n  const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n  const refreshControl = useMemo(() => (\n    <RefreshControl\n      colors={[theme['c-primary']]}\n      // progressBackgroundColor={theme.primary}\n      refreshing={status == 'refreshing'}\n      onRefresh={onRefresh} />\n  ), [status, onRefresh, theme])\n  const footerComponent = useMemo(() => {\n    let label: FooterLabel\n    switch (status) {\n      case 'refreshing': return null\n      case 'loading':\n        label = 'list_loading'\n        break\n      case 'end':\n        label = 'list_end'\n        break\n      case 'error':\n        label = 'list_error'\n        break\n      case 'idle':\n        label = null\n        break\n    }\n    return (\n      <View style={{ width: '100%', paddingBottom: visibleMultiSelect ? MULTI_SELECT_BAR_HEIGHT : 0 }} >\n        <Footer label={label} onLoadMore={onLoadMore} />\n      </View>\n    )\n  }, [onLoadMore, status, visibleMultiSelect])\n\n  return (\n    <FlatList\n      ref={flatListRef}\n      style={styles.list}\n      data={currentList}\n      numColumns={rowInfo.current.rowNum}\n      horizontal={false}\n      maxToRenderPerBatch={4}\n      // updateCellsBatchingPeriod={80}\n      windowSize={8}\n      removeClippedSubviews={true}\n      initialNumToRender={12}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      getItemLayout={getItemLayout}\n      // onRefresh={onRefresh}\n      // refreshing={refreshing}\n      onEndReachedThreshold={0.5}\n      onEndReached={handleLoadMore}\n      progressViewOffset={progressViewOffset}\n      ListHeaderComponent={ListHeaderComponent}\n      refreshControl={refreshControl}\n      ListFooterComponent={footerComponent}\n    />\n  )\n})\n\ntype FooterLabel = 'list_loading' | 'list_end' | 'list_error' | null\nconst Footer = ({ label, onLoadMore }: {\n  label: FooterLabel\n  onLoadMore: () => void\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const handlePress = () => {\n    if (label != 'list_error') return\n    onLoadMore()\n  }\n  return (\n    label\n      ? (\n          <View>\n            <Text onPress={handlePress} style={styles.footer} color={theme['c-font-label']}>{t(label)}</Text>\n          </View>\n        )\n      : null\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n  footer: {\n    textAlign: 'center',\n    padding: 10,\n  },\n})\n\nexport default List\n"
  },
  {
    "path": "src/components/OnlineList/ListItem.tsx",
    "content": "import { memo, useRef } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\n// import Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport Badge, { type BadgeType } from '@/components/common/Badge'\nimport { Icon } from '@/components/common/Icon'\nimport { useI18n } from '@/lang'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { LIST_ITEM_HEIGHT } from '@/config/constant'\nimport { createStyle, type RowInfo } from '@/utils/tools'\n\nexport const ITEM_HEIGHT = scaleSizeH(LIST_ITEM_HEIGHT)\n\nconst useQualityTag = (musicInfo: LX.Music.MusicInfoOnline) => {\n  const t = useI18n()\n  let info: { type: BadgeType | null, text: string } = { type: null, text: '' }\n  if (musicInfo.meta._qualitys.flac24bit) {\n    info.type = 'secondary'\n    info.text = t('quality_lossless_24bit')\n  } else if (musicInfo.meta._qualitys.flac ?? musicInfo.meta._qualitys.ape) {\n    info.type = 'secondary'\n    info.text = t('quality_lossless')\n  } else if (musicInfo.meta._qualitys['320k']) {\n    info.type = 'tertiary'\n    info.text = t('quality_high_quality')\n  }\n\n  return info\n}\n\nexport default memo(({ item, index, showSource, onPress, onLongPress, onShowMenu, selectedList, rowInfo, isShowAlbumName, isShowInterval }: {\n  item: LX.Music.MusicInfoOnline\n  index: number\n  showSource?: boolean\n  onPress: (item: LX.Music.MusicInfoOnline, index: number) => void\n  onLongPress: (item: LX.Music.MusicInfoOnline, index: number) => void\n  onShowMenu: (item: LX.Music.MusicInfoOnline, index: number, position: { x: number, y: number, w: number, h: number }) => void\n  selectedList: LX.Music.MusicInfoOnline[]\n  rowInfo: RowInfo\n  isShowAlbumName: boolean\n  isShowInterval: boolean\n}) => {\n  const theme = useTheme()\n\n  const isSelected = selectedList.includes(item)\n\n  const moreButtonRef = useRef<TouchableOpacity>(null)\n  const handleShowMenu = () => {\n    if (moreButtonRef.current?.measure) {\n      moreButtonRef.current.measure((fx, fy, width, height, px, py) => {\n        // console.log(fx, fy, width, height, px, py)\n        onShowMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n      })\n    }\n  }\n  const tagInfo = useQualityTag(item)\n\n  const singer = `${item.singer}${isShowAlbumName && item.meta.albumName ? ` · ${item.meta.albumName}` : ''}`\n\n  return (\n    <View style={{ ...styles.listItem, width: rowInfo.rowWidth, height: ITEM_HEIGHT, backgroundColor: isSelected ? theme['c-primary-background-hover'] : 'rgba(0,0,0,0)' }}>\n      <TouchableOpacity style={styles.listItemLeft} onPress={() => { onPress(item, index) }} onLongPress={() => { onLongPress(item, index) }}>\n        <Text style={styles.sn} size={13} color={theme['c-300']}>{index + 1}</Text>\n        <View style={styles.itemInfo}>\n          <Text numberOfLines={1}>{item.name}</Text>\n          <View style={styles.listItemSingle}>\n            { tagInfo.type ? <Badge type={tagInfo.type}>{tagInfo.text}</Badge> : null }\n            { showSource ? <Badge type=\"tertiary\">{item.source}</Badge> : null }\n            <Text style={styles.listItemSingleText} size={11} color={theme['c-500']} numberOfLines={1}>{singer}</Text>\n          </View>\n        </View>\n        {\n          isShowInterval ? (\n            <Text size={12} color={theme['c-250']} numberOfLines={1}>{item.interval}</Text>\n          ) : null\n        }\n      </TouchableOpacity>\n     <TouchableOpacity onPress={handleShowMenu} ref={moreButtonRef} style={styles.moreButton}>\n        <Icon name=\"dots-vertical\" style={{ color: theme['c-350'] }} size={12} />\n      </TouchableOpacity>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return !!(prevProps.item === nextProps.item &&\n    prevProps.index === nextProps.index &&\n    prevProps.isShowAlbumName === nextProps.isShowAlbumName &&\n    prevProps.isShowInterval === nextProps.isShowInterval &&\n    nextProps.selectedList.includes(nextProps.item) == prevProps.selectedList.includes(nextProps.item)\n  )\n})\n\nconst styles = createStyle({\n  listItem: {\n    // width: '100%',\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    // paddingLeft: 10,\n    paddingRight: 2,\n    alignItems: 'center',\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  listItemLeft: {\n    flex: 1,\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  sn: {\n    width: 38,\n    // fontSize: 12,\n    textAlign: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    paddingLeft: 3,\n    paddingRight: 3,\n  },\n  itemInfo: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingRight: 2,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n  },\n  // listItemTitle: {\n  //   // backgroundColor: 'rgba(0,0,0,0.2)',\n  //   flexGrow: 0,\n  //   flexShrink: 1,\n  //   // fontSize: 15,\n  // },\n  listItemSingle: {\n    paddingTop: 2,\n    flexDirection: 'row',\n    alignItems: 'center',\n    // alignItems: 'flex-end',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  listItemTimeLabel: {\n    marginRight: 5,\n    fontWeight: '400',\n  },\n  listItemSingleText: {\n    // fontSize: 13,\n    // paddingTop: 2,\n    flexGrow: 0,\n    flexShrink: 1,\n    fontWeight: '300',\n  },\n  listItemBadge: {\n    // fontSize: 10,\n    paddingLeft: 5,\n    paddingTop: 2,\n    alignSelf: 'flex-start',\n  },\n  listItemRight: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexBasis: 'auto',\n    justifyContent: 'center',\n  },\n  moreButton: {\n    height: '80%',\n    paddingLeft: 16,\n    paddingRight: 16,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    justifyContent: 'center',\n  },\n})\n\n"
  },
  {
    "path": "src/components/OnlineList/ListMenu.tsx",
    "content": "import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport { useI18n } from '@/lang'\nimport Menu, { type MenuType, type Position } from '@/components/common/Menu'\nimport { hasDislike } from '@/core/dislikeList'\n\nexport interface SelectInfo {\n  musicInfo: LX.Music.MusicInfoOnline\n  selectedList: LX.Music.MusicInfoOnline[]\n  index: number\n  single: boolean\n}\nconst initSelectInfo = {}\n\nexport interface ListMenuProps {\n  onPlay: (selectInfo: SelectInfo) => void\n  onPlayLater: (selectInfo: SelectInfo) => void\n  onAdd: (selectInfo: SelectInfo) => void\n  onCopyName: (selectInfo: SelectInfo) => void\n  onMusicSourceDetail: (selectInfo: SelectInfo) => void\n  onDislikeMusic: (selectInfo: SelectInfo) => void\n}\nexport interface ListMenuType {\n  show: (selectInfo: SelectInfo, position: Position) => void\n}\n\nexport type {\n  Position,\n}\n\nexport default forwardRef<ListMenuType, ListMenuProps>((props: ListMenuProps, ref) => {\n  const t = useI18n()\n  const [visible, setVisible] = useState(false)\n  const menuRef = useRef<MenuType>(null)\n  const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)\n  const [isDislikeMusic, setDislikeMusic] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo, position) {\n      selectInfoRef.current = selectInfo\n      setDislikeMusic(hasDislike(selectInfo.musicInfo))\n      if (visible) menuRef.current?.show(position)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          menuRef.current?.show(position)\n        })\n      }\n    },\n  }))\n\n  const menus = useMemo(() => {\n    return [\n      { action: 'play', label: t('play') },\n      { action: 'playLater', label: t('play_later') },\n      // { action: 'download', label: '下载' },\n      { action: 'add', label: t('add_to') },\n      { action: 'copyName', label: t('copy_name') },\n      { action: 'musicSourceDetail', label: t('music_source_detail') },\n      { action: 'dislike', label: t('dislike'), disabled: isDislikeMusic },\n    ] as const\n  }, [t, isDislikeMusic])\n\n  const handleMenuPress = ({ action }: typeof menus[number]) => {\n    const selectInfo = selectInfoRef.current\n    switch (action) {\n      case 'play':\n        props.onPlay(selectInfo)\n        break\n      case 'playLater':\n        props.onPlayLater(selectInfo)\n        break\n      case 'add':\n        props.onAdd(selectInfo)\n        break\n      case 'copyName':\n        props.onCopyName(selectInfo)\n        break\n      case 'musicSourceDetail':\n        props.onMusicSourceDetail(selectInfo)\n        // setVIsibleMusicPosition(true)\n        break\n      case 'dislike':\n        props.onDislikeMusic(selectInfo)\n        break\n      default:\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <Menu ref={menuRef} menus={menus} onPress={handleMenuPress} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/components/OnlineList/MultipleModeBar.tsx",
    "content": "import { useState, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'\nimport { Animated, View, TouchableOpacity } from 'react-native'\n\nimport Text from '@/components/common/Text'\nimport Button from '@/components/common/Button'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport { BorderWidths } from '@/theme'\nimport { scaleSizeH } from '@/utils/pixelRatio'\n\nexport type SelectMode = 'single' | 'range'\n\nexport const MULTI_SELECT_BAR_HEIGHT = scaleSizeH(40)\n\nexport interface MultipleModeBarProps {\n  onSwitchMode: (mode: SelectMode) => void\n  onSelectAll: (isAll: boolean) => void\n  onExitSelectMode: () => void\n}\nexport interface MultipleModeBarType {\n  show: () => void\n  setIsSelectAll: (isAll: boolean) => void\n  setSwitchMode: (mode: SelectMode) => void\n  exitSelectMode: () => void\n}\n\nexport default forwardRef<MultipleModeBarType, MultipleModeBarProps>(({ onSelectAll, onSwitchMode, onExitSelectMode }, ref) => {\n  // const isGetDetailFailedRef = useRef(false)\n  const [visible, setVisible] = useState(false)\n  const [animatePlayed, setAnimatPlayed] = useState(true)\n  const animFade = useRef(new Animated.Value(0)).current\n  const animTranslateY = useRef(new Animated.Value(0)).current\n  const [selectMode, setSelectMode] = useState<SelectMode>('single')\n  const [isSelectAll, setIsSelectAll] = useState(false)\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    show() {\n      handleShow()\n    },\n    setIsSelectAll(isAll) {\n      setIsSelectAll(isAll)\n    },\n    setSwitchMode(mode: SelectMode) {\n      setSelectMode(mode)\n    },\n    exitSelectMode() {\n      handleHide()\n    },\n  }))\n\n  const handleShow = useCallback(() => {\n    // console.log('show List')\n    setVisible(true)\n    setAnimatPlayed(false)\n    requestAnimationFrame(() => {\n      animTranslateY.setValue(20)\n\n      Animated.parallel([\n        Animated.timing(animFade, {\n          toValue: 0.92,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n        Animated.timing(animTranslateY, {\n          toValue: 0,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n      ]).start(() => {\n        setAnimatPlayed(true)\n      })\n    })\n  }, [animFade, animTranslateY])\n\n  const handleHide = useCallback(() => {\n    setAnimatPlayed(false)\n    Animated.parallel([\n      Animated.timing(animFade, {\n        toValue: 0,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n      Animated.timing(animTranslateY, {\n        toValue: 20,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n    ]).start(finished => {\n      if (!finished) return\n      setVisible(false)\n      setAnimatPlayed(true)\n    })\n  }, [animFade, animTranslateY])\n\n\n  const animaStyle = useMemo(() => ({\n    ...styles.container,\n    height: MULTI_SELECT_BAR_HEIGHT,\n    // backgroundColor: theme['c-content-background'],\n    borderBottomColor: theme['c-border-background'],\n    opacity: animFade, // Bind opacity to animated value\n    transform: [\n      { translateY: animTranslateY },\n    ],\n  }), [animFade, animTranslateY, theme])\n\n  const handleSelectAll = useCallback(() => {\n    const selectAll = !isSelectAll\n    setIsSelectAll(selectAll)\n    onSelectAll(selectAll)\n  }, [isSelectAll, onSelectAll])\n\n  const component = useMemo(() => {\n    return (\n      <Animated.View style={animaStyle}>\n        <View style={styles.switchBtn}>\n          <Button onPress={() => { onSwitchMode('single') }} style={{ ...styles.btn, backgroundColor: selectMode == 'single' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>\n            <Text color={theme['c-button-font']}>{global.i18n.t('list_select_single')}</Text>\n          </Button>\n          <Button onPress={() => { onSwitchMode('range') }} style={{ ...styles.btn, backgroundColor: selectMode == 'range' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>\n            <Text color={theme['c-button-font']}>{global.i18n.t('list_select_range')}</Text>\n          </Button>\n        </View>\n        <TouchableOpacity onPress={handleSelectAll} style={styles.btn}>\n          <Text color={theme['c-button-font']}>{global.i18n.t(isSelectAll ? 'list_select_unall' : 'list_select_all')}</Text>\n        </TouchableOpacity>\n        <TouchableOpacity onPress={onExitSelectMode} style={styles.btn}>\n          <Text color={theme['c-button-font']}>{global.i18n.t('list_select_cancel')}</Text>\n        </TouchableOpacity>\n      </Animated.View>\n    )\n  }, [animaStyle, selectMode, theme, handleSelectAll, isSelectAll, onExitSelectMode, onSwitchMode])\n\n  return !visible && animatePlayed ? null : component\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    position: 'absolute',\n    left: 0,\n    bottom: 0,\n    width: '100%',\n    // height: 40,\n    flexDirection: 'row',\n    borderBottomWidth: BorderWidths.normal,\n  },\n  switchBtn: {\n    flexDirection: 'row',\n    flex: 1,\n  },\n  btn: {\n    // flex: 1,\n    paddingLeft: 18,\n    paddingRight: 18,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/components/OnlineList/index.tsx",
    "content": "import { useRef, forwardRef, useImperativeHandle } from 'react'\nimport { View } from 'react-native'\n// import LoadingMask, { LoadingMaskType } from '@/components/common/LoadingMask'\nimport List, { type ListProps, type ListType, type Status, type RowInfoType } from './List'\nimport ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu'\nimport ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal'\nimport ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal'\nimport MultipleModeBar, { type MultipleModeBarType, type SelectMode } from './MultipleModeBar'\nimport { handleDislikeMusic, handlePlay, handlePlayLater, handleShare, handleShowMusicSourceDetail } from './listAction'\nimport { createStyle } from '@/utils/tools'\n\nexport interface OnlineListProps {\n  onRefresh: ListProps['onRefresh']\n  onLoadMore: ListProps['onLoadMore']\n  onPlayList?: ListProps['onPlayList']\n  progressViewOffset?: ListProps['progressViewOffset']\n  ListHeaderComponent?: ListProps['ListHeaderComponent']\n  checkHomePagerIdle?: boolean\n  rowType?: RowInfoType\n}\nexport interface OnlineListType {\n  setList: (list: LX.Music.MusicInfoOnline[], isAppend?: boolean, showSource?: boolean) => void\n  setStatus: (val: Status) => void\n}\n\nexport default forwardRef<OnlineListType, OnlineListProps>(({\n  onRefresh,\n  onLoadMore,\n  onPlayList,\n  progressViewOffset,\n  ListHeaderComponent,\n  checkHomePagerIdle = false,\n  rowType,\n}, ref) => {\n  const listRef = useRef<ListType>(null)\n  const multipleModeBarRef = useRef<MultipleModeBarType>(null)\n  const listMusicAddRef = useRef<ListMusicAddType>(null)\n  const listMusicMultiAddRef = useRef<ListAddMultiType>(null)\n  const listMenuRef = useRef<ListMenuType>(null)\n  // const loadingMaskRef = useRef<LoadingMaskType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setList(list, isAppend = false, showSource = false) {\n      listRef.current?.setList(list, isAppend, showSource)\n      multipleModeBarRef.current?.setIsSelectAll(false)\n    },\n    setStatus(val) {\n      listRef.current?.setStatus(val)\n    },\n  }))\n\n  const hancelMultiSelect = () => {\n    multipleModeBarRef.current?.show()\n    listRef.current?.setIsMultiSelectMode(true)\n  }\n  const hancelSwitchSelectMode = (mode: SelectMode) => {\n    multipleModeBarRef.current?.setSwitchMode(mode)\n    listRef.current?.setSelectMode(mode)\n  }\n  const hancelExitSelect = () => {\n    multipleModeBarRef.current?.exitSelectMode()\n    listRef.current?.setIsMultiSelectMode(false)\n  }\n\n  const showMenu = (musicInfo: LX.Music.MusicInfoOnline, index: number, position: Position) => {\n    listMenuRef.current?.show({\n      musicInfo,\n      index,\n      single: false,\n      selectedList: listRef.current!.getSelectedList(),\n    }, position)\n  }\n  const handleAddMusic = (info: SelectInfo) => {\n    if (info.selectedList.length) {\n      listMusicMultiAddRef.current?.show({ selectedList: info.selectedList, listId: '', isMove: false })\n    } else {\n      listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: '', isMove: false })\n    }\n  }\n\n  return (\n    <View style={styles.container}>\n      <View style={{ flex: 1 }}>\n        <List\n          ref={listRef}\n          onShowMenu={showMenu}\n          onMuiltSelectMode={hancelMultiSelect}\n          onSelectAll={isAll => multipleModeBarRef.current?.setIsSelectAll(isAll)}\n          onRefresh={onRefresh}\n          onLoadMore={onLoadMore}\n          onPlayList={onPlayList}\n          progressViewOffset={progressViewOffset}\n          ListHeaderComponent={ListHeaderComponent}\n          checkHomePagerIdle={checkHomePagerIdle}\n          rowType={rowType}\n        />\n        <MultipleModeBar\n          ref={multipleModeBarRef}\n          onSwitchMode={hancelSwitchSelectMode}\n          onSelectAll={isAll => listRef.current?.selectAll(isAll)}\n          onExitSelectMode={hancelExitSelect}\n        />\n      </View>\n      <ListMusicAdd ref={listMusicAddRef} onAdded={() => { hancelExitSelect() }} />\n      <ListMusicMultiAdd ref={listMusicMultiAddRef} onAdded={() => { hancelExitSelect() }} />\n      <ListMenu\n        ref={listMenuRef}\n        onPlay={info => { handlePlay(info.musicInfo) }}\n        onPlayLater={info => { hancelExitSelect(); handlePlayLater(info.musicInfo, info.selectedList, hancelExitSelect) }}\n        onCopyName={info => { handleShare(info.musicInfo) }}\n        onAdd={handleAddMusic}\n        onMusicSourceDetail={info => { void handleShowMusicSourceDetail(info.musicInfo) }}\n        onDislikeMusic={info => { void handleDislikeMusic(info.musicInfo) }}\n      />\n      {/* <LoadingMask ref={loadingMaskRef} /> */}\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    overflow: 'hidden',\n  },\n  list: {\n    flex: 1,\n  },\n  exitMultipleModeBtn: {\n    height: 40,\n  },\n})\n\n"
  },
  {
    "path": "src/components/OnlineList/listAction.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\nimport { addListMusics } from '@/core/list'\nimport { playList, playNext } from '@/core/player/player'\nimport { addTempPlayList } from '@/core/player/tempPlayList'\nimport settingState from '@/store/setting/state'\nimport { getListMusicSync } from '@/utils/listManage'\nimport { confirmDialog, openUrl, shareMusic, toast } from '@/utils/tools'\nimport { addDislikeInfo, hasDislike } from '@/core/dislikeList'\nimport playerState from '@/store/player/state'\nimport musicSdk from '@/utils/musicSdk'\nimport { toOldMusicInfo } from '@/utils'\n\nexport const handlePlay = (musicInfo: LX.Music.MusicInfoOnline) => {\n  void addListMusics(LIST_IDS.DEFAULT, [musicInfo], settingState.setting['list.addMusicLocationType']).then(() => {\n    const index = getListMusicSync(LIST_IDS.DEFAULT).findIndex(m => m.id == musicInfo.id)\n    if (index < 0) return\n    void playList(LIST_IDS.DEFAULT, index)\n  })\n}\nexport const handlePlayLater = (musicInfo: LX.Music.MusicInfoOnline, selectedList: LX.Music.MusicInfoOnline[], onCancelSelect: () => void) => {\n  if (selectedList.length) {\n    addTempPlayList(selectedList.map(s => ({ listId: '', musicInfo: s })))\n    onCancelSelect()\n  } else {\n    addTempPlayList([{ listId: '', musicInfo }])\n  }\n}\n\n\nexport const handleShare = (musicInfo: LX.Music.MusicInfoOnline) => {\n  shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo)\n}\n\nexport const handleShowMusicSourceDetail = async(minfo: LX.Music.MusicInfoOnline) => {\n  const url = musicSdk[minfo.source as LX.OnlineSource]?.getMusicDetailPageUrl(toOldMusicInfo(minfo))\n  if (!url) return\n  void openUrl(url)\n}\n\n\nexport const handleDislikeMusic = async(musicInfo: LX.Music.MusicInfoOnline) => {\n  const confirm = await confirmDialog({\n    message: musicInfo.singer ? global.i18n.t('lists_dislike_music_singer_tip', { name: musicInfo.name, singer: musicInfo.singer }) : global.i18n.t('lists_dislike_music_tip', { name: musicInfo.name }),\n    cancelButtonText: global.i18n.t('cancel_button_text_2'),\n    confirmButtonText: global.i18n.t('confirm_button_text'),\n    bgClose: false,\n  })\n  if (!confirm) return\n  await addDislikeInfo([{ name: musicInfo.name, singer: musicInfo.singer }])\n  toast(global.i18n.t('lists_dislike_music_add_tip'))\n  if (hasDislike(playerState.playMusicInfo.musicInfo)) {\n    void playNext(true)\n  }\n}\n\n"
  },
  {
    "path": "src/components/PageContent.tsx",
    "content": "// import { useEffect, useState } from 'react'\nimport { View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport ImageBackground from '@/components/common/ImageBackground'\nimport { useWindowSize } from '@/utils/hooks'\nimport { useMemo } from 'react'\nimport { scaleSizeAbsHR } from '@/utils/pixelRatio'\nimport { defaultHeaders } from './common/Image'\nimport SizeView from './SizeView'\nimport { useBgPic } from '@/store/common/hook'\n\ninterface Props {\n  children: React.ReactNode\n}\n\nconst BLUR_RADIUS = Math.max(scaleSizeAbsHR(18), 10)\n\nexport default ({ children }: Props) => {\n  const theme = useTheme()\n  const windowSize = useWindowSize()\n  const pic = useBgPic()\n  // const [wh, setWH] = useState<{ width: number | string, height: number | string }>({ width: '100%', height: Dimensions.get('screen').height })\n\n  // 固定宽高度 防止弹窗键盘时大小改变导致背景被缩放\n  // useEffect(() => {\n  //   const onChange = () => {\n  //     setWH({ width: '100%', height: '100%' })\n  //   }\n\n  //   const changeEvent = Dimensions.addEventListener('change', onChange)\n  //   return () => {\n  //     changeEvent.remove()\n  //   }\n  // }, [])\n  // const handleLayout = (e: LayoutChangeEvent) => {\n  //   // console.log('handleLayout', e.nativeEvent)\n  //   // console.log(Dimensions.get('screen'))\n  //   setWH({ width: e.nativeEvent.layout.width, height: Dimensions.get('screen').height })\n  // }\n  // console.log('render page content')\n\n  const themeComponent = useMemo(() => (\n    <View style={{ flex: 1, overflow: 'hidden' }}>\n      <ImageBackground\n        style={{ position: 'absolute', left: 0, top: 0, height: windowSize.height, width: windowSize.width, backgroundColor: theme['c-content-background'] }}\n        source={theme['bg-image']}\n        resizeMode=\"cover\"\n      >\n      </ImageBackground>\n      <View style={{ flex: 1, flexDirection: 'column', backgroundColor: theme['c-main-background'] }}>\n        {children}\n      </View>\n    </View>\n  ), [children, theme, windowSize.height, windowSize.width])\n  const picComponent = useMemo(() => {\n    return (\n      <View style={{ flex: 1, overflow: 'hidden' }}>\n        <ImageBackground\n          style={{ position: 'absolute', left: 0, top: 0, height: windowSize.height, width: windowSize.width, backgroundColor: theme['c-content-background'] }}\n          source={{ uri: pic!, headers: defaultHeaders }}\n          resizeMode=\"cover\"\n          blurRadius={BLUR_RADIUS}\n        >\n          <View style={{ flex: 1, flexDirection: 'column', backgroundColor: theme['c-content-background'], opacity: 0.76 }}></View>\n        </ImageBackground>\n        <View style={{ flex: 1, flexDirection: 'column' }}>\n          {children}\n        </View>\n      </View>\n    )\n  }, [children, pic, theme, windowSize.height, windowSize.width])\n\n  return (\n    <>\n      <SizeView />\n      {pic ? picComponent : themeComponent}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/SearchTipList/List.tsx",
    "content": "import { useState, forwardRef, useImperativeHandle, type Ref } from 'react'\nimport { FlatList, type FlatListProps } from 'react-native'\n\n// import InsetShadow from 'react-native-inset-shadow'\n\nexport type ItemT<T> = FlatListProps<T>['data']\n\nexport type ListProps<T> = Pick<FlatListProps<T>,\n| 'renderItem'\n| 'maxToRenderPerBatch'\n| 'windowSize'\n| 'initialNumToRender'\n| 'keyExtractor'\n| 'getItemLayout'\n| 'keyboardShouldPersistTaps'\n>\n\nexport interface ListType<T> {\n  setList: (list: T[]) => void\n}\n\nconst List = <T extends ItemT<T>>(props: ListProps<T>, ref: Ref<ListType<T>>) => {\n  const [list, setList] = useState<T[]>([])\n  useImperativeHandle(ref, () => ({\n    setList(list) {\n      setList(list)\n    },\n  }))\n\n  return <FlatList removeClippedSubviews={true} keyboardShouldPersistTaps={'always'} {...props} data={list} />\n}\n\nexport default forwardRef(List) as\n  <M,>(p: ListProps<M> & { ref?: Ref<ListType<M>> }) => JSX.Element | null\n\n\n"
  },
  {
    "path": "src/components/SearchTipList/index.tsx",
    "content": "import { useRef, useState, useCallback, useMemo, forwardRef, useImperativeHandle, type Ref } from 'react'\nimport { StyleSheet, View, Animated } from 'react-native'\n// import PropTypes from 'prop-types'\n// import { AppColors } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\nimport List, { type ItemT, type ListProps, type ListType } from './List'\n// import InsetShadow from 'react-native-inset-shadow'\n\nexport interface SearchTipListProps<T> extends ListProps<T> {\n  onPressBg?: () => void\n}\nexport interface SearchTipListType<T> {\n  setList: (list: T[]) => void\n  setHeight: (height: number) => void\n}\n\nconst noop = () => {}\n\nconst Component = <T extends ItemT<T>>({ onPressBg = noop, ...props }: SearchTipListProps<T>, ref: Ref<SearchTipListType<T>>) => {\n  const theme = useTheme()\n  const translateY = useRef(new Animated.Value(0)).current\n  const scaleY = useRef(new Animated.Value(0)).current\n  const [visible, setVisible] = useState(false)\n  const [animatePlayed, setAnimatPlayed] = useState(true)\n  const listRef = useRef<ListType<T>>(null)\n  const prevListRef = useRef<T[]>([])\n  const heightRef = useRef(0)\n\n  useImperativeHandle(ref, () => ({\n    setList(list) {\n      if (prevListRef.current.length) {\n        if (!list.length) handleHide()\n      } else if (list.length) handleShow()\n      prevListRef.current = list\n      requestAnimationFrame(() => {\n        listRef.current?.setList(list)\n      })\n    },\n    setHeight(height) {\n      heightRef.current = height\n    },\n  }))\n\n\n  const handleShow = useCallback(() => {\n    // console.log('handleShow', height, visible)\n    if (!heightRef.current) return\n    setVisible(true)\n    setAnimatPlayed(false)\n    requestAnimationFrame(() => {\n      translateY.setValue(-heightRef.current / 2)\n      scaleY.setValue(0)\n\n      Animated.parallel([\n      // Animated.timing(fade, {\n      //   toValue: 1,\n      //   duration: 300,\n      //   useNativeDriver: true,\n      // }),\n        Animated.timing(translateY, {\n          toValue: 0,\n          duration: 300,\n          useNativeDriver: true,\n        }),\n        Animated.timing(scaleY, {\n          toValue: 1,\n          duration: 300,\n          useNativeDriver: true,\n        }),\n      ]).start(() => {\n        setAnimatPlayed(true)\n      })\n    })\n  }, [translateY, scaleY])\n\n  const handleHide = useCallback(() => {\n    setAnimatPlayed(false)\n    Animated.parallel([\n      // Animated.timing(fade, {\n      //   toValue: 0,\n      //   duration: 200,\n      //   useNativeDriver: true,\n      // }),\n      Animated.timing(translateY, {\n        toValue: -heightRef.current / 2,\n        duration: 300,\n        useNativeDriver: true,\n      }),\n      Animated.timing(scaleY, {\n        toValue: 0,\n        duration: 300,\n        useNativeDriver: true,\n      }),\n    ]).start((finished) => {\n      // console.log(finished)\n      if (!finished) return\n      setVisible(false)\n      setAnimatPlayed(true)\n    })\n  }, [translateY, scaleY])\n\n\n  const component = useMemo(() => (\n    <Animated.View\n      style={{\n        ...styles.anima,\n        transform: [\n          { translateY },\n          { scaleY },\n        ],\n      }}>\n      <View style={{ ...styles.container, backgroundColor: theme['c-content-background'] }}>\n        <List ref={listRef} {...props} />\n      </View>\n      <View style={styles.blank} onTouchStart={onPressBg}></View>\n    </Animated.View>\n  ), [onPressBg, props, scaleY, theme, translateY])\n\n  return !visible && animatePlayed ? null : component\n}\n\nexport default forwardRef(Component) as\n  <T,>(p: SearchTipListProps<T> & { ref?: Ref<SearchTipListType<T>> }) => JSX.Element | null\n\nconst styles = StyleSheet.create({\n  anima: {\n    position: 'absolute',\n    left: 0,\n    top: 0,\n    height: '100%',\n    width: '100%',\n    zIndex: 10,\n  },\n  container: {\n    flex: 0,\n    // flexGrow: 0,\n    // borderBottomWidth: BorderWidths.normal,\n    elevation: 2,\n    maxHeight: '80%',\n  },\n  blank: {\n    flex: 1,\n    flexGrow: 1,\n    // backgroundColor: 'transparent',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n})\n"
  },
  {
    "path": "src/components/SizeView.tsx",
    "content": "import { memo, useCallback, useRef, useEffect } from 'react'\nimport { type LayoutChangeEvent, StyleSheet, View, StatusBar, Dimensions } from 'react-native'\nimport commonState from '@/store/common/state'\nimport settingState from '@/store/setting/state'\nimport { setStatusbarHeight } from '@/core/common'\nimport { windowSizeTools, getWindowSize } from '@/utils/windowSizeTools'\n\nconst getStatusbarHeight = (winHeight: number, layoutHeight: number) => {\n  const height = (!settingState.setting['common.alwaysKeepStatusbarHeight'] &&\n          parseFloat(winHeight.toFixed(2)) >= parseFloat(layoutHeight.toFixed(2)))\n    ? 0\n    : (StatusBar.currentHeight ?? 0)\n\n  return height\n}\n\nexport default memo(() => {\n  const currentHeightRef = useRef(commonState.statusbarHeight)\n  const sizeRef = useRef([0, 0])\n  const dimensionsChangedRef = useRef(true)\n  const handleLayout = useCallback(({ nativeEvent: { layout } }: LayoutChangeEvent | { nativeEvent: { layout: { width: number, height: number } } }) => {\n    // console.log('handleLayout')\n    if (!dimensionsChangedRef.current) return\n    void getWindowSize().then(size => {\n      dimensionsChangedRef.current = false\n      // console.log(layout, size)\n      sizeRef.current = [size.height, layout.height]\n      const height = getStatusbarHeight(size.height, layout.height)\n\n      if (currentHeightRef.current != height) {\n        currentHeightRef.current = height\n        setStatusbarHeight(height)\n      }\n      // console.log(layout, size)\n      const currentSize = windowSizeTools.getSize()\n      if (currentSize.width != layout.width || currentSize.height != layout.height) {\n        windowSizeTools.setWindowSize(layout.width, layout.height)\n      }\n    })\n  }, [])\n  useEffect(() => {\n    // let timeout: NodeJS.Timeout | null = null\n    const subscription = Dimensions.addEventListener('change', () => {\n      dimensionsChangedRef.current = true\n      // if (timeout) clearTimeout(timeout)\n      // timeout = setTimeout(() => {\n      //   timeout = null\n      //   viewRef.current?.measureInWindow((x, y, width, height) => {\n      //     handleLayout({ nativeEvent: { layout: { width, height } } })\n      //   })\n      // }, 100)\n    })\n\n    const handleSettingUpdate = (keys: Array<keyof LX.AppSetting>) => {\n      if (!keys.includes('common.alwaysKeepStatusbarHeight') || !sizeRef.current[1]) return\n      const height = getStatusbarHeight(sizeRef.current[0], sizeRef.current[1])\n\n      if (currentHeightRef.current != height) {\n        currentHeightRef.current = height\n        setStatusbarHeight(height)\n      }\n    }\n    global.state_event.on('configUpdated', handleSettingUpdate)\n\n    return () => {\n      subscription.remove()\n      global.state_event.off('configUpdated', handleSettingUpdate)\n    }\n  }, [])\n  return (<View style={StyleSheet.absoluteFill} onLayout={handleLayout} />)\n}, () => true)\n\n"
  },
  {
    "path": "src/components/SourceSelector.tsx",
    "content": "import { forwardRef, type Ref, useImperativeHandle, useMemo, useState } from 'react'\nimport { View } from 'react-native'\n\nimport DorpDownMenu, { type DorpDownMenuProps as _DorpDownMenuProps } from '@/components/common/DorpDownMenu'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\n\nimport { useSettingValue } from '@/store/setting/hook'\nimport { createStyle } from '@/utils/tools'\n\ntype Sources = Readonly<Array<LX.OnlineSource | 'all'>>\n\nexport interface SourceSelectorProps<S extends Sources> {\n  fontSize?: number\n  center?: _DorpDownMenuProps<any>['center']\n  onSourceChange: (source: S[number]) => void\n}\n\nexport interface SourceSelectorType<S extends Sources> {\n  setSourceList: (list: S, activeSource: S[number]) => void\n}\n\nexport const useSourceListI18n = (list: Sources) => {\n  const sourceNameType = useSettingValue('common.sourceNameType')\n  const t = useI18n()\n  return useMemo(() => {\n    return list.map(s => ({ label: t(`source_${sourceNameType}_${s}`), action: s }))\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [list, sourceNameType, t])\n}\n\nconst Component = <S extends Sources>({ fontSize = 15, center, onSourceChange }: SourceSelectorProps<S>, ref: Ref<SourceSelectorType<S>>) => {\n  const sourceNameType = useSettingValue('common.sourceNameType')\n  const [list, setList] = useState([] as unknown as S)\n  const [source, setSource] = useState<S[number]>('kw')\n  const t = useI18n()\n\n  useImperativeHandle(ref, () => ({\n    setSourceList(list, activeSource) {\n      setList(list)\n      setSource(activeSource)\n    },\n  }), [])\n\n  const sourceList_t = useSourceListI18n(list)\n\n  type DorpDownMenuProps = _DorpDownMenuProps<typeof sourceList_t>\n\n  const handleChangeSource: DorpDownMenuProps['onPress'] = ({ action }) => {\n    onSourceChange(action)\n    setSource(action)\n  }\n\n  return (\n    <DorpDownMenu\n      menus={sourceList_t}\n      center={center}\n      onPress={handleChangeSource}\n      fontSize={fontSize}\n      activeId={source}\n    >\n      <View style={styles.sourceMenu}>\n        <Text style={{ textAlign: center ? 'center' : 'left' }} numberOfLines={1} size={fontSize}>{t(`source_${sourceNameType}_${source}`)}</Text>\n      </View>\n    </DorpDownMenu>\n  )\n}\n\nexport default forwardRef(Component) as <S extends Sources>(p: SourceSelectorProps<S> & { ref?: Ref<SourceSelectorType<S>> }) => JSX.Element | null\n\n\nconst styles = createStyle({\n  sourceMenu: {\n    height: '100%',\n    justifyContent: 'center',\n    // paddingTop: 12,\n    // paddingBottom: 12,\n    paddingLeft: 15,\n    paddingRight: 15,\n    // backgroundColor: '#ccc',\n\n  },\n})\n"
  },
  {
    "path": "src/components/TimeoutExitEditModal.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useEffect } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { cancelTimeoutExit, getTimeoutExitTime, onTimeUpdate, startTimeoutExit, stopTimeoutExit, useTimeoutExitTimeInfo } from '@/core/player/timeoutExit'\nimport { useI18n } from '@/lang'\nimport CheckBox from './common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { updateSetting } from '@/core/common'\nimport settingState from '@/store/setting/state'\n\nconst MAX_MIN = 1440\nconst rxp = /([1-9]\\d*)/\nconst formatTime = (time: number) => {\n  // let d = parseInt(time / 86400)\n  // d = d ? d.toString() + ':' : ''\n  // time = time % 86400\n  let h = Math.trunc(time / 3600)\n  let hStr = 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 `${hStr}${m}:${s}`\n}\nconst Status = () => {\n  const theme = useTheme()\n  const t = useI18n()\n  const exitTimeInfo = useTimeoutExitTimeInfo()\n  return (\n    <View style={styles.tip}>\n      {\n      exitTimeInfo.time < 0\n        ? (\n            <Text>{t('timeout_exit_tip_off')}</Text>\n          )\n        : (\n            <Text>{t('timeout_exit_tip_on', { time: formatTime(exitTimeInfo.time) })}</Text>\n          )\n      }\n      {exitTimeInfo.isPlayedStop ? <Text color={theme['c-font-label']} size={13}>{t('timeout_exit_btn_wait_tip')}</Text> : null}\n    </View>\n  )\n}\n\n\ninterface TimeInputType {\n  setText: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst TimeInput = forwardRef<TimeInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n  const t = useI18n()\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setText(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={t('timeout_exit_input_tip')}\n      value={text}\n      onChangeText={setText}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\n\nconst Setting = () => {\n  const t = useI18n()\n  const timeoutExitPlayed = useSettingValue('player.timeoutExitPlayed')\n  const onCheckChange = (check: boolean) => {\n    updateSetting({ 'player.timeoutExitPlayed': check })\n  }\n\n  return (\n    <View style={styles.checkbox}>\n      <CheckBox check={timeoutExitPlayed} label={t('timeout_exit_label_isPlayed')} onChange={onCheckChange} />\n    </View>\n  )\n}\n\nexport const useTimeInfo = () => {\n  const [exitTimeInfo, setExitTimeInfo] = useState({\n    cancelText: '',\n    confirmText: '',\n    isPlayedStop: false,\n    active: false,\n  })\n  const t = useI18n()\n\n  useEffect(() => {\n    let active: boolean | null = null\n    const remove = onTimeUpdate((time, isPlayedStop) => {\n      if (time < 0) {\n        if (active) {\n          setExitTimeInfo({\n            cancelText: isPlayedStop ? t('timeout_exit_btn_wait_cancel') : '',\n            confirmText: '',\n            isPlayedStop,\n            active: false,\n          })\n          active = false\n        }\n      } else {\n        if (active !== true) {\n          setExitTimeInfo({\n            cancelText: t('timeout_exit_btn_cancel'),\n            confirmText: t('timeout_exit_btn_update'),\n            isPlayedStop,\n            active: true,\n          })\n          active = true\n        }\n      }\n    })\n\n    return () => {\n      remove()\n    }\n  }, [t])\n\n  return exitTimeInfo\n}\n\nexport interface TimeoutExitEditModalType {\n  show: () => void\n}\ninterface TimeoutExitEditModalProps {\n  timeInfo: ReturnType<typeof useTimeInfo>\n}\n\nexport default forwardRef<TimeoutExitEditModalType, TimeoutExitEditModalProps>(({ timeInfo }, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const timeInputRef = useRef<TimeInputType>(null)\n  const [visible, setVisible] = useState(false)\n  const t = useI18n()\n\n  const handleShow = () => {\n    alertRef.current?.setVisible(true)\n    requestAnimationFrame(() => {\n      if (settingState.setting['player.timeoutExit']) timeInputRef.current?.setText(settingState.setting['player.timeoutExit'])\n      //   setTimeout(() => {\n      //     timeInputRef.current?.focus()\n      //   }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show() {\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleCancel = () => {\n    if (timeInfo.isPlayedStop) {\n      cancelTimeoutExit()\n      return\n    }\n    if (!timeInfo.active) return\n    stopTimeoutExit()\n    toast(t('timeout_exit_tip_cancel'))\n  }\n  const handleConfirm = () => {\n    let timeStr = timeInputRef.current?.getText() ?? ''\n    if (rxp.test(timeStr)) {\n      // if (timeStr != RegExp.$1) toast(t('input_error'))\n      timeStr = RegExp.$1\n      if (parseInt(timeStr) > MAX_MIN) {\n        toast(t('timeout_exit_tip_max', { num: MAX_MIN }))\n        // timeStr = timeStr.substring(0, timeStr.length - 1)\n        return\n      }\n    } else {\n      if (timeStr.length) toast(t('input_error'))\n      timeStr = ''\n    }\n    if (!timeStr) return\n    const time = parseInt(timeStr)\n    cancelTimeoutExit()\n    startTimeoutExit(time * 60)\n    toast(t('timeout_exit_tip_on', { time: formatTime(getTimeoutExitTime()) }))\n    updateSetting({ 'player.timeoutExit': String(time) })\n    alertRef.current?.setVisible(false)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          cancelText={timeInfo.cancelText}\n          confirmText={timeInfo.confirmText}\n          onCancel={handleCancel}\n          onConfirm={handleConfirm}\n        >\n          <View style={styles.alertContent}>\n            <Status />\n            <View style={styles.inputContent}>\n              <TimeInput ref={timeInputRef} />\n              <Text style={styles.inputLabel}>{t('timeout_exit_min')}</Text>\n            </View>\n            <Setting />\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\nconst styles = createStyle({\n  alertContent: {\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  tip: {\n    marginBottom: 8,\n  },\n  checkbox: {\n    marginTop: 5,\n  },\n  inputContent: {\n    marginTop: 8,\n    flex: 1,\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // borderRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n  },\n  inputLabel: {\n    marginLeft: 8,\n  },\n})\n\n\n"
  },
  {
    "path": "src/components/common/Badge.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from './Text'\n// const menuItemHeight = 42\n// const menuItemWidth = 100\n\nconst styles = createStyle({\n  text: {\n    // paddingLeft: 4,\n    // paddingRight: 4,\n    // borderRadius: 2,\n    // lineHeight: 12,\n    // marginTop: 2,\n    marginRight: 5,\n    fontWeight: '400',\n    // marginRight: 5,\n    // marginBottom: 2,\n    // alignSelf: 'flex-start',\n    alignSelf: 'center',\n  },\n})\n\nexport type BadgeType = 'normal' | 'secondary' | 'tertiary'\n\nexport default memo(({ type = 'normal', children }: {\n  type?: BadgeType\n  children: string\n}) => {\n  const theme = useTheme()\n  // console.log(visible)\n  const colors = useMemo(() => {\n    const colors = { textColor: '' }\n    switch (type) {\n      case 'normal':\n        // colors.bgColor = theme.primary\n        colors.textColor = theme['c-badge-primary']\n        break\n      case 'secondary':\n        // colors.bgColor = theme.primary\n        colors.textColor = theme['c-badge-secondary']\n        break\n      case 'tertiary':\n        // colors.bgColor = theme.primary\n        colors.textColor = theme['c-badge-tertiary']\n        break\n    }\n    return colors\n  }, [type, theme])\n\n  return <Text style={styles.text} size={9} color={colors.textColor}>{children}</Text>\n})\n\n"
  },
  {
    "path": "src/components/common/Button.tsx",
    "content": "import { useTheme } from '@/store/theme/hook'\nimport { useMemo, useRef, useImperativeHandle, forwardRef } from 'react'\nimport { Pressable, type PressableProps, StyleSheet, type View, type ViewProps } from 'react-native'\n// import { AppColors } from '@/theme'\n\n\nexport interface BtnProps extends PressableProps {\n  ripple?: PressableProps['android_ripple']\n  style?: ViewProps['style']\n  onChangeText?: (value: string) => void\n  onClearText?: () => void\n  children: React.ReactNode\n}\n\n\nexport interface BtnType {\n  measure: (callback: (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void) => void\n}\n\nexport default forwardRef<BtnType, BtnProps>(({ ripple: propsRipple = {}, disabled, children, style, ...props }, ref) => {\n  const theme = useTheme()\n  const btnRef = useRef<View>(null)\n  const ripple = useMemo(() => ({\n    color: theme['c-primary-light-200-alpha-700'],\n    ...propsRipple,\n  }), [theme, propsRipple])\n\n  useImperativeHandle(ref, () => ({\n    measure(callback) {\n      btnRef.current?.measure(callback)\n    },\n  }))\n\n  return (\n    <Pressable\n      android_ripple={ripple}\n      disabled={disabled}\n      style={StyleSheet.compose({ opacity: disabled ? 0.3 : 1 }, style)}\n      {...props}\n      ref={btnRef}\n    >\n      {children}\n    </Pressable>\n  )\n})\n\n"
  },
  {
    "path": "src/components/common/ButtonPrimary.tsx",
    "content": "import { memo } from 'react'\n\nimport Button, { type BtnProps } from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\n\nexport interface ButtonProps extends BtnProps {\n  size?: number\n}\n\nexport default memo(({ disabled, size = 14, onPress, children }: ButtonProps) => {\n  const theme = useTheme()\n\n  return (\n    <Button style={{ ...styles.button, backgroundColor: theme['c-button-background'] }} onPress={onPress} disabled={disabled}>\n      <Text size={size} color={theme['c-button-font']}>{children}</Text>\n    </Button>\n  )\n})\n\nconst styles = createStyle({\n  button: {\n    paddingHorizontal: 10,\n    paddingVertical: 5,\n    borderRadius: 4,\n    marginRight: 10,\n  },\n})\n"
  },
  {
    "path": "src/components/common/CheckBox/Checkbox.tsx",
    "content": "import * as React from 'react'\nimport {\n  Animated,\n  type GestureResponderEvent,\n  StyleSheet,\n  View,\n  Pressable,\n} from 'react-native'\n\nimport { Icon } from '../Icon'\nimport { createStyle } from '@/utils/tools'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nexport interface Props {\n  /**\n   * Status of checkbox.\n   */\n  status: 'checked' | 'unchecked' | 'indeterminate'\n  /**\n   * Whether checkbox is disabled.\n   */\n  disabled?: boolean\n  /**\n   * Function to execute on press.\n   */\n  onPress?: (e: GestureResponderEvent) => void\n\n  size?: number\n\n  /**\n   * Custom color for checkbox.\n   */\n  tintColors: {\n    true: string\n    false: string\n  }\n}\n\nconst ANIMATION_DURATION = 200\nconst PADDING = scaleSizeW(4)\n\n/**\n * Checkboxes allow the selection of multiple options from a set.\n * This component follows platform guidelines for Android, but can be used\n * on any platform.\n */\nconst Checkbox = ({\n  status,\n  disabled,\n  size = 1,\n  onPress,\n  tintColors,\n  ...rest\n}: Props) => {\n  const checked = status === 'checked'\n  const indeterminate = status === 'indeterminate'\n\n  const icon = indeterminate\n    ? 'minus-box'\n    : 'checkbox-marked'\n\n  const { current: scaleAnim } = React.useRef<Animated.Value>(\n    new Animated.Value(checked ? 1 : 0),\n  )\n\n  const isFirstRendering = React.useRef<boolean>(true)\n\n\n  React.useEffect(() => {\n    // Do not run animation on very first rendering\n    if (isFirstRendering.current) {\n      isFirstRendering.current = false\n      return\n    }\n\n    Animated.timing(scaleAnim, {\n      toValue: checked ? 1 : 0,\n      duration: ANIMATION_DURATION,\n      useNativeDriver: true,\n    }).start()\n  }, [checked, scaleAnim])\n\n\n  return (\n    <Pressable\n      {...rest}\n      onPress={onPress}\n      disabled={disabled}\n      accessibilityRole=\"checkbox\"\n      accessibilityState={{ disabled, checked }}\n      accessibilityLiveRegion=\"polite\"\n      style={{ ...styles.container, padding: PADDING, marginLeft: -PADDING }}\n    >\n      <Icon\n        allowFontScaling={false}\n        name=\"checkbox-blank-outline\"\n        size={24 * size}\n        color={tintColors.false}\n      />\n      <View style={[StyleSheet.absoluteFill, styles.fillContainer]}>\n        <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>\n          <Icon\n            allowFontScaling={false}\n            name={icon}\n            size={24 * size}\n            color={tintColors.true}\n          />\n        </Animated.View>\n      </View>\n    </Pressable>\n  )\n}\n\nCheckbox.displayName = 'Checkbox'\n\nconst styles = createStyle({\n  container: {\n    alignItems: 'center',\n    justifyContent: 'center',\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  fillContainer: {\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n\nexport default Checkbox\n\n"
  },
  {
    "path": "src/components/common/CheckBox/index.tsx",
    "content": "import { useCallback, useEffect, useMemo, useState } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\nimport CheckBox from './Checkbox'\n\nimport { createStyle, tipDialog } from '@/utils/tools'\nimport { scaleSizeH, scaleSizeW } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '../Text'\nimport { Icon } from '../Icon'\n\nexport interface CheckBoxProps {\n  check: boolean\n  label?: string\n  children?: React.ReactNode\n  onChange: (check: boolean) => void\n  disabled?: boolean\n  need?: boolean\n  size?: number\n  marginRight?: number\n  marginBottom?: number\n\n  helpTitle?: string\n  helpDesc?: string\n}\n\nexport default ({ check, label, children, onChange, helpTitle, helpDesc, disabled = false, need = false, marginRight = 0, marginBottom = 0, size = 1 }: CheckBoxProps) => {\n  const theme = useTheme()\n  const [isDisabled, setDisabled] = useState(false)\n  const tintColors = {\n    true: theme['c-primary'],\n    false: theme['c-600'],\n  }\n  const disabledTintColors = {\n    true: theme['c-primary-alpha-600'],\n    false: theme['c-400'],\n  }\n\n  useEffect(() => {\n    if (need) {\n      if (check) {\n        if (!isDisabled) setDisabled(true)\n      } else {\n        if (isDisabled) setDisabled(false)\n      }\n    } else {\n      isDisabled && setDisabled(false)\n    }\n  }, [check, need, isDisabled])\n\n  const handleLabelPress = useCallback(() => {\n    if (isDisabled) return\n    onChange?.(!check)\n  }, [isDisabled, onChange, check])\n\n  const helpComponent = useMemo(() => {\n    const handleShowHelp = () => {\n      void tipDialog({\n        title: helpTitle ?? '',\n        message: helpDesc,\n        btnText: global.i18n.t('understand'),\n      })\n    }\n    return (helpTitle ?? helpDesc) ? (\n      <TouchableOpacity style={styles.helpBtn} onPress={handleShowHelp}>\n        <Icon size={15 * size} name=\"help\" />\n      </TouchableOpacity>\n    ) : null\n  }, [helpTitle, helpDesc, size])\n\n\n  const contentStyle = { ...styles.content, marginBottom: scaleSizeH(marginBottom) }\n  const labelStyle = { ...styles.label, marginRight: scaleSizeW(marginRight) }\n\n  return (\n    disabled\n      ? (\n          <View style={contentStyle}>\n            <CheckBox status={check ? 'checked' : 'unchecked'} disabled={true} tintColors={disabledTintColors} size={size} />\n            <View style={labelStyle}>{label ? <Text style={styles.name} color={theme['c-500']} size={15 * size}>{label}</Text> : children}</View>\n            {helpComponent}\n          </View>\n        )\n      : (\n          <View style={contentStyle}>\n            <CheckBox status={check ? 'checked' : 'unchecked'} disabled={isDisabled} onPress={handleLabelPress} tintColors={tintColors} size={size} />\n            <TouchableOpacity style={labelStyle} activeOpacity={0.3} onPress={handleLabelPress}>\n              {label ? <Text style={styles.name} size={15 * size}>{label}</Text> : children}\n            </TouchableOpacity>\n            {helpComponent}\n          </View>\n        )\n  )\n}\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    marginRight: 15,\n    alignItems: 'center',\n    flexDirection: 'row',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  checkbox: {\n    flex: 0,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  label: {\n    flexGrow: 0,\n    flexShrink: 1,\n    // marginRight: 15,\n    // alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    paddingRight: 3,\n  },\n  name: {\n    marginTop: 2,\n  },\n  helpBtn: {\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n    paddingHorizontal: 8,\n    paddingVertical: 3,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/List.tsx",
    "content": "import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'\nimport { View } from 'react-native'\nimport { externalStorageDirectoryPath, readDir } from '@/utils/fs'\nimport { createStyle, toast } from '@/utils/tools'\n// import { useTranslation } from '@/plugins/i18n'\nimport Modal, { type ModalType } from '@/components/common/Modal'\n\nimport Header from './components/Header'\nimport Main from './components/Main'\nimport Footer from './components/Footer'\nimport { sizeFormate } from '@/utils'\nimport { useTheme } from '@/store/theme/hook'\nimport { type PathItem } from './components/ListItem'\n// import { getSelectedManagedFolder } from '@/utils/data'\n// let prevPath = externalStorageDirectoryPath\n\nconst parentDirInfo = new Map<string, string>()\nconst caches = new Map<string, PathItem[]>()\n\nconst handleReadDir = async(path: string, dirOnly: boolean, filter?: string[], isRefresh = false) => {\n  let filterRxp = filter?.length ? new RegExp(`\\\\.(${filter.join('|')})$`, 'i') : null\n  const cacheKey = `${path}_${dirOnly ? 'true' : 'false'}_${filter ? filter.toString() : 'null'}`\n  if (!isRefresh && caches.has(cacheKey)) return caches.get(cacheKey)!\n  return readDir(path).then(paths => {\n    // console.log('read')\n    // prevPath = path\n    let list = [] as PathItem[]\n    // console.log(paths)\n    for (const path of paths) {\n      // console.log(path)\n      if (filterRxp != null && path.isFile && !filterRxp.test(path.name)) continue\n\n      const isDirectory = path.isDirectory\n      if (dirOnly) {\n        list.push({\n          name: path.name,\n          path: path.path,\n          mtime: new Date(path.lastModified),\n          size: path.size,\n          isDir: isDirectory,\n          sizeText: isDirectory ? '' : sizeFormate(path.size ?? 0),\n          disabled: path.isFile,\n        })\n      } else {\n        list.push({\n          name: path.name,\n          path: path.path,\n          mtime: new Date(path.lastModified),\n          size: path.size,\n          isDir: isDirectory,\n          sizeText: isDirectory ? '' : sizeFormate(path.size ?? 0),\n        })\n      }\n    }\n\n    list.sort((a, b) => a.name.charCodeAt(0) - b.name.charCodeAt(0))\n    let fileList = [] as PathItem[]\n    list = [...list.filter(i => {\n      if (i.isDir) return true\n      else {\n        fileList.push(i)\n        return false\n      }\n    }), ...fileList]\n    caches.set(cacheKey, list)\n    return list\n  })\n}\n\ninterface ReadOptions {\n  title: string\n  dirOnly: boolean\n  filter?: string[]\n}\nconst initReadOptions = {}\nexport interface ListProps {\n  onConfirm: (path: string) => void\n  onHide?: () => void\n}\n\nexport interface ListType {\n  show: (title: string, dir?: string, dirOnly?: boolean, filter?: string[]) => void\n  hide: () => void\n}\n\nexport default forwardRef<ListType, ListProps>(({\n  onConfirm,\n  onHide = () => {},\n}: ListProps, ref) => {\n  const [path, setPath] = useState('')\n  const [list, setList] = useState<PathItem[]>([])\n  const isUnmountedRef = useRef(true)\n  const readOptions = useRef<ReadOptions>(initReadOptions as ReadOptions)\n  const [isReading, setIsReading] = useState(false)\n  const modalRef = useRef<ModalType>(null)\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    show(title, dir = '', dirOnly = false, filter) {\n      readOptions.current = {\n        title,\n        dirOnly,\n        filter,\n      }\n      modalRef.current?.setVisible(true)\n      // void getSelectedManagedFolder().then(uri => {\n      //   if (!uri) return\n      //   void readDir(uri, dirOnly, filter)\n      // })\n      if (dir) void readDir(dir, dirOnly, filter)\n      else void readDir(externalStorageDirectoryPath, dirOnly, filter)\n    },\n    hide() {\n      modalRef.current?.setVisible(false)\n    },\n  }))\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  const readDir = async(newPath: string, dirOnly: boolean, filter?: string[], isRefresh?: boolean, isOpen?: boolean): Promise<PathItem[]> => {\n    if (isReading) return []\n    setIsReading(true)\n    return handleReadDir(newPath, dirOnly, filter, isRefresh).then(list => {\n      if (isUnmountedRef.current) return []\n      if (!isOpen && newPath != path && newPath.startsWith(path)) parentDirInfo.set(newPath, path)\n      setList(list)\n      setPath(newPath)\n      return list\n    }).catch((err: any) => {\n      toast(`Read dir error: ${err.message as string}`, 'long')\n      // console.log('prevPath', prevPath)\n      // if (isReadingDir.current) return\n      // setPath(prevPath)\n      return []\n    }).finally(() => {\n      setIsReading(false)\n    })\n  }\n\n  const onSetPath = (pathInfo: PathItem) => {\n    // console.log('onSetPath')\n    if (pathInfo.isDir) {\n      void readDir(pathInfo.path, readOptions.current.dirOnly, readOptions.current.filter)\n    } else {\n      onConfirm(pathInfo.path)\n      // setPath(pathInfo.path)\n    }\n  }\n  const handleConfirm = () => {\n    if (!path) return\n    onConfirm(path)\n  }\n\n  const toParentDir = () => {\n    const parentPath = parentDirInfo.get(path)\n    if (parentPath) {\n      void readDir(parentPath, readOptions.current.dirOnly, readOptions.current.filter)\n    } else {\n      toast('Permission denied')\n    }\n  }\n\n  const handleHide = () => {\n    modalRef.current?.setVisible(false)\n    onHide()\n  }\n\n  // const dirList = useMemo(() => [parentDir, ...list], [list, parentDir])\n\n  return (\n    <Modal ref={modalRef} bgHide={false} statusBarPadding={false}>\n      <View style={{ ...styles.container, backgroundColor: theme['c-content-background'] }}>\n        <Header\n          onRefreshDir={async(path) => readDir(path, readOptions.current.dirOnly, readOptions.current.filter, true)}\n          onOpenDir={async(path) => readDir(path, readOptions.current.dirOnly, readOptions.current.filter, false, true)}\n          title={readOptions.current.title}\n          path={path} />\n        <Main list={list} toParentDir={toParentDir} onSetPath={onSetPath} loading={isReading} />\n        <Footer onConfirm={handleConfirm} onHide={handleHide} dirOnly={readOptions.current.dirOnly} />\n      </View>\n    </Modal>\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/Footer.tsx",
    "content": "import { memo } from 'react'\nimport { View, StyleSheet } from 'react-native'\nimport Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\n\nexport default memo(({ onConfirm, onHide, dirOnly }: {\n  onConfirm: () => void\n  onHide: () => void\n  dirOnly: boolean\n}) => {\n  const t = useI18n()\n  const theme = useTheme()\n\n  return (\n    <View style={{ ...styles.footer, backgroundColor: theme['c-content-background'] }}>\n      <Button style={{ ...styles.footerBtn, width: dirOnly ? '50%' : '100%' }} onPress={onHide}>\n        <Text color={theme['c-button-font']}>{t('cancel')}</Text>\n      </Button>\n      {dirOnly\n        ? <Button style={styles.footerBtn} onPress={onConfirm}>\n            <Text color={theme['c-button-font']}>{t('confirm')}</Text>\n          </Button>\n        : null\n      }\n    </View>\n  )\n})\n\nconst styles = StyleSheet.create({\n  footer: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: 'row',\n    // borderTopWidth: BorderWidths.normal,\n    elevation: 8,\n  },\n  footerBtn: {\n    width: '50%',\n    paddingTop: 16,\n    paddingBottom: 16,\n    alignItems: 'center',\n  },\n})\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/Header.tsx",
    "content": "import { memo, useCallback, useEffect, useRef } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\nimport Text from '@/components/common/Text'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { getExternalStoragePaths, stat } from '@/utils/fs'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { useStatusbarHeight } from '@/store/common/hook'\nimport NewFolderModal, { type NewFolderType } from './NewFolderModal'\nimport OpenStorageModal, { type OpenDirModalType } from './OpenStorageModal'\nimport type { PathItem } from './ListItem'\n\n\nexport default memo(({\n  title,\n  path,\n  onRefreshDir,\n  onOpenDir,\n}: {\n  title: string\n  path: string\n  onRefreshDir: (dir: string) => Promise<PathItem[]>\n  onOpenDir: (dir: string) => Promise<PathItem[]>\n}) => {\n  const theme = useTheme()\n  const newFolderTypeRef = useRef<NewFolderType>(null)\n  const openDirModalTypeRef = useRef<OpenDirModalType>(null)\n  const storagePathsRef = useRef<string[]>([])\n  const statusBarHeight = useStatusbarHeight()\n\n  const checkExternalStoragePath = useCallback(() => {\n    storagePathsRef.current = []\n    void getExternalStoragePaths().then(async(storagePaths) => {\n      for (const path of storagePaths) {\n        try {\n          if (!(await stat(path)).canRead) continue\n        } catch { continue }\n        storagePathsRef.current.push(path)\n      }\n    })\n  }, [])\n  useEffect(() => {\n    checkExternalStoragePath()\n  }, [checkExternalStoragePath])\n\n  const refresh = () => {\n    void onRefreshDir(path)\n    checkExternalStoragePath()\n  }\n\n  const openStorage = () => {\n    openDirModalTypeRef.current?.show(storagePathsRef.current)\n  }\n\n  const handleShowNewFolderModal = () => {\n    newFolderTypeRef.current?.show(path)\n  }\n\n  return (\n    <>\n      <View style={{\n        ...styles.header,\n        height: scaleSizeH(50) + statusBarHeight,\n        paddingTop: statusBarHeight,\n        backgroundColor: theme['c-content-background'],\n      }} onStartShouldSetResponder={() => true}>\n        <View style={styles.titleContent}>\n          <Text color={theme['c-primary-font']} numberOfLines={1}>{title}</Text>\n          <Text style={styles.subTitle} color={theme['c-primary-font']} size={13} numberOfLines={1}>{path}</Text>\n        </View>\n        <View style={styles.actions}>\n          <TouchableOpacity style={styles.actionBtn} onPress={openStorage}>\n            <Icon name=\"sd-card\" color={theme['c-primary-font']} size={22} />\n          </TouchableOpacity>\n          <TouchableOpacity style={styles.actionBtn} onPress={handleShowNewFolderModal}>\n            <Icon name=\"add_folder\" color={theme['c-primary-font']} size={22} />\n          </TouchableOpacity>\n          <TouchableOpacity style={styles.actionBtn} onPress={refresh}>\n            <Icon name=\"available_updates\" color={theme['c-primary-font']} size={22} />\n          </TouchableOpacity>\n        </View>\n      </View>\n      <OpenStorageModal ref={openDirModalTypeRef} onOpenDir={onOpenDir} />\n      <NewFolderModal ref={newFolderTypeRef} onRefreshDir={onRefreshDir} />\n    </>\n  )\n})\n\nconst styles = createStyle({\n  header: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: 'row',\n    paddingLeft: 15,\n    paddingRight: 15,\n    alignItems: 'center',\n    elevation: 2,\n    zIndex: 2,\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  titleContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // height: 57,\n    // paddingRight: 5,\n    // paddingBottom: 10,\n  },\n  // title: {\n  //   paddingTop: 10,\n  // },\n  subTitle: {\n    paddingTop: 1,\n  },\n  actions: {\n    flexDirection: 'row',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  actionBtn: {\n    paddingTop: 8,\n    paddingBottom: 8,\n    paddingLeft: 6,\n    paddingRight: 6,\n    marginLeft: 10,\n  },\n  newFolderContent: {\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  newFolderTitle: {\n    marginBottom: 5,\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 240,\n    borderRadius: 4,\n    paddingTop: 2,\n    paddingBottom: 2,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/ListItem.tsx",
    "content": "import { memo } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { type RowInfo, createStyle } from '@/utils/tools'\n\nexport interface PathItem {\n  name: string\n  path: string\n  isDir: boolean\n  mtime?: Date\n  desc?: string\n  size?: number\n  sizeText?: string\n  disabled?: boolean\n}\n\nexport default memo(({ item, onPress, rowInfo }: {\n  item: PathItem\n  onPress: (item: PathItem) => void\n  rowInfo: RowInfo\n}) => {\n  const theme = useTheme()\n\n  // const moreButtonRef = useRef()\n  // const handleShowMenu = useCallback(() => {\n  //   if (moreButtonRef.current && moreButtonRef.current.measure) {\n  //     moreButtonRef.current.measure((fx, fy, width, height, px, py) => {\n  //       // console.log(fx, fy, width, height, px, py)\n  //       showMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n  //     })\n  //   }\n  // }, [item, index, showMenu])\n\n  return (\n    <View style={{ ...styles.listItem, width: rowInfo.rowWidth }} onStartShouldSetResponder={() => true}>\n      {\n        item.disabled ? (\n          <View style={{ ...styles.listItem, opacity: 0.3 }}>\n            <View style={styles.itemInfo}>\n              <Text style={styles.listItemTitleText}>{item.name}</Text>\n              <Text style={styles.listItemDesc} size={12} color={theme['c-font-label']} numberOfLines={1}>{item.mtime ? new Date(item.mtime).toLocaleString() : item.desc}</Text>\n            </View>\n            {\n              item.isDir ? null\n                : <Text style={styles.size} size={12} color={theme['c-font-label']}>{item.sizeText}</Text>\n            }\n          </View>\n        ) : (\n          <TouchableOpacity style={styles.listItem} onPress={ () => { onPress(item) } }>\n            <View style={styles.itemInfo}>\n              <Text style={styles.listItemTitleText}>{item.name}</Text>\n              <Text style={styles.listItemDesc} size={12} color={theme['c-font-label']} numberOfLines={1}>{item.mtime ? new Date(item.mtime).toLocaleString() : item.desc}</Text>\n            </View>\n            {\n              item.isDir\n                ? <Icon name=\"chevron-right\" color={theme['c-primary-light-100-alpha-600']} size={18} />\n                : <Text style={styles.size} size={12} color={theme['c-font-label']}>{item.sizeText}</Text>\n            }\n          </TouchableOpacity>\n        )\n      }\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  listItem: {\n    width: '100%',\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    paddingLeft: 10,\n    paddingRight: 10,\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  itemInfo: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingTop: 10,\n    paddingBottom: 10,\n  },\n  listItemTitleText: {\n    flexDirection: 'row',\n    alignItems: 'flex-end',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    flexGrow: 0,\n    flexShrink: 1,\n  },\n  listItemDesc: {\n    paddingTop: 2,\n  },\n  size: {\n    alignSelf: 'flex-end',\n    marginBottom: 10,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/Main.tsx",
    "content": "import { useI18n } from '@/lang'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle, getRowInfo } from '@/utils/tools'\nimport { useEffect, useMemo, useRef } from 'react'\nimport { View, FlatList } from 'react-native'\n\nimport ListItem, { type PathItem } from './ListItem'\nimport LoadingMask, { type LoadingMaskType } from '@/components/common/LoadingMask'\n\n\nexport default ({ list, loading, onSetPath, toParentDir }: {\n  list: PathItem[]\n  loading: boolean\n  onSetPath: (item: PathItem) => void\n  toParentDir: () => void\n}) => {\n  const t = useI18n()\n  const theme = useTheme()\n  const loadingMaskRef = useRef<LoadingMaskType>(null)\n  const rowInfo = useRef(getRowInfo('full'))\n  const fullRow = useRef({ rowNum: undefined, rowWidth: '100%' } as const)\n\n  const ParentItemComponent = useMemo(() => (\n    <View style={{ backgroundColor: theme['c-primary-light-700-alpha-900'] }}>\n      <ListItem item={{\n        name: '..',\n        desc: t('parent_dir_name'),\n        isDir: true,\n        path: '',\n      }} rowInfo={fullRow.current} onPress={toParentDir} />\n    </View>\n  ), [t, theme, toParentDir])\n\n  useEffect(() => {\n    loadingMaskRef.current?.setVisible(loading)\n  }, [loading])\n\n  const ListComponent = useMemo(() => (\n    <FlatList\n      keyboardShouldPersistTaps={'always'}\n      style={styles.list}\n      data={list}\n      numColumns={rowInfo.current.rowNum}\n      renderItem={({ item }) => <ListItem item={item} rowInfo={rowInfo.current} onPress={onSetPath} />}\n      keyExtractor={item => item.path + '/' + item.name}\n      removeClippedSubviews={true}\n    />\n  ), [list, onSetPath])\n\n  // const dirList = useMemo(() => [parentDir, ...list], [list, parentDir])\n\n  return (\n    <View style={styles.main}>\n      {ParentItemComponent}\n      {ListComponent}\n      <LoadingMask ref={loadingMaskRef} />\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  main: {\n    flexGrow: 1,\n    flexShrink: 1,\n    overflow: 'hidden',\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/NewFolderModal.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport Text from '@/components/common/Text'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport { createStyle, toast } from '@/utils/tools'\nimport { mkdir } from '@/utils/fs'\nimport { useTheme } from '@/store/theme/hook'\nimport type { PathItem } from './ListItem'\nconst filterFileName = /[\\\\/:*?#\"<>|]/\n\n\ninterface NameInputType {\n  setName: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst NameInput = forwardRef<NameInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setName(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={global.i18n.t('create_new_folder_tip')}\n      value={text}\n      onChangeText={setText}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\nexport interface NewFolderType {\n  show: (path: string) => void\n}\nexport default forwardRef<NewFolderType, { onRefreshDir: (dir: string) => Promise<PathItem[]> }>(({ onRefreshDir }, ref) => {\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n  const nameInputRef = useRef<NameInputType>(null)\n  const pathRef = useRef<string>('')\n\n  useImperativeHandle(ref, () => ({\n    show(path) {\n      pathRef.current = path\n      confirmAlertRef.current?.setVisible(true)\n      requestAnimationFrame(() => {\n        setTimeout(() => {\n          nameInputRef.current?.focus()\n        }, 300)\n      })\n    },\n  }))\n\n  const handleHideNewFolderAlert = () => {\n    nameInputRef.current?.setName('')\n  }\n  const handleConfirmNewFolderAlert = () => {\n    const text = nameInputRef.current?.getText() ?? ''\n    if (!text) return\n    if (filterFileName.test(text)) {\n      toast(global.i18n.t('create_new_folder_error_tip'), 'long')\n      return\n    }\n    const newPath = `${pathRef.current}/${text}`\n    mkdir(newPath).then(() => {\n      void onRefreshDir(pathRef.current).then((list) => {\n        const target = list.find(item => item.name == text)\n        if (target) void onRefreshDir(target.path)\n      })\n      nameInputRef.current?.setName('')\n    }).catch((err: any) => {\n      toast('Create failed: ' + (err.message as string))\n    })\n    confirmAlertRef.current?.setVisible(false)\n  }\n\n  return (\n    <ConfirmAlert\n      onHide={handleHideNewFolderAlert}\n      onConfirm={handleConfirmNewFolderAlert}\n      ref={confirmAlertRef}\n    >\n      <View style={styles.newFolderContent}>\n        <Text style={styles.newFolderTitle}>{global.i18n.t('create_new_folder')}</Text>\n        <NameInput ref={nameInputRef} />\n      </View>\n    </ConfirmAlert>\n  )\n})\n\nconst styles = createStyle({\n  newFolderContent: {\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  newFolderTitle: {\n    marginBottom: 5,\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 240,\n    borderRadius: 4,\n    paddingTop: 2,\n    paddingBottom: 2,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/components/OpenStorageModal.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport Text from '@/components/common/Text'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport { createStyle, toast } from '@/utils/tools'\nimport { getManagedFolders, stat, removeManagedFolder, selectManagedFolder } from '@/utils/fs'\nimport { useTheme } from '@/store/theme/hook'\nimport { getOpenStoragePath, saveOpenStoragePath } from '@/utils/data'\nimport Button from '@/components/common/Button'\nimport ButtonPrimary from '@/components/common/ButtonPrimary'\nimport { useUnmounted } from '@/utils/hooks'\nimport { Icon } from '@/components/common/Icon'\nimport type { PathItem } from './ListItem'\nconst filterFileName = /[\\\\:*?#\"<>|]/\n\n\ninterface PathInputType {\n  setPath: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst PathInput = forwardRef<PathInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setPath(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={global.i18n.t('open_storage_tip')}\n      value={text}\n      onChangeText={setText}\n      multiline\n      numberOfLines={3}\n      textAlignVertical='top'\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\nexport interface OpenDirModalType {\n  show: (paths: string[]) => void\n}\nexport default forwardRef<OpenDirModalType, { onOpenDir: (dir: string) => Promise<PathItem[]> }>(({\n  onOpenDir,\n}, ref) => {\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n  const inputRef = useRef<PathInputType>(null)\n  const [paths, setPaths] = useState<string[]>([])\n  const [contentPaths, setContentPaths] = useState<string[]>([])\n  const isUnmounted = useUnmounted()\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    show(paths) {\n      setPaths(paths)\n      void getManagedFolders().then((dirs) => {\n        setContentPaths(dirs)\n      })\n      confirmAlertRef.current?.setVisible(true)\n      requestAnimationFrame(() => {\n        void getOpenStoragePath().then(path => {\n          if (path) inputRef.current?.setPath(path)\n        })\n      })\n    },\n  }))\n\n  // const handleHideAlert = () => {\n  //   inputRef.current?.setPath('')\n  // }\n  const handleConfirmAlert = async() => {\n    const text = inputRef.current?.getText() ?? ''\n    if (text) {\n      if (!text.startsWith('content://') && (!text.startsWith('/') || filterFileName.test(text))) {\n        toast(global.i18n.t('open_storage_error_tip'), 'long')\n        return\n      }\n      try {\n        if (!(await stat(text)).canRead) {\n          toast('Permission denied', 'long')\n          return\n        }\n      } catch (err: any) {\n        toast('Open failed: ' + err.message, 'long')\n        return\n      }\n      void onOpenDir(text)\n    }\n    void saveOpenStoragePath(text)\n    confirmAlertRef.current?.setVisible(false)\n  }\n  const removeSelectStoragePath = (path: string) => {\n    void removeManagedFolder(path).then(async() => {\n      return getManagedFolders().then((dirs) => {\n        setContentPaths(dirs)\n      })\n    })\n  }\n  const handleSelectStorage = () => {\n    void selectManagedFolder(true).then((dir) => {\n      if (!dir || isUnmounted.current) return\n      void onOpenDir(dir.path)\n      confirmAlertRef.current?.setVisible(false)\n    }).catch((err) => {\n      toast(global.i18n.t('open_storage_select_managed_folder_failed_tip', { msg: err.message }))\n    })\n  }\n\n  return (\n    <ConfirmAlert\n      onConfirm={handleConfirmAlert}\n      ref={confirmAlertRef}\n    >\n      <View style={styles.newFolderContent} onStartShouldSetResponder={() => true}>\n        <Text style={styles.newFolderTitle}>\n          {\n            paths.length > 1\n              ? global.i18n.t('open_storage_title')\n              : global.i18n.t('open_storage_not_found_title')\n          }\n        </Text>\n        <PathInput ref={inputRef} />\n        <View style={styles.list}>\n          {\n            paths.map(path => {\n              return (\n                <Button style={styles.pathBtn} key={path} onPress={() => inputRef.current?.setPath(path)}>\n                  <Text size={12}>{path}</Text>\n                </Button>\n              )\n            })\n          }\n          {\n            contentPaths.map(path => {\n              return (\n                <View style={styles.listContentItem} key={path}>\n                  <Button style={styles.pathBtn} onPress={() => inputRef.current?.setPath(path)}>\n                    <Text size={12}>{path}</Text>\n                  </Button>\n                  <Button style={styles.removeBtn} onPress={() => { removeSelectStoragePath(path) }}>\n                    <Icon color={theme['c-font-label']} name=\"close\" size={12} />\n                  </Button>\n                </View>\n              )\n            })\n          }\n        </View>\n        <View style={styles.tips}>\n          <Text style={styles.tip} size={12}>\n            {global.i18n.t('open_storage_select_path_tip')}\n          </Text>\n          <ButtonPrimary style={styles.btn} size={12} onPress={handleSelectStorage}>{global.i18n.t('open_storage_select_path')}</ButtonPrimary>\n        </View>\n      </View>\n    </ConfirmAlert>\n  )\n})\n\nconst styles = createStyle({\n  newFolderContent: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  newFolderTitle: {\n    marginBottom: 5,\n    width: 300,\n    maxWidth: '100%',\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 240,\n    borderRadius: 4,\n    paddingTop: 3,\n    paddingBottom: 3,\n    height: 'auto',\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n    marginVertical: 10,\n  },\n  listContentItem: {\n    paddingVertical: 10,\n    flexDirection: 'row',\n  },\n  pathBtn: {\n    paddingHorizontal: 5,\n    paddingVertical: 10,\n    flex: 1,\n  },\n  removeBtn: {\n    flex: 0,\n    alignItems: 'center',\n    justifyContent: 'center',\n    paddingVertical: 10,\n    paddingHorizontal: 15,\n  },\n  tips: {\n    flexDirection: 'column',\n    alignItems: 'flex-start',\n    gap: 10,\n  },\n  tip: {\n    flex: 1,\n  },\n  btn: {\n    // alignSelf: 'flex-end',\n    flex: 0,\n  },\n})\n\n"
  },
  {
    "path": "src/components/common/ChoosePath/index.tsx",
    "content": "import { useState, useRef, forwardRef, useImperativeHandle } from 'react'\n// import { StyleSheet, View, Text, StatusBar, ScrollView } from 'react-native'\n\n// import { useGetter, useDispatch } from '@/store'\nimport List, { type ListType } from './List'\n\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport { toast, TEMP_FILE_PATH, checkStoragePermissions, requestStoragePermission, confirmDialog } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { selectFile, unlink } from '@/utils/fs'\nimport { useUnmounted } from '@/utils/hooks'\nimport settingState from '@/store/setting/state'\nimport { log } from '@/utils/log'\nimport { updateSetting } from '@/core/common'\n\nexport interface ReadOptions {\n  title: string\n  isPersist?: boolean\n  dirOnly?: boolean\n  filter?: string[]\n}\nconst initReadOptions = {}\n\ninterface ChoosePathProps {\n  onConfirm: (path: string) => void\n}\n\nexport interface ChoosePathType {\n  show: (options: ReadOptions) => void\n}\n\nexport default forwardRef<ChoosePathType, ChoosePathProps>(({\n  onConfirm = () => {},\n}: ChoosePathProps, ref) => {\n  const t = useI18n()\n  const listRef = useRef<ListType>(null)\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n  const [deny, setDeny] = useState(false)\n  const readOptions = useRef<ReadOptions>(initReadOptions as ReadOptions)\n  const isUnmounted = useUnmounted()\n\n  const handleOpenExternalStorage = async(options: ReadOptions) => {\n    return checkStoragePermissions().then(isGranted => {\n      readOptions.current = options\n      if (isGranted) {\n        listRef.current?.show(options.title, '', options.dirOnly, options.filter)\n      } else {\n        confirmAlertRef.current?.setVisible(true)\n      }\n    })\n  }\n\n  useImperativeHandle(ref, () => ({\n    show(options) {\n      if (!settingState.setting['common.useSystemFileSelector'] || options.dirOnly) {\n        // if (options.isPersist) {\n        void handleOpenExternalStorage(options)\n        // } else {\n        //   void selectManagedFolder().then((dir) => {\n        //     if (!dir || isUnmounted.current) return\n        //     listRef.current?.show(options.title, dir.path, options.dirOnly, options.filter)\n        //   })\n        // }\n      } else {\n        void selectFile({\n          extTypes: options.filter,\n          toPath: TEMP_FILE_PATH,\n        }).then((file) => {\n          // console.log(file)\n          if (!file || isUnmounted.current) return\n          if (options.filter && !options.filter.some(ext => file.data.toLowerCase().endsWith('.' + ext))) {\n            toast(t('storage_file_no_match'), 'long')\n            void unlink(file.data)\n            return\n          }\n          onConfirm(file.data)\n        }).catch(err => {\n          if (isUnmounted.current) return\n          log.warn('open document failed: ' + err.message)\n          void confirmDialog({\n            message: t('storage_file_no_select_file_failed_tip'),\n            bgClose: false,\n          }).then((confirm) => {\n            if (!confirm) {\n              toast(t('disagree_tip'), 'long')\n              return\n            }\n            updateSetting({ 'common.useSystemFileSelector': false })\n            void handleOpenExternalStorage(options)\n          })\n        })\n      }\n    },\n  }))\n\n  const handleTipsCancel = () => {\n    toast(t('disagree_tip'), 'long')\n  }\n  const handleTipsConfirm = () => {\n    confirmAlertRef.current?.setVisible(false)\n    void requestStoragePermission().then(result => {\n      // console.log(result)\n      setDeny(result == null)\n      if (result) {\n        listRef.current?.show(readOptions.current.title, '', readOptions.current.dirOnly, readOptions.current.filter)\n      } else {\n        toast(t('storage_permission_tip_disagree'), 'long')\n      }\n    })\n  }\n  const onPathConfirm = (path: string) => {\n    listRef.current?.hide()\n    onConfirm(path)\n  }\n\n  return (\n    <>\n      <List ref={listRef} onConfirm={onPathConfirm} />\n      <ConfirmAlert\n        ref={confirmAlertRef}\n        onCancel={handleTipsCancel}\n        onConfirm={handleTipsConfirm}\n        bgHide={false}\n        closeBtn={false}\n        showConfirm={!deny}\n        cancelText={t('disagree')}\n        confirmText={t('agree')}\n        text={t(deny ? 'storage_permission_tip_disagree_ask_again' : 'storage_permission_tip_request')} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/components/common/ConfirmAlert.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { View, ScrollView } from 'react-native'\nimport Dialog, { type DialogType } from './Dialog'\nimport Button from './Button'\nimport { createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang/index'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from './Text'\n\nconst styles = createStyle({\n  main: {\n    // flexGrow: 0,\n    flexShrink: 1,\n    marginTop: 15,\n    marginLeft: 5,\n    marginRight: 5,\n    marginBottom: 25,\n  },\n  content: {\n    flexGrow: 0,\n    paddingLeft: 10,\n    paddingRight: 10,\n  },\n  btns: {\n    flexDirection: 'row',\n    justifyContent: 'center',\n    paddingBottom: 15,\n    // paddingRight: 15,\n  },\n  btnsDirection: {\n    paddingLeft: 15,\n  },\n  btnsReversedDirection: {\n    paddingLeft: 15,\n    flexDirection: 'row-reverse',\n  },\n  btn: {\n    flex: 1,\n    paddingTop: 9,\n    paddingBottom: 9,\n    paddingLeft: 10,\n    paddingRight: 10,\n    alignItems: 'center',\n    borderRadius: 4,\n  },\n  btnDirection: {\n    marginRight: 15,\n  },\n  btnReversedDirection: {\n    marginLeft: 15,\n  },\n})\n\nexport interface ConfirmAlertProps {\n  onCancel?: () => void\n  onHide?: () => void\n  onConfirm?: () => void\n  keyHide?: boolean\n  bgHide?: boolean\n  closeBtn?: boolean\n  title?: string\n  text?: string\n  cancelText?: string\n  confirmText?: string\n  showConfirm?: boolean\n  disabledConfirm?: boolean\n  reverseBtn?: boolean\n  children?: React.ReactNode | React.ReactNode[]\n}\n\nexport interface ConfirmAlertType {\n  setVisible: (visible: boolean) => void\n}\n\nexport default forwardRef<ConfirmAlertType, ConfirmAlertProps>(({\n  onHide,\n  onCancel,\n  onConfirm = () => {},\n  keyHide,\n  bgHide,\n  closeBtn,\n  title = '',\n  text = '',\n  cancelText = '',\n  confirmText = '',\n  showConfirm = true,\n  disabledConfirm = false,\n  children,\n  reverseBtn = false,\n}: ConfirmAlertProps, ref) => {\n  const theme = useTheme()\n  const t = useI18n()\n\n  const dialogRef = useRef<DialogType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setVisible(visible: boolean) {\n      dialogRef.current?.setVisible(visible)\n    },\n  }))\n\n  const handleCancel = () => {\n    onCancel?.()\n    dialogRef.current?.setVisible(false)\n  }\n\n  return (\n    <Dialog onHide={onHide} keyHide={keyHide} bgHide={bgHide} closeBtn={closeBtn} title={title} ref={dialogRef}>\n      <View style={styles.main}>\n        <ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>\n          {children ?? <Text>{text}</Text>}\n        </ScrollView>\n      </View>\n      <View style={{ ...styles.btns, ...(reverseBtn ? styles.btnsReversedDirection : styles.btnsDirection) }}>\n        <Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>\n          <Text color={theme['c-button-font']}>{cancelText || t('cancel')}</Text>\n        </Button>\n        {showConfirm\n          ? <Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme['c-button-background'] }} onPress={onConfirm} disabled={disabledConfirm}>\n              <Text color={theme['c-button-font']}>{confirmText || t('confirm')}</Text>\n            </Button>\n          : null}\n      </View>\n    </Dialog>\n  )\n})\n"
  },
  {
    "path": "src/components/common/Dialog.tsx",
    "content": "import { useImperativeHandle, forwardRef, useMemo, useRef } from 'react'\nimport { View, TouchableHighlight } from 'react-native'\n\nimport Modal, { type ModalType } from './Modal'\nimport { Icon } from '@/components/common/Icon'\nimport { useKeyboard } from '@/utils/hooks'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from './Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\n\nconst HEADER_HEIGHT = 20\nconst styles = createStyle({\n  centeredView: {\n    flex: 1,\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n  modalView: {\n    maxWidth: '90%',\n    minWidth: '60%',\n    maxHeight: '78%',\n    // backgroundColor: 'white',\n    borderRadius: 4,\n    // shadowColor: '#000',\n    // shadowOffset: {\n    //   width: 0,\n    //   height: 2,\n    // },\n    // shadowOpacity: 0.25,\n    // shadowRadius: 4,\n    elevation: 3,\n  },\n  header: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: 'row',\n    borderTopLeftRadius: 4,\n    borderTopRightRadius: 4,\n    height: HEADER_HEIGHT,\n  },\n  title: {\n    paddingLeft: 5,\n    paddingRight: 25,\n    lineHeight: HEADER_HEIGHT,\n  },\n  closeBtn: {\n    position: 'absolute',\n    right: 0,\n    borderTopRightRadius: 4,\n    flexGrow: 0,\n    flexShrink: 0,\n    height: HEADER_HEIGHT,\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n})\n\nexport interface DialogProps {\n  onHide?: () => void\n  keyHide?: boolean\n  bgHide?: boolean\n  closeBtn?: boolean\n  title?: string\n  children: React.ReactNode | React.ReactNode[]\n  height?: number | `${number}%`\n}\n\nexport interface DialogType {\n  setVisible: (visible: boolean) => void\n}\n\nexport default forwardRef<DialogType, DialogProps>(({\n  onHide,\n  keyHide = true,\n  bgHide = true,\n  closeBtn = true,\n  title = '',\n  children,\n  height,\n}: DialogProps, ref) => {\n  const theme = useTheme()\n  const { keyboardShown, keyboardHeight } = useKeyboard()\n  const modalRef = useRef<ModalType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setVisible(visible: boolean) {\n      modalRef.current?.setVisible(visible)\n    },\n  }))\n\n  const closeBtnComponent = useMemo(() => {\n    return closeBtn\n      ? <TouchableHighlight style={{ ...styles.closeBtn, width: scaleSizeH(HEADER_HEIGHT) }} underlayColor={theme['c-primary-dark-200-alpha-600']} onPress={() => modalRef.current?.setVisible(false)}>\n          <Icon name=\"close\" color={theme['c-primary-dark-500-alpha-500']} size={10} />\n        </TouchableHighlight>\n      : null\n  }, [closeBtn, theme])\n\n  return (\n    <Modal onHide={onHide} keyHide={keyHide} bgHide={bgHide} bgColor=\"rgba(50,50,50,.3)\" ref={modalRef}>\n      <View style={{ ...styles.centeredView, paddingBottom: keyboardShown ? keyboardHeight : 0 }}>\n        <View style={{ ...styles.modalView, height, backgroundColor: theme['c-content-background'] }} onStartShouldSetResponder={() => true}>\n          <View style={{ ...styles.header, backgroundColor: theme['c-primary-light-100-alpha-100'] }}>\n            <Text style={styles.title} size={13} color={theme['c-primary-light-1000']} numberOfLines={1}>{title}</Text>\n            {closeBtnComponent}\n          </View>\n          {children}\n        </View>\n      </View>\n    </Modal>\n  )\n})\n"
  },
  {
    "path": "src/components/common/DorpDownMenu.tsx",
    "content": "import { useRef } from 'react'\n// import { View } from 'react-native'\n\nimport Menu, { type MenuType, type MenuProps, type Menus } from './Menu'\nimport Button, { type BtnType, type BtnProps } from './Button'\n// import { useLayout } from '@/utils/hooks'\n\nexport interface DorpDownMenuProps<T extends Menus> extends Omit<MenuProps<T>, 'width'> {\n  children: React.ReactNode\n  btnStyle?: BtnProps['style']\n}\n\nexport default <T extends Menus>({\n  menus,\n  onPress,\n  height,\n  fontSize,\n  center,\n  children,\n  activeId,\n  btnStyle,\n}: DorpDownMenuProps<T>) => {\n  const buttonRef = useRef<BtnType>(null)\n  const menuRef = useRef<MenuType>(null)\n\n  const showMenu = () => {\n    buttonRef.current?.measure((fx, fy, width, height, px, py) => {\n      // console.log(fx, fy, width, height, px, py)\n      menuRef.current?.show({ x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) }, {\n        width,\n        height,\n      })\n    })\n  }\n\n  return (\n    <Button style={btnStyle} ref={buttonRef} onPress={showMenu}>\n      {children}\n      <Menu\n        ref={menuRef}\n        menus={menus}\n        center={center}\n        onPress={onPress}\n        fontSize={fontSize}\n        height={height}\n        activeId={activeId}\n      />\n    </Button>\n  )\n}\n"
  },
  {
    "path": "src/components/common/DorpDownPanel/Panel.tsx",
    "content": "import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport { View, TouchableWithoutFeedback } from 'react-native'\nimport { useWindowSize } from '@/utils/hooks'\n\nimport Modal, { type ModalType } from '@/components/common/Modal'\nimport { createStyle } from '@/utils/tools'\n// import { useGetter } from '@/store'\n\n// const menuItemHeight = 42\n// const menuItemWidth = 100\ninterface Position { w: number, h: number, x: number, y: number }\n\nconst styles = createStyle({\n  menu: {\n    position: 'absolute',\n    // borderWidth: StyleSheet.hairlineWidth,\n    // borderColor: 'lightgray',\n    // borderRadius: 2,\n    backgroundColor: 'rgba(0,0,0,0)',\n  },\n  menuItem: {\n    paddingLeft: 10,\n    paddingRight: 10,\n    // height: menuItemHeight,\n    // width: menuItemWidth,\n    // alignItems: 'center',\n    justifyContent: 'center',\n    // backgroundColor: '#ccc',\n  },\n  menuText: {\n    // textAlign: 'center',\n  },\n})\n\nconst Panel = ({\n  buttonPosition,\n  // panelStyle = {},\n  onHide,\n  children,\n}: {\n  buttonPosition: Position\n  onHide: () => void\n  children: React.ReactNode | React.ReactNode[]\n}) => {\n  // const dimensions = useWindowSize()\n  const windowSize = useWindowSize()\n  // const theme = useGetter('common', 'theme')\n  // const fadeAnim = useRef(new Animated.Value(0)).current\n  // console.log(buttonPosition)\n\n  // console.log(dimensions)\n  const style = useMemo(() => {\n    const isBottom = buttonPosition.y > windowSize.height / 2\n    let top: number\n    let height: number\n    let justifyContent: 'flex-end' | 'flex-start'\n    if (isBottom) {\n      const buttonPositionY = Math.ceil(buttonPosition.y)\n      height = buttonPositionY - windowSize.height * 0.3\n      top = buttonPositionY - height\n      justifyContent = 'flex-end'\n    } else {\n      top = Math.floor(buttonPosition.y) + Math.floor(buttonPosition.h)\n      height = windowSize.height * 0.7 - top\n      justifyContent = 'flex-start'\n    }\n    const frameStyle = {\n      flex: 1,\n      height,\n      top,\n      justifyContent,\n      width: windowSize.width,\n    }\n    return frameStyle\n  }, [windowSize, buttonPosition])\n\n  return (\n    <TouchableWithoutFeedback onPress={onHide}>\n      <View style={{ ...styles.menu, ...style }}>\n        <View onStartShouldSetResponder={() => true}>\n          {children}\n        </View>\n      </View>\n    </TouchableWithoutFeedback>\n  )\n}\nexport interface PanelProps {\n  onHide?: () => void\n  keyHide?: boolean\n  bgHide?: boolean\n  closeBtn?: boolean\n  title?: string\n  children: React.ReactNode | React.ReactNode[]\n  // style:\n}\n\nexport interface PanelType {\n  show: (position: Position) => void\n  hide: () => void\n}\n\nexport default forwardRef<PanelType, PanelProps>(({ onHide, keyHide, bgHide, children }, ref) => {\n  const modalRef = useRef<ModalType>(null)\n  const [position, setPosition] = useState<Position>({ w: 0, h: 0, x: 0, y: 0 })\n\n  useImperativeHandle(ref, () => ({\n    show(newPosition) {\n      setPosition(newPosition)\n      modalRef.current?.setVisible(true)\n    },\n    hide() {\n      modalRef.current?.setVisible(false)\n    },\n  }))\n\n  // console.log(visible)\n  return (\n    <Modal ref={modalRef} onHide={onHide} onStartShouldSetResponder={() => true} keyHide={keyHide} bgHide={bgHide}>\n      <Panel buttonPosition={position} onHide={() => modalRef.current?.setVisible(false)}>\n        {children}\n      </Panel>\n    </Modal>\n  )\n})\n\n"
  },
  {
    "path": "src/components/common/DorpDownPanel/index.tsx",
    "content": "import { useRef, forwardRef } from 'react'\n// import { View } from 'react-native'\n\nimport Panel, { type PanelType } from './Panel'\nimport Button, { type BtnType } from '@/components/common/Button'\n\n\nexport interface DorpDownPanelProps {\n  onHide?: () => void\n  children: React.ReactNode | React.ReactNode[]\n  PanelContent: React.ReactNode | React.ReactNode[]\n}\nexport interface DorpDownPanelType {\n  hide: () => void\n}\n\nexport default forwardRef<DorpDownPanelType, DorpDownPanelProps>(({\n  children,\n  PanelContent,\n  onHide,\n}) => {\n  const buttonRef = useRef<BtnType>(null)\n  const panelRef = useRef<PanelType>(null)\n\n  const showMenu = () => {\n    buttonRef.current?.measure((fx, fy, width, height, px, py) => {\n      // console.log(fx, fy, width, height, px, py)\n      panelRef.current?.show({ x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n    })\n  }\n\n  return (\n    <Button ref={buttonRef} onPress={showMenu}>\n      {children}\n      <Panel ref={panelRef} onHide={onHide}>\n        {PanelContent}\n      </Panel>\n    </Button>\n  )\n})\n"
  },
  {
    "path": "src/components/common/DrawerLayoutFixed.tsx",
    "content": "import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'\nimport { DrawerLayoutAndroid, type DrawerLayoutAndroidProps, View, type LayoutChangeEvent } from 'react-native'\n// import { getWindowSise } from '@/utils/tools'\nimport { usePageVisible } from '@/store/common/hook'\nimport { type COMPONENT_IDS } from '@/config/constant'\n\ninterface Props extends DrawerLayoutAndroidProps {\n  visibleNavNames: COMPONENT_IDS[]\n  widthPercentage: number\n  widthPercentageMax?: number\n}\n\nexport interface DrawerLayoutFixedType {\n  openDrawer: () => void\n  closeDrawer: () => void\n  fixWidth: () => void\n}\n\nconst DrawerLayoutFixed = forwardRef<DrawerLayoutFixedType, Props>(({ visibleNavNames, widthPercentage, widthPercentageMax, children, ...props }, ref) => {\n  const drawerLayoutRef = useRef<DrawerLayoutAndroid>(null)\n  const [w, setW] = useState<number | `${number}%`>('100%')\n  const [drawerWidth, setDrawerWidth] = useState(0)\n  const changedRef = useRef({ width: 0, changed: false })\n\n  const fixDrawerWidth = useCallback(() => {\n    if (!changedRef.current.width) return\n    changedRef.current.changed = true\n    // console.log('usePageVisible', visible, changedRef.current.width)\n    setW(changedRef.current.width - 1)\n  }, [])\n\n  // 修复 DrawerLayoutAndroid 在导航到其他屏幕再返回后无法打开的问题\n  usePageVisible(visibleNavNames, useCallback((visible) => {\n    if (!visible || !changedRef.current.width) return\n    fixDrawerWidth()\n  }, [fixDrawerWidth]))\n\n  useImperativeHandle(ref, () => ({\n    openDrawer() {\n      drawerLayoutRef.current?.openDrawer()\n    },\n    closeDrawer() {\n      drawerLayoutRef.current?.closeDrawer()\n    },\n    fixWidth() {\n      fixDrawerWidth()\n    },\n  }), [fixDrawerWidth])\n\n\n  const handleLayout = useCallback((e: LayoutChangeEvent) => {\n    // console.log('handleLayout', e.nativeEvent.layout.width, changedRef.current.width)\n    if (changedRef.current.changed) {\n      // setW(e.nativeEvent.layout.width - 1)\n      setW('100%')\n      changedRef.current.changed = false\n    } else {\n      const width = e.nativeEvent.layout.width\n      if (changedRef.current.width == width) return\n      changedRef.current.width = width\n\n      // 重新设置面板宽度\n      const wp = Math.floor(width * widthPercentage)\n      // console.log(wp, widthPercentageMax)\n      setDrawerWidth(widthPercentageMax ? Math.min(wp, widthPercentageMax) : wp)\n\n      // 强制触发渲染以应用更改\n      changedRef.current.changed = true\n      setW(width - 1)\n    }\n  }, [widthPercentage, widthPercentageMax])\n\n  return (\n    <View\n      onLayout={handleLayout}\n      style={{ width: w, flex: 1 }}\n    >\n      <DrawerLayoutAndroid\n        ref={drawerLayoutRef}\n        keyboardDismissMode=\"on-drag\"\n        drawerWidth={drawerWidth}\n        {...props}\n      >\n        <View style={{ marginRight: w == '100%' ? 0 : -1, flex: 1 }}>\n          {children}\n        </View>\n      </DrawerLayoutAndroid>\n    </View>\n  )\n})\n\n// const styles = createStyle({\n//   container: {\n//     flex: 1,\n//   },\n// })\n\nexport default DrawerLayoutFixed\n"
  },
  {
    "path": "src/components/common/FileSelect.tsx",
    "content": "import ChoosePath, { type ReadOptions, type ChoosePathType } from '@/components/common/ChoosePath'\nimport { forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nexport interface FileSelectType {\n  show: (options: ReadOptions, onSelect: typeof noop) => void\n}\nconst noop = (path: string) => {}\nexport default forwardRef<FileSelectType, {}>((props, ref) => {\n  const [visible, setVisible] = useState(false)\n  const choosePathRef = useRef<ChoosePathType>(null)\n  const onSelectRef = useRef<typeof noop>(noop)\n  // console.log('render import export')\n\n  useImperativeHandle(ref, () => ({\n    show(options, onSelect) {\n      onSelectRef.current = onSelect ?? noop\n      if (visible) {\n        choosePathRef.current?.show(options)\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show(options)\n        })\n      }\n    },\n  }))\n\n  return (\n    visible\n      ? <ChoosePath ref={choosePathRef} onConfirm={onSelectRef.current} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/components/common/Icon.tsx",
    "content": "import { createIconSetFromIcoMoon } from 'react-native-vector-icons'\nimport icoMoonConfig from '@/resources/fonts/selection.json'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { memo, type ComponentProps } from 'react'\nimport { useTextShadow, useTheme } from '@/store/theme/hook'\nimport { StyleSheet, type StyleProp, type TextStyle } from 'react-native'\n\n// import IconAntDesign from 'react-native-vector-icons/AntDesign'\n// import IconEntypo from 'react-native-vector-icons/Entypo'\n// import IconEvilIcons from 'react-native-vector-icons/EvilIcons'\n// import IconFeather from 'react-native-vector-icons/Feather'\n// import IconFontAwesome from 'react-native-vector-icons/FontAwesome'\n// import IconFontAwesome5 from 'react-native-vector-icons/FontAwesome5'\n// import IconFontisto from 'react-native-vector-icons/Fontisto'\n// import IconFoundation from 'react-native-vector-icons/Foundation'\n// import IconIonicons from 'react-native-vector-icons/Ionicons'\n// import IconMaterialIcons from 'react-native-vector-icons/MaterialIcons'\n// import IconMaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'\n// import IconOcticons from 'react-native-vector-icons/Octicons'\n// import IconZocial from 'react-native-vector-icons/Zocial'\n// import IconSimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons'\n\n\nconst IcoMoon = createIconSetFromIcoMoon(icoMoonConfig)\n\n\n// https://oblador.github.io/react-native-vector-icons/\n\ntype IconType = ReturnType<typeof createIconSetFromIcoMoon>\n\ninterface IconProps extends Omit<ComponentProps<IconType>, 'style'> {\n  style?: StyleProp<TextStyle>\n  rawSize?: number\n}\n\nexport const Icon = memo(({ size = 15, rawSize, color, style, ...props }: IconProps) => {\n  const theme = useTheme()\n  const textShadow = useTextShadow()\n  const newStyle = textShadow ? StyleSheet.compose({\n    textShadowColor: theme['c-primary-dark-300-alpha-800'],\n    textShadowOffset: { width: 0.2, height: 0.2 },\n    textShadowRadius: 2,\n  }, style) : style\n  return (\n    <IcoMoon\n      size={rawSize ?? scaleSizeW(size)}\n      color={color ?? theme['c-font']}\n      // @ts-expect-error\n      style={newStyle}\n      {...props}\n    />\n  )\n})\n\n\nexport {\n  // IconAntDesign,\n  // IconEntypo,\n  // IconEvilIcons,\n  // IconFeather,\n  // IconFontAwesome,\n  // IconFontAwesome5,\n  // IconFontisto,\n  // IconFoundation,\n  // IconIonicons,\n  // IconMaterialIcons,\n  // IconMaterialCommunityIcons,\n  // IconOcticons,\n  // IconZocial,\n  // IconSimpleLineIcons,\n}\n"
  },
  {
    "path": "src/components/common/Image.tsx",
    "content": "import { useTheme } from '@/store/theme/hook'\nimport { BorderRadius } from '@/theme'\nimport { createStyle } from '@/utils/tools'\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react'\nimport { View, type ViewProps, Image as _Image, StyleSheet } from 'react-native'\nimport FastImage, { type FastImageProps } from '@d11/react-native-fast-image'\nimport Text from './Text'\nimport { useLayout } from '@/utils/hooks'\nexport type { OnLoadEvent } from '@d11/react-native-fast-image'\n\nexport interface ImageProps extends ViewProps {\n  style: FastImageProps['style']\n  url?: string | number | null\n  cache?: boolean\n  resizeMode?: FastImageProps['resizeMode']\n  onError?: (url: string | number) => void\n}\n\n\nexport const 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\nconst EmptyPic = memo(({ style, nativeID }: { style: ImageProps['style'], nativeID: ImageProps['nativeID'] }) => {\n  const theme = useTheme()\n  const { onLayout, width } = useLayout()\n  const size = width * 0.36\n\n  return (\n    <View style={StyleSheet.compose({ ...styles.emptyPic, backgroundColor: theme['c-primary-light-900-alpha-200'], gap: size * 0.1 }, style)} onLayout={onLayout} nativeID={nativeID}>\n      <Text size={size} color={theme['c-primary-light-400-alpha-200']}>L</Text>\n      <Text size={size} color={theme['c-primary-light-400-alpha-200']} style={styles.text}>X</Text>\n    </View>\n  )\n})\n\nconst Image = memo(({ url, cache, resizeMode = FastImage.resizeMode.cover, style, onError, nativeID }: ImageProps) => {\n  const [isError, setError] = useState(false)\n  const handleError = useCallback(() => {\n    setError(true)\n    onError?.(url!)\n  }, [onError, url])\n  useEffect(() => {\n    setError(false)\n  }, [url])\n  let uri = typeof url == 'number'\n    ? _Image.resolveAssetSource(url).uri\n    : url?.startsWith('/')\n      ? 'file://' + url\n      : url\n  const showDefault = useMemo(() => !uri || isError, [isError, uri])\n  return (\n    showDefault ? <EmptyPic style={style} nativeID={nativeID} />\n      : (\n          <FastImage\n            style={style}\n            transition=\"fade\"\n            source={{\n              uri: uri!,\n              headers: defaultHeaders,\n              priority: FastImage.priority.normal,\n              cache: cache === false ? 'web' : 'immutable',\n            }}\n            onError={handleError}\n            resizeMode={resizeMode}\n            nativeID={nativeID}\n          />\n        )\n  )\n}, (prevProps, nextProps) => {\n  return prevProps.url == nextProps.url &&\n    prevProps.style == nextProps.style &&\n    prevProps.nativeID == nextProps.nativeID\n})\n\nexport const getSize = (uri: string, success: (width: number, height: number) => void, failure?: (error: any) => void) => {\n  _Image.getSize(uri, success, failure)\n}\nexport const clearMemoryCache = async() => {\n  return Promise.all([FastImage.clearMemoryCache(), FastImage.clearDiskCache()])\n}\nexport default Image\n\nconst styles = createStyle({\n  emptyPic: {\n    borderRadius: BorderRadius.normal,\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n  text: {\n    paddingLeft: 2,\n  },\n})\n"
  },
  {
    "path": "src/components/common/ImageBackground.tsx",
    "content": "// https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Image/ImageBackground.js\n/**\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n *\n * @flow\n * @format\n */\n\nimport { forwardRef } from 'react'\nimport {\n  View,\n  StyleSheet,\n  Image,\n} from 'react-native'\nimport type { ImageBackgroundProps as _ImageBackgroundProps } from 'react-native'\n\n\n/**\n * Very simple drop-in replacement for <Image> which supports nesting views.\n *\n * ```ReactNativeWebPlayer\n * import { Component } from 'react';\n * import { AppRegistry, View, ImageBackground, Text } from 'react-native';\n *\n * class DisplayAnImageBackground extends Component {\n *   render() {\n *     return (\n *       <ImageBackground\n *         style={{width: 50, height: 50}}\n *         source={{uri: 'https://reactnative.dev/img/opengraph.png'}}\n *       >\n *         <Text>React</Text>\n *       </ImageBackground>\n *     );\n *   }\n * }\n *\n * // App registration and rendering\n * AppRegistry.registerComponent('DisplayAnImageBackground', () => DisplayAnImageBackground);\n * ```\n */\n\nexport type ImageBackgroundType = View\n\nexport interface ImageBackgroundProps extends Omit<_ImageBackgroundProps, 'source'> {\n  source?: _ImageBackgroundProps['source'] | null\n}\n\nexport default forwardRef<View, ImageBackgroundProps>(({\n  children,\n  style,\n  imageStyle,\n  imageRef,\n  importantForAccessibility,\n  source,\n  ...props\n}, ref) => {\n  const flattenedStyle = StyleSheet.flatten(style)\n  return (\n    <View\n      accessibilityIgnoresInvertColors={true}\n      importantForAccessibility={importantForAccessibility}\n      ref={ref}\n      style={style}>\n      {\n        source == null ? null : (\n          <Image\n            {...props}\n            source={source}\n            importantForAccessibility={importantForAccessibility}\n            style={[\n              StyleSheet.absoluteFill,\n              {\n                // Temporary Workaround:\n                // Current (imperfect yet) implementation of <Image> overwrites width and height styles\n                // (which is not quite correct), and these styles conflict with explicitly set styles\n                // of <ImageBackground> and with our internal layout model here.\n                // So, we have to proxy/reapply these styles explicitly for actual <Image> component.\n                // This workaround should be removed after implementing proper support of\n                // intrinsic content size of the <Image>.\n                width: flattenedStyle?.width,\n                height: flattenedStyle?.height,\n              },\n              imageStyle,\n            ]}\n            ref={imageRef}\n          />\n        )\n      }\n      {children}\n    </View>\n  )\n})\n\nexport const prefetch = async(url: string) => {\n  return Image.prefetch(url)\n}\n"
  },
  {
    "path": "src/components/common/Input.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useCallback } from 'react'\nimport { TextInput, View, TouchableOpacity, StyleSheet, type TextInputProps } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { setSpText } from '@/utils/pixelRatio'\n\nconst styles = createStyle({\n  content: {\n    flexDirection: 'row',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    flexGrow: 1,\n    flexShrink: 1,\n    // height: 38,\n    alignItems: 'center',\n    // paddingRight: 5,\n  },\n  input: {\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    // backgroundColor: 'white',\n    borderRadius: 2,\n    paddingTop: 0,\n    paddingBottom: 0,\n    height: 32,\n    paddingLeft: 5,\n    paddingRight: 0,\n    flexGrow: 1,\n    flexShrink: 1,\n    // height: '100%',\n    // width: '100%',\n    fontSize: 14,\n  },\n  clearBtnContent: {\n    flexGrow: 0,\n    flexShrink: 0,\n  },\n  clearBtn: {\n    height: '70%',\n    paddingLeft: 5,\n    paddingRight: 5,\n    justifyContent: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n})\n\nexport interface InputProps extends TextInputProps {\n  onChangeText?: (value: string) => void\n  onClearText?: () => void\n  clearBtn?: boolean\n  size?: number\n}\n\n\nexport interface InputType {\n  blur: () => void\n  focus: () => void\n  clear: () => void\n  isFocused: () => boolean\n}\n\nexport default forwardRef<InputType, InputProps>(({ onChangeText, onClearText, clearBtn, style, size = 14, ...props }, ref) => {\n  const inputRef = useRef<TextInput>(null)\n  const theme = useTheme()\n  // const scaleClearBtn = useRef(new Animated.Value(0)).current\n\n  useImperativeHandle(ref, () => ({\n    blur() {\n      inputRef.current?.blur()\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n    clear() {\n      inputRef.current?.clear()\n    },\n    isFocused() {\n      return inputRef.current?.isFocused() ?? false\n    },\n  }))\n\n  // const showClearBtn = useCallback(() => {\n  //   Animated.timing(scaleClearBtn, {\n  //     toValue: 1,\n  //     duration: 200,\n  //     useNativeDriver: true,\n  //   }).start()\n  // }, [scaleClearBtn])\n  // const hideClearBtn = useCallback(() => {\n  //   Animated.timing(scaleClearBtn, {\n  //     toValue: 0,\n  //     duration: 200,\n  //     useNativeDriver: true,\n  //   }).start()\n  // }, [scaleClearBtn])\n\n  const clearText = useCallback(() => {\n    inputRef.current?.clear()\n    // hideClearBtn()\n    onChangeText?.('')\n    onClearText?.()\n  }, [onChangeText, onClearText])\n\n  const changeText = useCallback((text: string) => {\n    // if (text.length) {\n    //   showClearBtn()\n    // } else {\n    //   hideClearBtn()\n    // }\n    onChangeText?.(text)\n  }, [onChangeText])\n\n  return (\n    <View style={styles.content}>\n      <TextInput\n        autoCapitalize=\"none\"\n        onChangeText={changeText}\n        autoComplete=\"off\"\n        style={StyleSheet.compose({ ...styles.input, color: theme['c-font'], fontSize: setSpText(size) }, style)}\n        placeholderTextColor={theme['c-primary-dark-100-alpha-600']}\n        selectionColor={theme['c-primary-light-100-alpha-300']}\n        ref={inputRef} {...props} />\n      {/* <View style={styles.clearBtnContent}>\n      <Animated.View style={{ ...styles.clearBtnContent, transform: [{ scale: scaleClearBtn }] }}> */}\n        {clearBtn\n          ? <View style={styles.clearBtnContent}>\n              <TouchableOpacity style={styles.clearBtn} onPress={clearText}>\n                <Icon name=\"remove\" color={theme['c-primary-dark-100-alpha-500']} size={11} />\n              </TouchableOpacity>\n            </View>\n          : null\n        }\n      {/* </Animated.View>\n      </View> */}\n    </View>\n  )\n})\n\n"
  },
  {
    "path": "src/components/common/Loading.tsx",
    "content": "import { memo } from 'react'\n\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { ActivityIndicator, StyleSheet, type ActivityIndicatorProps } from 'react-native'\nimport { setSpText } from '@/utils/pixelRatio'\nimport Text from './Text'\n\nexport interface LoadingProps extends Omit<ActivityIndicatorProps, 'size'> {\n  size?: number\n  label?: string\n}\n\nconst LoadingLabel = ({ style, label, ...props }: LoadingProps) => {\n  return (\n    <>\n      <ActivityIndicator\n        style={StyleSheet.compose(styles.loadingLabel, style)}\n        {...props}\n      />\n      <Text color={props.color} size={props.size! * 0.8}>{label}</Text>\n    </>\n  )\n}\n\nexport default memo(({ size = 15, label, ...props }: LoadingProps) => {\n  const theme = useTheme()\n\n  return (\n    label ? <LoadingLabel color={theme['c-font-label']} size={setSpText(size)} label={label} {...props} />\n      : (\n          <ActivityIndicator\n            color={theme['c-font-label']}\n            size={setSpText(size)}\n            {...props}\n          />\n        )\n  )\n})\n\n\nconst styles = createStyle({\n  loadingLabel: {\n    marginRight: 6,\n  },\n})\n"
  },
  {
    "path": "src/components/common/LoadingMask.tsx",
    "content": "import { useState, useCallback, useMemo, useRef, forwardRef, useImperativeHandle } from 'react'\nimport { Animated } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport Loading from './Loading'\n\n// interface LoadingMaskProps {\n\n// }\n\nexport interface LoadingMaskType {\n  setVisible: (visible: boolean) => void\n}\n\nexport default forwardRef<LoadingMaskType, {}>((props, ref) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const animFade = useRef(new Animated.Value(0)).current\n  const [maskVisible, setMaskVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    setVisible(visible: boolean) {\n      if (maskVisible == visible) return\n      visible ? handleShow() : handleHide()\n    },\n  }))\n\n  const handleShow = useCallback(() => {\n    // console.log('handleShow')\n    setMaskVisible(true)\n\n    Animated.parallel([\n      Animated.timing(animFade, {\n        toValue: 1,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n    ]).start()\n  }, [animFade])\n\n  const handleHide = useCallback(() => {\n    // Will change fadeAnim value to 0 in 5 seconds\n    // console.log('handleHide')\n    Animated.parallel([\n      Animated.timing(animFade, {\n        toValue: 0,\n        duration: 300,\n        useNativeDriver: true,\n      }),\n    ]).start((finished) => {\n      // console.log(finished)\n      if (!finished) return\n      setMaskVisible(false)\n    })\n  }, [animFade])\n\n\n  const maskComponent = useMemo(() => (\n    <Animated.View style={{ ...styles.container, backgroundColor: theme['c-main-background'], opacity: animFade }}>\n      <Loading size={25} label={t('list_loading')} />\n    </Animated.View>\n  ), [animFade, t, theme])\n\n  return maskVisible ? maskComponent : null\n})\n\nconst styles = createStyle({\n  container: {\n    position: 'absolute',\n    left: 0,\n    top: 0,\n    width: '100%',\n    height: '100%',\n    zIndex: 10,\n    justifyContent: 'center',\n    alignItems: 'center',\n    gap: 10,\n  },\n})\n"
  },
  {
    "path": "src/components/common/Menu.tsx",
    "content": "import { useImperativeHandle, forwardRef, useMemo, useRef, useState, type Ref } from 'react'\nimport { View, Animated, TouchableHighlight } from 'react-native'\nimport { useWindowSize } from '@/utils/hooks'\n\nimport Modal, { type ModalType } from './Modal'\n\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from './Text'\nimport { scaleSizeH, scaleSizeW } from '@/utils/pixelRatio'\n\nconst menuItemHeight = scaleSizeH(40)\nconst menuItemWidth = scaleSizeW(100)\n\nexport interface Position { w: number, h: number, x: number, y: number, menuWidth?: number, menuHeight?: number }\nexport interface MenuSize { width?: number, height?: number }\nexport type Menus = Readonly<Array<{ action: string, label: string, disabled?: boolean }>>\n\nconst styles = createStyle({\n  mask: {\n    position: 'absolute',\n    top: 0,\n    bottom: 0,\n    left: 0,\n    right: 0,\n    opacity: 0,\n    backgroundColor: 'black',\n  },\n  menu: {\n    position: 'absolute',\n    // borderWidth: StyleSheet.hairlineWidth,\n    borderColor: 'lightgray',\n    borderRadius: 2,\n    backgroundColor: 'white',\n    elevation: 3,\n  },\n  menuItem: {\n    paddingLeft: 10,\n    paddingRight: 10,\n    // height: menuItemHeight,\n    // width: menuItemWidth,\n    // alignItems: 'center',\n    justifyContent: 'center',\n    // backgroundColor: '#ccc',\n  },\n  // menuText: {\n  //   // textAlign: 'center',\n  //   fontSize: 14,\n  // },\n})\n\ninterface Props<M extends Menus = Menus> {\n  menus: Readonly<M>\n  onPress?: (menu: M[number]) => void\n  buttonPosition: Position\n  menuSize: MenuSize\n  onHide: () => void\n  width?: number\n  height?: number\n  fontSize?: number\n  center?: boolean\n  activeId?: M[number]['action'] | null\n}\n\nconst Menu = ({\n  buttonPosition,\n  menuSize,\n  menus,\n  width,\n  height,\n  onPress = () => {},\n  onHide,\n  activeId,\n  fontSize = 15,\n  center = false,\n}: Props) => {\n  const theme = useTheme()\n  const windowSize = useWindowSize()\n  // const fadeAnim = useRef(new Animated.Value(0)).current\n  // console.log(buttonPosition)\n\n  const menuItemStyle = useMemo(() => {\n    return {\n      width: width ?? menuSize.width ?? menuItemWidth,\n      height: height ?? menuSize.height ?? menuItemHeight,\n    }\n  }, [menuSize, width, height])\n\n  const menuStyle = useMemo(() => {\n    let menuHeight = menus.length * menuItemStyle.height\n    const topHeight = buttonPosition.y - 20\n    const bottomHeight = windowSize.height - buttonPosition.y - buttonPosition.h - 20\n    if (menuHeight > topHeight && menuHeight > bottomHeight) menuHeight = Math.max(topHeight, bottomHeight)\n\n    const menuWidth = menuItemStyle.width\n    const bottomSpace = windowSize.height - buttonPosition.y - buttonPosition.h - 20\n    const rightSpace = windowSize.width - buttonPosition.x - menuWidth\n    const showInBottom = bottomSpace >= menuHeight\n    const showInRight = rightSpace >= menuWidth\n    const frameStyle: {\n      height: number\n      width: number\n      top: number\n      left?: number\n      right?: number\n    } = {\n      height: menuHeight,\n      top: showInBottom ? buttonPosition.y + buttonPosition.h : buttonPosition.y - menuHeight,\n      width: menuWidth,\n    }\n    if (showInRight) {\n      frameStyle.left = buttonPosition.x\n    } else {\n      frameStyle.right = windowSize.width - buttonPosition.x - buttonPosition.w\n    }\n    return frameStyle\n  }, [menus.length, menuItemStyle, buttonPosition, windowSize])\n\n  const menuPress = (menu: Menus[number]) => {\n    // if (menu.disabled) return\n    onPress(menu)\n    onHide()\n  }\n\n  // console.log('render menu')\n  // console.log(activeId)\n  // console.log(menuStyle)\n  // console.log(menuItemStyle)\n  return (\n    <View style={{ ...styles.menu, ...menuStyle, backgroundColor: theme['c-content-background'] }} onStartShouldSetResponder={() => true}>\n      <Animated.ScrollView keyboardShouldPersistTaps={'always'}>\n        {\n          menus.map((menu, index) => (\n            menu.disabled\n              ? (\n                  <View\n                    key={menu.action}\n                    style={{ ...styles.menuItem, width: menuItemStyle.width, height: menuItemStyle.height, opacity: 0.4 }}\n                  >\n                    <Text style={{ textAlign: center ? 'center' : 'left' }} size={fontSize} numberOfLines={1}>{menu.label}</Text>\n                  </View>\n                )\n              : menu.action == activeId\n                ? (\n                    <View\n                      key={menu.action}\n                      style={{ ...styles.menuItem, width: menuItemStyle.width, height: menuItemStyle.height }}\n                    >\n                      <Text style={{ textAlign: center ? 'center' : 'left' }} color={theme['c-primary-font-active']} size={fontSize} numberOfLines={1}>{menu.label}</Text>\n                    </View>\n                  )\n                : (\n                    <TouchableHighlight\n                      key={menu.action}\n                      style={{ ...styles.menuItem, width: menuItemStyle.width, height: menuItemStyle.height }}\n                      underlayColor={theme['c-primary-background-active']}\n                      onPress={() => { menuPress(menu) }}\n                    >\n                      <Text style={{ textAlign: center ? 'center' : 'left' }} size={fontSize} numberOfLines={1}>{menu.label}</Text>\n                    </TouchableHighlight>\n                  )\n\n          ))\n        }\n      </Animated.ScrollView>\n    </View>\n  )\n}\n\nexport interface MenuProps<M extends Menus = Menus> {\n  menus: M\n  onPress: (menu: M[number]) => void\n  onHide?: () => void\n  width?: number\n  height?: number\n  fontSize?: number\n  center?: boolean\n  activeId?: M[number]['action'] | null\n}\n\nexport interface MenuType {\n  show: (position: Position, menuSize?: MenuSize) => void\n  hide: () => void\n}\n\nconst Component = <M extends Menus>({ menus, width, height, activeId, onHide, onPress, fontSize, center }: MenuProps<M>, ref: Ref<MenuType>) => {\n  // console.log(visible)\n  const modalRef = useRef<ModalType>(null)\n  const [position, setPosition] = useState<Position>({ w: 0, h: 0, x: 0, y: 0 })\n  const [menuSize, setMenuSize] = useState<MenuSize>({ })\n  const hide = () => {\n    modalRef.current?.setVisible(false)\n  }\n  useImperativeHandle(ref, () => ({\n    show(newPosition, menuSize) {\n      setPosition(newPosition)\n      if (menuSize) setMenuSize(menuSize)\n      modalRef.current?.setVisible(true)\n    },\n    hide() {\n      hide()\n    },\n  }))\n\n  return (\n    <Modal onHide={onHide} ref={modalRef}>\n      <Menu menus={menus} width={width} height={height} activeId={activeId} buttonPosition={position} menuSize={menuSize} onPress={onPress} onHide={hide} fontSize={fontSize} center={center} />\n    </Modal>\n  )\n}\n\n// export default forwardRef(Component) as ForwardRefFn<MenuType>\nexport default forwardRef(Component) as <M extends Menus>(p: MenuProps<M> & { ref?: Ref<MenuType> }) => JSX.Element | null\n"
  },
  {
    "path": "src/components/common/Modal.tsx",
    "content": "// import { createStyle } from '@/utils/tools'\nimport { useImperativeHandle, forwardRef, useState, useMemo } from 'react'\nimport { Modal, TouchableWithoutFeedback, View, type ModalProps as _ModalProps } from 'react-native'\nimport { useStatusbarHeight } from '@/store/common/hook'\n// import { useWindowSize } from '@/utils/hooks'\n\n// const styles = createStyle({\n//   container: {\n//     flex: 1,\n//   },\n//   // mask: {\n//   //   position: 'absolute',\n//   //   top: 0,\n//   //   left: 0,\n//   //   bottom: 0,\n//   //   right: 0,\n//   //   // width: '100%',\n//   //   // height: '100%',\n//   // },\n// })\n\nexport interface ModalProps extends Omit<_ModalProps, 'visible'> {\n  onHide?: () => void\n  /**\n   * 按返回键是否隐藏\n   */\n  keyHide?: boolean\n  /**\n   * 点击背景是否隐藏\n   */\n  bgHide?: boolean\n  /**\n   * 背景颜色\n   */\n  bgColor?: string\n  /**\n   * 是否填充状态栏\n   */\n  statusBarPadding?: boolean\n}\n\n\nexport interface ModalType {\n  setVisible: (visible: boolean) => void\n}\n\nexport default forwardRef<ModalType, ModalProps>(({\n  onHide = () => {},\n  keyHide = true,\n  bgHide = true,\n  bgColor = 'rgba(0,0,0,0)',\n  statusBarPadding = true,\n  children,\n  ...props\n}: ModalProps, ref) => {\n  const [visible, setVisible] = useState(false)\n  // const { window: windowSize } = useWindowSize()\n  const statusBarHeight = useStatusbarHeight()\n  const handleRequestClose = () => {\n    if (keyHide) {\n      setVisible(false)\n      onHide()\n    }\n  }\n  const handleBgClose = () => {\n    if (bgHide) {\n      setVisible(false)\n      onHide()\n    }\n  }\n\n  useImperativeHandle(ref, () => ({\n    setVisible(_visible) {\n      if (visible == _visible) return\n      setVisible(_visible)\n      if (!_visible) onHide()\n    },\n  }))\n\n  const memoChildren = useMemo(() => children, [children])\n\n  return (\n    <Modal\n      animationType=\"fade\"\n      transparent={true}\n      hardwareAccelerated={true}\n      statusBarTranslucent={true}\n      visible={visible}\n      onRequestClose={handleRequestClose}\n      {...props}\n    >\n      {/* <StatusBar /> */}\n      {/* <View style={{ flex: 1, paddingTop: statusBarPadding ? StatusBar.currentHeight : 0 }}> */}\n      <TouchableWithoutFeedback style={{ flex: 1, paddingTop: statusBarPadding ? statusBarHeight : 0 }} onPress={handleBgClose}>\n        <View style={{ flex: 1, backgroundColor: bgColor }}>\n          {memoChildren}\n        </View>\n      </TouchableWithoutFeedback>\n      {/* </View> */}\n    </Modal>\n  )\n})\n"
  },
  {
    "path": "src/components/common/Popup.tsx",
    "content": "import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\n\nimport Modal, { type ModalType } from './Modal'\nimport { Icon } from '@/components/common/Icon'\nimport { useKeyboard } from '@/utils/hooks'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from './Text'\nimport { useStatusbarHeight } from '@/store/common/hook'\n\nconst styles = createStyle({\n  centeredView: {\n    flex: 1,\n    // justifyContent: 'flex-end',\n    // alignItems: 'center',\n  },\n  modalView: {\n    elevation: 6,\n    flexGrow: 0,\n    flexShrink: 1,\n  },\n  header: {\n    flex: 0,\n    flexDirection: 'row',\n    borderTopLeftRadius: 8,\n    borderTopRightRadius: 8,\n  },\n  title: {\n    paddingLeft: 10,\n    paddingRight: 25,\n    paddingTop: 10,\n    paddingBottom: 10,\n    // lineHeight: 20,\n  },\n  closeBtn: {\n    position: 'absolute',\n    right: 0,\n    // borderTopRightRadius: 8,\n    flexGrow: 0,\n    flexShrink: 0,\n    height: 30,\n    width: 30,\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: '#eee',\n  },\n})\n\nexport interface PopupProps {\n  onHide?: () => void\n  keyHide?: boolean\n  bgHide?: boolean\n  closeBtn?: boolean\n  position?: 'top' | 'left' | 'right' | 'bottom'\n  title?: string\n  children: React.ReactNode\n}\n\nexport interface PopupType {\n  setVisible: (visible: boolean) => void\n}\n\nexport default forwardRef<PopupType, PopupProps>(({\n  onHide = () => {},\n  keyHide = true,\n  bgHide = true,\n  closeBtn = true,\n  position = 'bottom',\n  title = '',\n  children,\n}: PopupProps, ref) => {\n  const theme = useTheme()\n  const { keyboardShown, keyboardHeight } = useKeyboard()\n  const statusBarHeight = useStatusbarHeight()\n\n  const modalRef = useRef<ModalType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setVisible(visible: boolean) {\n      modalRef.current?.setVisible(visible)\n    },\n  }))\n\n  const closeBtnComponent = useMemo(() => closeBtn\n    ? <TouchableOpacity style={styles.closeBtn} onPress={() => modalRef.current?.setVisible(false)}>\n        <Icon name=\"close\" style={{ color: theme['c-font-label'] }} size={12} />\n      </TouchableOpacity>\n    : null, [closeBtn, theme])\n\n  const [centeredViewStyle, modalViewStyle] = useMemo(() => {\n    switch (position) {\n      case 'top':\n        return [\n          {\n            position: 'absolute',\n            left: 0,\n            right: 0,\n            bottom: 0,\n            top: 0,\n            justifyContent: 'flex-start',\n          },\n          {\n            width: '100%',\n            maxHeight: '78%',\n            minHeight: '20%',\n            // backgroundColor: 'white',\n          },\n        ] as const\n      case 'left':\n        return [\n          {\n            position: 'absolute',\n            left: 0,\n            right: 0,\n            bottom: 0,\n            top: 0,\n            flexDirection: 'row',\n            justifyContent: 'flex-start',\n          },\n          {\n            minWidth: '45%',\n            maxWidth: '78%',\n            height: '100%',\n            paddingTop: statusBarHeight,\n            // backgroundColor: 'white',\n          },\n        ] as const\n      case 'right':\n        return [\n          {\n            position: 'absolute',\n            left: 0,\n            right: 0,\n            bottom: 0,\n            top: 0,\n            flexDirection: 'row',\n            justifyContent: 'flex-end',\n          },\n          {\n            minWidth: '45%',\n            maxWidth: '78%',\n            height: '100%',\n            paddingTop: statusBarHeight,\n            // backgroundColor: 'white',\n          },\n        ] as const\n      case 'bottom':\n      default:\n        return [\n          {\n            position: 'absolute',\n            left: 0,\n            right: 0,\n            bottom: 0,\n            top: 0,\n            justifyContent: 'flex-end',\n          },\n          {\n            width: '100%',\n            maxHeight: '78%',\n            minHeight: '20%',\n            // backgroundColor: 'white',\n            borderTopLeftRadius: 8,\n            borderTopRightRadius: 8,\n          },\n        ] as const\n    }\n  }, [position, statusBarHeight])\n\n  return (\n    <Modal onHide={onHide} keyHide={keyHide} bgHide={bgHide} bgColor=\"rgba(50,50,50,.2)\" ref={modalRef}>\n      <View style={{ ...styles.centeredView, ...centeredViewStyle, paddingBottom: keyboardShown ? keyboardHeight : 0 }}>\n        <View style={{ ...styles.modalView, ...modalViewStyle, backgroundColor: theme['c-content-background'] }} onStartShouldSetResponder={() => true}>\n          <View style={styles.header}>\n            <Text size={13} style={styles.title} numberOfLines={1}>{title}</Text>\n            {closeBtnComponent}\n          </View>\n          {children}\n        </View>\n      </View>\n    </Modal>\n  )\n})\n"
  },
  {
    "path": "src/components/common/ScaledImage.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { StyleSheet } from 'react-native'\nimport Image, { getSize, type ImageProps } from '@/components/common/Image'\n\nexport interface ScaledImageProps extends Pick<ImageProps, 'style'> {\n  url: string\n  width?: number\n  height?: number\n  maxWidth?: number\n  maxHeight?: number\n}\n\nexport default ({ url, width, height, maxWidth, maxHeight, style }: ScaledImageProps) => {\n  const [wh, setWH] = useState({ width: 0, height: 0 })\n\n  useEffect(() => {\n    getSize(url, (realWidth, realHeight) => {\n      let w = width ?? 0\n      let h = height ?? 0\n\n      if (w && !h) {\n        h = realHeight * (w / realWidth)\n      } else if (!w && h) {\n        w = realWidth * (h / realHeight)\n      } else {\n        if (maxWidth && realWidth > maxWidth) {\n          w = maxWidth\n          h = realHeight * (w / realWidth)\n\n          if (maxHeight && h > maxHeight) {\n            w = realWidth * (maxHeight / realHeight)\n            h = maxHeight\n          }\n        } else if (maxHeight && realHeight > maxHeight) {\n          w = realWidth * (h / realHeight)\n          h = maxHeight\n        }\n      }\n      setWH({ width: w || realWidth, height: h || realHeight })\n    })\n  }, [height, maxHeight, maxWidth, url, width])\n\n  return (\n    wh.width ? (<Image\n      url={url}\n      style={StyleSheet.compose({ height: wh.height, width: wh.width }, style)}\n    />) : null\n  )\n}\n"
  },
  {
    "path": "src/components/common/Slider.tsx",
    "content": "import { memo } from 'react'\n\nimport Slider, { type SliderProps as _SliderProps } from '@react-native-community/slider'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\n\nexport type SliderProps = Pick<_SliderProps,\n'value'\n| 'minimumValue'\n| 'maximumValue'\n| 'onSlidingStart'\n| 'onSlidingComplete'\n| 'onValueChange'\n| 'step'\n>\n\nexport default memo(({ value, minimumValue, maximumValue, onSlidingStart, onSlidingComplete, onValueChange, step }: SliderProps) => {\n  const theme = useTheme()\n\n  const handleValueChange = (value: number) => {\n    // 修复当value小于等于minimumValue时，首次调用会传入0的问题\n    if (onValueChange && minimumValue != null) onValueChange(Math.max(value, minimumValue))\n  }\n\n  return (\n    <Slider\n      value={value}\n      style={styles.slider}\n      minimumValue={minimumValue}\n      maximumValue={maximumValue}\n      minimumTrackTintColor={theme['c-primary-alpha-500']}\n      maximumTrackTintColor={theme['c-primary-alpha-500']}\n      thumbTintColor={theme['c-primary']}\n      onSlidingStart={onSlidingStart}\n      onSlidingComplete={onSlidingComplete}\n      onValueChange={handleValueChange}\n      step={step}\n    />\n  )\n})\n\n\nconst styles = createStyle({\n  slider: {\n    flexShrink: 0,\n    flexGrow: 1,\n    // width: '100%',\n    // maxWidth: 300,\n    height: 40,\n    // backgroundColor: '#eee',\n  },\n})\n"
  },
  {
    "path": "src/components/common/StatusBar.tsx",
    "content": "import { useTheme } from '@/store/theme/hook'\nimport { StatusBar as RNStatusBar } from 'react-native'\n\nconst StatusBar = function() {\n  const theme = useTheme()\n  const statusBarStyle = theme.isDark ? 'light-content' : 'dark-content'\n  return <RNStatusBar backgroundColor=\"rgba(0,0,0,0)\" barStyle={statusBarStyle} translucent={true} />\n}\n\nStatusBar.currentHeight = RNStatusBar.currentHeight ?? 0\nStatusBar.setBarStyle = RNStatusBar.setBarStyle\n\nexport default StatusBar\n"
  },
  {
    "path": "src/components/common/Text.tsx",
    "content": "import { memo, type ComponentProps } from 'react'\nimport { Text, type TextProps as _TextProps, StyleSheet, Animated, type ColorValue, type TextStyle } from 'react-native'\nimport { useTextShadow, useTheme } from '@/store/theme/hook'\nimport { setSpText } from '@/utils/pixelRatio'\nimport { useAnimateColor } from '@/utils/hooks/useAnimateColor'\nimport { DEFAULT_DURATION, useAnimateNumber } from '@/utils/hooks/useAnimateNumber'\n// import { AppColors } from '@/theme'\n\nexport interface TextProps extends _TextProps {\n  /**\n   * 字体大小\n   */\n  size?: number\n  /**\n   * 字体颜色\n   */\n  color?: ColorValue\n}\n\n// const warpText = <P extends TextProps>(Component: ComponentType<TextProps>) => {\n//   return ({ style, size = 15, color, children, ...props }: P) => {\n//     const theme = useTheme()\n//     return (\n//       <Component\n//         style={StyleSheet.compose({ fontFamily: 'System', fontSize: setSpText(size), color: color ?? theme['c-font'] }, style)}\n//         {...props}\n//       >{children}</Component>\n//     )\n//   }\n// }\n\nexport default memo(({ style, size = 15, color, children, ...props }: TextProps) => {\n  const theme = useTheme()\n  const textShadow = useTextShadow()\n  style = StyleSheet.compose(textShadow ? {\n    // fontFamily: 'System',\n    textShadowColor: theme['c-primary-dark-300-alpha-800'],\n    textShadowOffset: { width: 0.2, height: 0.2 },\n    textShadowRadius: 2,\n    fontSize: setSpText(size),\n    color: color ?? theme['c-font'],\n  } : {\n    // fontFamily: 'System',\n    fontSize: setSpText(size),\n    color: color ?? theme['c-font'],\n  }, style)\n\n  return (\n    <Text\n      style={style}\n      {...props}\n    >{children}</Text>\n  )\n})\n\nexport interface AnimatedTextProps extends _AnimatedTextProps {\n  /**\n   * 字体大小\n   */\n  size?: number\n  /**\n   * 字体颜色\n   */\n  color?: ColorValue\n}\nexport const AnimatedText = ({ style, size = 15, color, children, ...props }: AnimatedTextProps) => {\n  const theme = useTheme()\n  const textShadow = useTextShadow()\n  style = StyleSheet.compose(textShadow ? {\n    // fontFamily: 'System',\n    textShadowColor: theme['c-primary-dark-300-alpha-800'],\n    textShadowOffset: { width: 0.2, height: 0.2 },\n    textShadowRadius: 2,\n    fontSize: setSpText(size),\n    color: color ?? theme['c-font'],\n  } : {\n    // fontFamily: 'System',\n    fontSize: setSpText(size),\n    color: color ?? theme['c-font'],\n  }, style as TextStyle)\n\n  return <Animated.Text style={style} {...props}>{children}</Animated.Text>\n}\n\n\ntype _AnimatedTextProps = ComponentProps<(typeof Animated)['Text']>\nexport interface AnimatedColorTextProps extends _AnimatedTextProps {\n  /**\n   * 字体大小\n   */\n  size?: number\n  /**\n   * 字体颜色\n   */\n  color?: string\n  /**\n   * 字体透明度\n   */\n  opacity?: number\n}\nexport const AnimatedColorText = ({ style, size = 15, opacity: _opacity, color: _color, children, ...props }: AnimatedColorTextProps) => {\n  const theme = useTheme()\n  const textShadow = useTextShadow()\n\n  const [color] = useAnimateColor(_color ?? theme['c-font'])\n  const [opacity] = useAnimateNumber(_opacity ?? 1, DEFAULT_DURATION, false)\n\n  style = StyleSheet.compose(textShadow ? {\n    // fontFamily: 'System',\n    textShadowColor: theme['c-primary-dark-300-alpha-800'],\n    textShadowOffset: { width: 0.2, height: 0.2 },\n    textShadowRadius: 2,\n    fontSize: setSpText(size),\n    color: color as unknown as ColorValue,\n    opacity,\n  } : {\n    // fontFamily: 'System',\n    fontSize: setSpText(size),\n    color: color as unknown as ColorValue,\n    opacity,\n  }, style as TextStyle)\n\n  return <Animated.Text style={style} {...props}>{children}</Animated.Text>\n}\n"
  },
  {
    "path": "src/components/player/PlayerBar/components/ControlBtn.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { useIsPlay } from '@/store/player/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { playNext, playPrev, togglePlay } from '@/core/player/player'\nimport { createStyle } from '@/utils/tools'\nimport { useHorizontalMode } from '@/utils/hooks'\n\nconst BTN_SIZE = 24\nconst handlePlayPrev = () => {\n  void playPrev()\n}\nconst handlePlayNext = () => {\n  void playNext()\n}\n\nconst PlayPrevBtn = () => {\n  const theme = useTheme()\n\n  return (\n    <TouchableOpacity style={styles.cotrolBtn} activeOpacity={0.5} onPress={handlePlayPrev}>\n      <Icon name='prevMusic' color={theme['c-button-font']} size={BTN_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nconst PlayNextBtn = () => {\n  const theme = useTheme()\n\n  return (\n    <TouchableOpacity style={styles.cotrolBtn} activeOpacity={0.5} onPress={handlePlayNext}>\n      <Icon name='nextMusic' color={theme['c-button-font']} size={BTN_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nconst TogglePlayBtn = () => {\n  const isPlay = useIsPlay()\n  const theme = useTheme()\n\n  return (\n    <TouchableOpacity style={styles.cotrolBtn} activeOpacity={0.5} onPress={togglePlay}>\n      <Icon name={isPlay ? 'pause' : 'play'} color={theme['c-button-font']} size={BTN_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nexport default () => {\n  const isHorizontalMode = useHorizontalMode()\n  return (\n    <>\n      {/* <TouchableOpacity activeOpacity={0.5} onPress={toggleNextPlayMode}>\n        <Text style={{ ...styles.cotrolBtn }}>\n          <Icon name={playModeIcon} style={{ color: theme.secondary10 }} size={18} />\n        </Text>\n      </TouchableOpacity>\n    */}\n      {/* {btnPrev} */}\n      { isHorizontalMode ? <PlayPrevBtn /> : null }\n      <TogglePlayBtn />\n      <PlayNextBtn />\n    </>\n  )\n}\n\n\nconst styles = createStyle({\n  cotrolBtn: {\n    width: 46,\n    height: 46,\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n  },\n})\n"
  },
  {
    "path": "src/components/player/PlayerBar/components/Pic.tsx",
    "content": "import { StyleSheet, TouchableOpacity } from 'react-native'\nimport { navigations } from '@/navigation'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport commonState from '@/store/common/state'\nimport playerState from '@/store/player/state'\nimport { LIST_IDS, NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport Image from '@/components/common/Image'\nimport { useCallback } from 'react'\nimport { setLoadErrorPicUrl, setMusicInfo } from '@/core/player/playInfo'\n\nconst PIC_HEIGHT = scaleSizeH(46)\n\nconst styles = StyleSheet.create({\n  image: {\n    width: PIC_HEIGHT,\n    height: PIC_HEIGHT,\n    borderRadius: 2,\n  },\n})\n\nexport default ({ isHome }: { isHome: boolean }) => {\n  const musicInfo = usePlayerMusicInfo()\n  const handlePress = () => {\n    // console.log('')\n    // console.log(playMusicInfo)\n    if (!musicInfo.id) return\n    navigations.pushPlayDetailScreen(commonState.componentIds.home!)\n\n    // toast(global.i18n.t('play_detail_todo_tip'), 'long')\n  }\n\n  const handleLongPress = () => {\n    if (!isHome) return\n    const listId = playerState.playMusicInfo.listId\n    if (!listId || listId == LIST_IDS.DOWNLOAD) return\n    global.app_event.jumpListPosition()\n  }\n\n  const handleError = useCallback((url: string | number) => {\n    setLoadErrorPicUrl(url as string)\n    setMusicInfo({\n      pic: null,\n    })\n  }, [])\n\n  return (\n    <TouchableOpacity onLongPress={handleLongPress} onPress={handlePress} activeOpacity={0.7} >\n      <Image url={musicInfo.pic} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_pic} style={styles.image} onError={handleError} />\n    </TouchableOpacity>\n  )\n}\n\n\n// const styles = StyleSheet.create({\n//   playInfoImg: {\n\n//   },\n// })\n"
  },
  {
    "path": "src/components/player/PlayerBar/components/PlayInfo.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View, StyleSheet } from 'react-native'\n\nimport Progress, { ProgressPlain } from '@/components/player/Progress'\nimport Status from './Status'\nimport { useProgress } from '@/store/player/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { usePageVisible } from '@/store/common/hook'\nimport { scaleSizeH, scaleSizeW, scaleSizeWR } from '@/utils/pixelRatio'\nimport { useBufferProgress } from '@/plugins/player'\nimport { useSettingValue } from '@/store/setting/hook'\n\nconst FONT_SIZE = 13\nconst PADDING_TOP_RAW = 1.8\nconst PADDING_TOP = Math.round(scaleSizeWR(PADDING_TOP_RAW))\nconst MARGIN_TOP = Math.round(scaleSizeH(2))\nconst PADDING_TOP_PROGRESS = PADDING_TOP + MARGIN_TOP\n\nconst PlayTimeCurrent = ({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  // console.log(timeStr)\n  return <Text size={FONT_SIZE} color={theme['c-500']}>{timeStr}</Text>\n}\n\nconst PlayTimeMax = memo(({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  return <Text size={FONT_SIZE} color={theme['c-500']}>{timeStr}</Text>\n})\n\nexport default ({ isHome }: { isHome: boolean }) => {\n  const theme = useTheme()\n  const [autoUpdate, setAutoUpdate] = useState(true)\n  const { maxPlayTimeStr, nowPlayTimeStr, progress, maxPlayTime } = useProgress(autoUpdate)\n  const buffered = useBufferProgress()\n  const allowProgressBarSeek = useSettingValue('common.allowProgressBarSeek')\n\n  usePageVisible([COMPONENT_IDS.home], useCallback((visible) => {\n    if (isHome) setAutoUpdate(visible)\n  }, [isHome]))\n\n  return (\n    <View style={stylesRaw.container}>\n      {/* <MusicName /> */}\n      <View style={styles.status}>\n        <Status autoUpdate={autoUpdate} />\n      </View>\n      <View style={{ flexGrow: 0, flexShrink: 0, flexDirection: 'row', alignItems: 'flex-start' }} >\n        <PlayTimeCurrent timeStr={nowPlayTimeStr} />\n        <Text size={FONT_SIZE} color={theme['c-500']}> / </Text>\n        <PlayTimeMax timeStr={maxPlayTimeStr} />\n      </View>\n      <View style={[StyleSheet.absoluteFill, stylesRaw.progress]}>\n        {\n          allowProgressBarSeek\n            ? <Progress progress={progress} duration={maxPlayTime} buffered={buffered} paddingTop={PADDING_TOP_PROGRESS} />\n            : <ProgressPlain progress={progress} duration={maxPlayTime} buffered={buffered} paddingTop={PADDING_TOP_PROGRESS} />\n        }\n      </View>\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  // container: {\n  //   // height: 16,\n  //   maxHeight: 32,\n  //   flexGrow: 1,\n  //   flexShrink: 0,\n  //   // flexDirection: 'column',\n  //   // justifyContent: 'center',\n  //   // alignItems: 'center',\n  //   // marginBottom: -1,\n  //   // backgroundColor: '#ccc',\n  //   // overflow: 'hidden',\n  //   // height:\n  //   // position: 'absolute',\n  //   // width: '100%',\n  //   // top: 0,\n  //   paddingTop: PADDING_TOP_RAW,\n  //   paddingHorizontal: 3,\n  //   flexDirection: 'row',\n  //   alignItems: 'center',\n  //   justifyContent: 'space-between',\n  // },\n  status: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingRight: 5,\n    // backgroundColor: '#ccc',\n  },\n})\n\nconst stylesRaw = StyleSheet.create({\n  container: {\n    // height: 16,\n    maxHeight: scaleSizeH(32),\n    flexGrow: 1,\n    flexShrink: 0,\n    paddingTop: PADDING_TOP,\n    paddingHorizontal: scaleSizeW(3),\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-between',\n  },\n  progress: {\n    // paddingVertical: 2,\n    marginBottom: MARGIN_TOP,\n    zIndex: 100,\n  },\n})\n"
  },
  {
    "path": "src/components/player/PlayerBar/components/Status.tsx",
    "content": "import { useLrcPlay } from '@/plugins/lyric'\nimport { useIsPlay, useStatusText } from '@/store/player/hook'\n// import { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\n\nexport default ({ autoUpdate }: { autoUpdate: boolean }) => {\n  const { text } = useLrcPlay(autoUpdate)\n  const statusText = useStatusText()\n  const isPlay = useIsPlay()\n  // console.log('render status')\n\n  const status = isPlay ? text : statusText\n\n  return <Text numberOfLines={1} size={12}>{status}</Text>\n}\n\n// const styles = createStyle({\n//   text: {\n//     // fontSize: 10,\n//     // lineHeight: 18,\n//     // height: 18,\n//     // height: '100%',\n//     // backgroundColor: 'rgba(0,0,0,0.2)',\n//   },\n// })\n"
  },
  {
    "path": "src/components/player/PlayerBar/components/Title.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { navigations } from '@/navigation'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\n// import { toast } from '@/utils/tools'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport commonState from '@/store/common/state'\nimport playerState from '@/store/player/state'\nimport Text from '@/components/common/Text'\nimport { LIST_IDS } from '@/config/constant'\nimport { createStyle } from '@/utils/tools'\n\n\nexport default ({ isHome }: { isHome: boolean }) => {\n  // const { t } = useTranslation()\n  const musicInfo = usePlayerMusicInfo()\n  const downloadFileName = useSettingValue('download.fileName')\n  const theme = useTheme()\n\n  const handlePress = () => {\n    // console.log('')\n    // console.log(playMusicInfo)\n    if (!musicInfo.id) return\n    navigations.pushPlayDetailScreen(commonState.componentIds.home!)\n    // toast(global.i18n.t('play_detail_todo_tip'), 'long')\n  }\n\n  const handleLongPress = () => {\n    const listId = playerState.playMusicInfo.listId\n    if (!listId || listId == LIST_IDS.DOWNLOAD) return\n    global.app_event.jumpListPosition()\n  }\n  // console.log('render title')\n\n  const title = musicInfo.id\n    ? musicInfo.singer\n      ? downloadFileName.replace('歌手', musicInfo.singer).replace('歌名', musicInfo.name)\n      : musicInfo.name\n    : ''\n  // console.log(playMusicInfo)\n  return (\n    <TouchableOpacity style={styles.container} onLongPress={handleLongPress} onPress={handlePress} activeOpacity={0.7} >\n      <Text color={theme['c-font-label']} numberOfLines={1}>{title}</Text>\n    </TouchableOpacity>\n  )\n}\n// const Singer = () => {\n//   const playMusicInfo = useGetter('player', 'playMusicInfo')\n//   return (\n//     <View style={{ flexGrow: 0, flexShrink: 0 }}>\n//       <Text style={{ width: '100%', color: AppColors.normal }} numberOfLines={1}>\n//         {playMusicInfo ? playMusicInfo.musicInfo.singer : ''}\n//       </Text>\n//     </View>\n//   )\n// }\n// const MusicName = () => {\n//   const playMusicInfo = useGetter('player', 'playMusicInfo')\n//   return (\n//     <View style={{ flexGrow: 0, flexShrink: 1 }}>\n//       <Text style={{ width: '100%', color: AppColors.normal }} numberOfLines={1}>\n//         {playMusicInfo ? playMusicInfo.musicInfo.name : '^-^'}\n//       </Text>\n//     </View>\n//   )\n// }\n\nconst styles = createStyle({\n  container: {\n    width: '100%',\n    paddingHorizontal: 2,\n    // paddingBottom: 4,\n    // height: '50%',\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n  },\n})\n"
  },
  {
    "path": "src/components/player/PlayerBar/index.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { View } from 'react-native'\nimport { useKeyboard } from '@/utils/hooks'\n\nimport Pic from './components/Pic'\nimport Title from './components/Title'\nimport PlayInfo from './components/PlayInfo'\nimport ControlBtn from './components/ControlBtn'\nimport { createStyle } from '@/utils/tools'\n// import { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nexport default memo(({ isHome = false }: { isHome?: boolean }) => {\n  // const { onLayout, ...layout } = useLayout()\n  const { keyboardShown } = useKeyboard()\n  const theme = useTheme()\n  const autoHidePlayBar = useSettingValue('common.autoHidePlayBar')\n\n  const playerComponent = useMemo(() => (\n    <View style={{ ...styles.container, backgroundColor: theme['c-content-background'] }}>\n      <Pic isHome={isHome} />\n      <View style={styles.center}>\n        <Title isHome={isHome} />\n        {/* <View style={{ ...styles.row, justifyContent: 'space-between' }}>\n          <PlayTime />\n        </View> */}\n        <PlayInfo isHome={isHome} />\n      </View>\n      <View style={styles.right}>\n        <ControlBtn />\n      </View>\n    </View>\n  ), [theme, isHome])\n\n  // console.log('render pb')\n\n  return autoHidePlayBar && keyboardShown ? null : playerComponent\n})\n\n\nconst styles = createStyle({\n  container: {\n    width: '100%',\n    // height: 100,\n    // paddingTop: progressContentPadding,\n    // marginTop: -progressContentPadding,\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n    // borderTopWidth: BorderWidths.normal2,\n    paddingVertical: 5,\n    paddingLeft: 5,\n    // backgroundColor: AppColors.primary,\n    // backgroundColor: 'red',\n    borderTopLeftRadius: 6,\n    borderTopRightRadius: 6,\n    flexDirection: 'row',\n    alignItems: 'center',\n    elevation: 10,\n  },\n  left: {\n    // borderRadius: 3,\n    flexGrow: 0,\n    flexShrink: 0,\n  },\n  center: {\n    flexDirection: 'column',\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingLeft: 5,\n    height: '100%',\n    // justifyContent: 'space-evenly',\n    // height: 48,\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n  },\n  right: {\n    flexDirection: 'row',\n    alignItems: 'center',\n    flexGrow: 0,\n    flexShrink: 0,\n    paddingLeft: 5,\n    paddingRight: 5,\n  },\n  // row: {\n  //   flexDirection: 'row',\n  //   flexGrow: 0,\n  //   flexShrink: 0,\n  // },\n})\n"
  },
  {
    "path": "src/components/player/Progress.tsx",
    "content": "import { memo, useCallback, useEffect, useRef, useState } from 'react'\nimport { View, PanResponder } from 'react-native'\nimport { useDrag } from '@/utils/hooks'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\n// import { scaleSizeW } from '@/utils/pixelRatio'\n// import { AppColors } from '@/theme'\n\n\nconst DefaultBar = memo(() => {\n  // const theme = useTheme()\n\n  return <View style={{\n    ...styles.progressBar,\n    // backgroundColor: theme['c-primary-light-200-alpha-900'],\n    position: 'absolute',\n    width: '100%',\n    left: 0,\n    top: 0,\n  }}></View>\n})\n\nconst BufferedBar = memo(({ progress }: { progress: number }) => {\n  // console.log(bufferedProgress)\n  const theme = useTheme()\n  return <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-600-alpha-900'], position: 'absolute', width: `${progress * 100}%`, left: 0, top: 0 }}></View>\n})\n\nconst PreassBar = memo(({ onDragState, setDragProgress, onSetProgress }: {\n  onDragState: (drag: boolean) => void\n  setDragProgress: (progress: number) => void\n  onSetProgress: (progress: number) => void\n}) => {\n  const {\n    onLayout,\n    onDragStart,\n    onDragEnd,\n    onDrag,\n  } = useDrag(onSetProgress, onDragState, setDragProgress)\n  // const handlePress = useCallback((event: GestureResponderEvent) => {\n  //   onPress(event.nativeEvent.locationX)\n  // }, [onPress])\n\n  const panResponder = useRef(\n    PanResponder.create({\n      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,\n      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,\n\n      // onMoveShouldSetPanResponder: () => true,\n      onPanResponderMove: (evt, gestureState) => {\n        onDrag(gestureState.dx)\n      },\n      onPanResponderGrant: (evt, gestureState) => {\n        // console.log(evt.nativeEvent.locationX, gestureState)\n        onDragStart(gestureState.dx, evt.nativeEvent.locationX)\n      },\n      onPanResponderRelease: () => {\n        onDragEnd()\n      },\n      // onPanResponderTerminate: (evt, gestureState) => {\n      //   onDragEnd()\n      // },\n    }),\n  ).current\n\n  return <View onLayout={onLayout} style={styles.pressBar} {...panResponder.panHandlers} />\n})\n\n\nexport const ProgressPlain = ({ progress, duration, buffered, paddingTop }: {\n  progress: number\n  duration: number\n  buffered: number\n  paddingTop?: number\n}) => {\n  // const { progress } = usePlayTimeBuffer()\n  const theme = useTheme()\n  // console.log(progress)\n  const progressStr: `${number}%` = `${progress * 100}%`\n\n  const durationRef = useRef(duration)\n  useEffect(() => {\n    durationRef.current = duration\n  }, [duration])\n\n  return (\n    <View style={{ ...styles.progress, paddingTop }}>\n      <View style={{ flex: 1 }}>\n        <DefaultBar />\n        <BufferedBar progress={buffered} />\n        <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-alpha-900'], width: progressStr, position: 'absolute', left: 0, top: 0 }} />\n      </View>\n      <View style={styles.pressBar} />\n    </View>\n  )\n}\n\nconst Progress = ({ progress, duration, buffered, paddingTop }: {\n  progress: number\n  duration: number\n  buffered: number\n  paddingTop?: number\n}) => {\n  // const { progress } = usePlayTimeBuffer()\n  const theme = useTheme()\n  const [draging, setDraging] = useState(false)\n  const [dragProgress, setDragProgress] = useState(0)\n  // console.log(progress)\n  const progressStr: `${number}%` = `${progress * 100}%`\n\n  const durationRef = useRef(duration)\n  useEffect(() => {\n    durationRef.current = duration\n  }, [duration])\n  const onSetProgress = useCallback((progress: number) => {\n    global.app_event.setProgress(progress * durationRef.current)\n  }, [])\n\n  return (\n    <View style={{ ...styles.progress, paddingTop }}>\n      <View style={{ flex: 1 }}>\n        <DefaultBar />\n        <BufferedBar progress={buffered} />\n        {\n          draging\n            ? (\n                <>\n                  <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-200-alpha-900'], width: progressStr, position: 'absolute', left: 0, top: 0 }} />\n                  <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-100-alpha-800'], width: `${dragProgress * 100}%`, position: 'absolute', left: 0, top: 0 }} />\n                </>\n              ) : (\n                <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-alpha-900'], width: progressStr, position: 'absolute', left: 0, top: 0 }} />\n              )\n        }\n      </View>\n      <PreassBar onDragState={setDraging} setDragProgress={setDragProgress} onSetProgress={onSetProgress} />\n      {/* <View style={{ ...styles.progressBar, height: '100%', width: progressStr }}><Pressable style={styles.progressDot}></Pressable></View> */}\n    </View>\n  )\n}\n\n\n// const progressContentPadding = 9\n// const progressHeight = 3\nconst styles = createStyle({\n  progress: {\n    flex: 1,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    zIndex: 1,\n  },\n  progressBar: {\n    height: '100%',\n    borderRadius: 3,\n  },\n  pressBar: {\n    position: 'absolute',\n    // backgroundColor: 'rgba(0,0,0,0.5)',\n    left: 0,\n    top: 0,\n    // height: progressContentPadding * 2 + progressHeight,\n    height: '100%',\n    width: '100%',\n  },\n})\n\nexport default Progress\n"
  },
  {
    "path": "src/components/player/ProgressBar.tsx",
    "content": "import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { View, PanResponder } from 'react-native'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeW, scaleSizeH } from '@/utils/pixelRatio'\nimport { useDrag } from '@/utils/hooks'\nimport { Icon } from '@/components/common/Icon'\n// import { AppColors } from '@/theme'\n\n\nconst DefaultBar = memo(() => {\n  const theme = useTheme()\n\n  return <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-300-alpha-800'], position: 'absolute', width: '100%', left: 0, top: 0 }}></View>\n})\n\nconst BufferedBar = memo(({ progress }: { progress: number }) => {\n  // console.log(bufferedProgress)\n  const theme = useTheme()\n  return <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-400-alpha-700'], position: 'absolute', width: `${progress * 100}%`, left: 0, top: 0 }}></View>\n})\n\n\nconst PreassBar = memo(({ onDragState, setDragProgress, onSetProgress }: {\n  onDragState: (drag: boolean) => void\n  setDragProgress: (progress: number) => void\n  onSetProgress: (progress: number) => void\n}) => {\n  const {\n    onLayout,\n    onDragStart,\n    onDragEnd,\n    onDrag,\n  } = useDrag(onSetProgress, onDragState, setDragProgress)\n  // const handlePress = useCallback((event: GestureResponderEvent) => {\n  //   onPress(event.nativeEvent.locationX)\n  // }, [onPress])\n\n  const panResponder = useRef(\n    PanResponder.create({\n      onStartShouldSetPanResponderCapture: (evt, gestureState) => true,\n      onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,\n\n      // onMoveShouldSetPanResponder: () => true,\n      onPanResponderMove: (evt, gestureState) => {\n        onDrag(gestureState.dx)\n      },\n      onPanResponderGrant: (evt, gestureState) => {\n        // console.log(evt.nativeEvent.locationX, gestureState)\n        onDragStart(gestureState.dx, evt.nativeEvent.locationX)\n      },\n      onPanResponderRelease: () => {\n        onDragEnd()\n      },\n      // onPanResponderTerminate: (evt, gestureState) => {\n      //   onDragEnd()\n      // },\n    }),\n  ).current\n\n  return <View onLayout={onLayout} style={styles.pressBar} {...panResponder.panHandlers} />\n})\n\n\nconst Progress = ({ progress, duration, buffered }: {\n  progress: number\n  duration: number\n  buffered: number\n}) => {\n  // const { progress: bufferProgress } = usePlayTimeBuffer()\n  const theme = useTheme()\n  const [draging, setDraging] = useState(false)\n  const [dragProgress, setDragProgress] = useState(0)\n  // console.log(progress)\n  const progressStr: `${number}%` = `${progress * 100}%`\n\n  const progressDotStyle = useMemo(() => {\n    return {\n      width: progressDotSize,\n      position: 'absolute',\n      right: -progressDotSize / 2,\n      top: -(progressDotSize - progressHeightSize) / 2,\n    } as const\n  }, [])\n\n  const durationRef = useRef(duration)\n  useEffect(() => {\n    durationRef.current = duration\n  }, [duration])\n  const onSetProgress = useCallback((progress: number) => {\n    global.app_event.setProgress(progress * durationRef.current)\n  }, [])\n\n  return (\n    <View style={styles.progress}>\n      <View>\n        <DefaultBar />\n        <BufferedBar progress={buffered} />\n        {\n          draging\n            ? (\n                <>\n                  <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-100-alpha-700'], width: progressStr, position: 'absolute', left: 0, top: 0 }} />\n                  <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-100-alpha-600'], width: `${dragProgress * 100}%`, position: 'absolute', left: 0, top: 0 }}>\n                    <Icon name=\"full_stop\" color={theme['c-primary-light-100']} rawSize={progressDotSize} style={progressDotStyle} />\n                  </View>\n                </>\n              ) : (\n                <View style={{ ...styles.progressBar, backgroundColor: theme['c-primary-light-100-alpha-400'], width: progressStr, position: 'absolute', left: 0, top: 0 }}>\n                  <Icon name=\"full_stop\" color={theme['c-primary-light-100']} rawSize={progressDotSize} style={progressDotStyle} />\n                </View>\n              )\n        }\n\n      </View>\n      <PreassBar onDragState={setDraging} setDragProgress={setDragProgress} onSetProgress={onSetProgress} />\n      {/* <View style={{ ...styles.progressBar, height: '100%', width: progressStr }}><Pressable style={styles.progressDot}></Pressable></View> */}\n    </View>\n  )\n}\n\n\nconst progressContentPadding = 10\nconst progressHeight = 3.6\nconst progressContentHeight = progressContentPadding * 2 + progressHeight\nconst progressHeightSize = scaleSizeH(progressHeight)\nlet progressDotSize = scaleSizeW(progressContentHeight * 0.8)\nconst styles = createStyle({\n  progress: {\n    width: '100%',\n    height: progressContentHeight,\n    // backgroundColor: 'rgba(0,0,0,0.5)',\n    paddingTop: progressContentPadding,\n    paddingBottom: progressContentPadding,\n    zIndex: 1,\n  },\n  progressBar: {\n    height: progressHeight,\n    borderRadius: 4,\n  },\n  pressBar: {\n    position: 'absolute',\n    // backgroundColor: 'rgba(0,0,0,0.5)',\n    left: 0,\n    top: 0,\n    height: progressContentHeight,\n    paddingTop: progressContentPadding,\n    paddingBottom: progressContentPadding,\n    width: '100%',\n    zIndex: 6,\n  },\n})\n\nexport default Progress\n"
  },
  {
    "path": "src/config/constant.ts",
    "content": "export const HEADER_HEIGHT = 42\nexport const LIST_ITEM_HEIGHT = 54\nexport const LIST_SCROLL_POSITION_KEY = '__LIST_SCROLL_POSITION_KEY__'\n\nexport const SPLIT_CHAR = {\n  DISLIKE_NAME: '@',\n  DISLIKE_NAME_ALIAS: '#',\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\n// export const COMPONENT_IDS = {\n//   home: 'home',\n//   playDetail: 'playDetail',\n// } as const\n// export type COMPONENT_IDS_TYPE = keyof typeof COMPONENT_IDS\nexport enum COMPONENT_IDS {\n  home = 'home',\n  playDetail = 'playDetail',\n  songlistDetail = 'songlistDetail',\n  comment = 'comment',\n}\n\nexport enum NAV_SHEAR_NATIVE_IDS {\n  playDetail_pic = 'playDetail_pic',\n  playDetail_header = 'playDetail_header',\n  // playDetail_pageIndicator = 'playDetail_pageIndicator',\n  playDetail_player = 'playDetail_player',\n  songlistDetail_pic = 'songlistDetail_pic',\n  songlistDetail_title = 'songlistDetail_title',\n}\n\n\nexport const storageDataPrefix = {\n  setting: '@setting_v1',\n  userList: '@user_list',\n  viewPrevState: '@view_prev_state',\n\n  list: '@list__',\n  listScrollPosition: '@list_scroll_position',\n  listPrevSelectId: '@list_prev_select_id',\n\n  lyric: '@lyric__',\n  musicUrl: '@music_url__',\n  musicOtherSource: '@music_other_source__',\n  playInfo: '@play_info',\n\n  syncAuthKey: '@sync_auth_key',\n  syncHost: '@sync_host',\n  syncHostHistory: '@sync_host_history',\n\n  openStoragePath: '@open_storage_path',\n  selectedManagedFolder: '@selected_managed_folder',\n  notificationTipEnable: '@notification_tip_enable',\n  ignoringBatteryOptimizationTipEnable: '@ignoring_battery_optimization_tip_enable',\n\n  searchHistoryList: '@search_history_list',\n  listUpdateInfo: '@list_update_info',\n  ignoreVersion: '@ignore_version',\n  ignoreVersionFailTipTimeKey: '@ignore_version_fail_tip_time',\n  leaderboardSetting: '@leaderboard_setting',\n  songListSetting: '@songist_setting',\n  searchSetting: '@search_setting',\n\n  fontSize: '@font_size',\n\n  theme: '@theme',\n\n  cheatTip: '@cheat_tip',\n  remoteLyricTip: '@remote_lyric_tip',\n\n  dislikeList: '@dislike_list',\n\n  userApi: '@user_api__',\n} as const\n\n// v0.x.x 版本的 data keys\nexport const storageDataPrefixOld = {\n  setting: '@setting',\n  list: '@list__',\n  listPosition: '@listposition__',\n  listSort: '@listsort__',\n  // lyric: '@lyric__',\n  // musicUrl: '@music_url__',\n  playInfo: '@play_info',\n  syncAuthKey: '@sync_auth_key',\n  syncHost: '@sync_host',\n  syncHostHistory: '@sync_host_history',\n  notificationTipEnable: '@notification_tip_enable',\n} as const\n\nexport const APP_PROVIDER_NAME = 'cn.toside.music.mobile.provider'\n\n\nexport const NAV_MENUS = [\n  { id: 'nav_search', icon: 'search-2' },\n  { id: 'nav_songlist', icon: 'album' },\n  { id: 'nav_top', icon: 'leaderboard' },\n  { id: 'nav_love', icon: 'love' },\n  // { id: 'download', icon: 'download-2' },\n  { id: 'nav_setting', icon: 'setting' },\n] as const\n\nexport type NAV_ID_Type = typeof NAV_MENUS[number]['id']\n\nexport const LXM_FILE_EXT_RXP = ['json', 'lxmc', 'bin']\nexport const USER_API_SOURCE_FILE_EXT_RXP = ['js']\n\nexport const MUSIC_TOGGLE_MODE = {\n  listLoop: 'listLoop', // 列表循环\n  random: 'random', // 列表随机\n  list: 'list', // 顺序播放\n  singleLoop: 'singleLoop', // 单曲循环\n  none: 'none', // 禁用\n} as const\n\nexport const MUSIC_TOGGLE_MODE_LIST = [\n  MUSIC_TOGGLE_MODE.listLoop,\n  MUSIC_TOGGLE_MODE.random,\n  MUSIC_TOGGLE_MODE.list,\n  MUSIC_TOGGLE_MODE.singleLoop,\n  MUSIC_TOGGLE_MODE.none,\n] as const\n\nexport const DEFAULT_SETTING = {\n  leaderboard: {\n    source: 'kw' as LX.OnlineSource,\n    boardId: 'kw__16',\n  },\n\n  songList: {\n    source: 'kw' as LX.OnlineSource,\n    sortId: 'new',\n    tagName: '',\n    tagId: '',\n  },\n\n  search: {\n    temp_source: 'kw' as LX.OnlineSource,\n    source: 'all' as LX.OnlineSource | 'all',\n    type: 'music' as 'music' | 'songlist',\n  },\n\n  viewPrevState: {\n    id: 'nav_search' as NAV_ID_Type,\n    // query: {},\n  },\n}\n"
  },
  {
    "path": "src/config/defaultSetting.ts",
    "content": "const defaultSetting: LX.AppSetting = {\n  version: '2.0',\n  'common.isAutoTheme': false,\n  'common.langId': null,\n  'common.apiSource': '',\n  'common.sourceNameType': 'alias',\n  'common.shareType': 'system',\n  'common.isAgreePact': false,\n  'common.autoHidePlayBar': true,\n  'common.drawerLayoutPosition': 'left',\n  'common.homePageScroll': true,\n  'common.allowProgressBarSeek': true,\n  'common.showBackBtn': false,\n  'common.showExitBtn': true,\n  'common.useSystemFileSelector': true,\n  'common.alwaysKeepStatusbarHeight': false,\n\n  'player.startupAutoPlay': false,\n  'player.startupPushPlayDetailScreen': false,\n  'player.togglePlayMethod': 'listLoop',\n  'player.playQuality': '128k',\n  'player.isSavePlayTime': false,\n  'player.volume': 1,\n  'player.playbackRate': 1,\n  'player.cacheSize': '1024',\n  'player.timeoutExit': '',\n  'player.timeoutExitPlayed': true,\n  'player.isAutoCleanPlayedList': false,\n  'player.isHandleAudioFocus': true,\n  'player.isEnableAudioOffload': true,\n  'player.isShowLyricTranslation': false,\n  'player.isShowLyricRoma': false,\n  'player.isShowNotificationImage': true,\n  'player.isS2t': false,\n  'player.isShowBluetoothLyric': false,\n\n  // 'playDetail.isZoomActiveLrc': false,\n  // 'playDetail.isShowLyricProgressSetting': false,\n  'playDetail.style.align': 'left',\n  'playDetail.vertical.style.lrcFontSize': 210,\n  'playDetail.horizontal.style.lrcFontSize': 220,\n  'playDetail.isShowLyricProgressSetting': false,\n\n  'desktopLyric.enable': false,\n  'desktopLyric.isLock': false,\n  'desktopLyric.width': 100,\n  'desktopLyric.maxLineNum': 5,\n  'desktopLyric.isSingleLine': false,\n  'desktopLyric.showToggleAnima': true,\n  'desktopLyric.position.x': 0,\n  'desktopLyric.position.y': 0,\n  'desktopLyric.textPosition.x': 'left',\n  'desktopLyric.textPosition.y': 'top',\n  'desktopLyric.style.fontSize': 180,\n  'desktopLyric.style.opacity': 100,\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.6)',\n\n  'search.isShowHotSearch': false,\n  'search.isShowHistorySearch': false,\n\n  'list.isClickPlayList': false,\n  'list.isShowSource': true,\n  'list.isShowAlbumName': false,\n  'list.isShowInterval': true,\n  'list.isSaveScrollLocation': true,\n  'list.addMusicLocationType': 'top',\n\n  'download.fileName': '歌名 - 歌手',\n\n  'sync.enable': false,\n\n  // 'theme.id': 'blue_plus',\n  'theme.id': 'green',\n  'theme.lightId': 'green',\n  'theme.darkId': 'black',\n  'theme.hideBgDark': false,\n  'theme.dynamicBg': false,\n  'theme.fontShadow': false,\n}\n\n\n// 使用新年皮肤\nif (new Date().getMonth() < 2) {\n  defaultSetting['theme.id'] = 'happy_new_year'\n  defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 18, 34, 1)'\n}\n\nexport default defaultSetting\n"
  },
  {
    "path": "src/config/globalData.ts",
    "content": "import { version } from '../../package.json'\nimport { createAppEventHub } from '@/event/appEvent'\nimport { createListEventHub } from '@/event/listEvent'\nimport { createDislikeEventHub } from '@/event/dislikeEvent'\nimport { createStateEventHub } from '@/event/stateEvent'\nif (process.versions == null) {\n  // @ts-expect-error\n  process.versions = {\n    app: version,\n  }\n} else process.versions.app = version\n\n// global.i18n = null\n\n// let screenW = Dimensions.get('window').width\n// let screenH = Dimensions.get('window').height\n// if (screenW > screenH) {\n//   const temp = screenW\n//   screenW = screenH\n//   screenH = temp\n// }\n\n\nglobal.lx = {\n  fontSize: 1,\n  playerStatus: {\n    isInitialized: false,\n    isRegisteredService: false,\n    isIniting: false,\n  },\n\n  restorePlayInfo: null,\n  // allList: null,\n  // globalObj: null,\n  // listScrollPosition: {},\n  // listSort: {},\n\n  isScreenKeepAwake: false,\n\n  // 是否播放完后退出应用\n  isPlayedStop: false,\n\n  // prevListPlayIndex: -1,\n\n  // syncKeyInfo: {},\n\n  isEnableSyncLog: false,\n  isEnableUserApiLog: false,\n\n  playerTrackId: '',\n\n  gettingUrlId: '',\n\n  qualityList: {},\n  apis: {},\n  apiInitPromise: [Promise.resolve(false), true, () => {}],\n\n  jumpMyListPosition: false,\n\n  settingActiveId: 'basic',\n\n  homePagerIdle: true,\n\n  // syncKeyInfo: initValue as LX.Sync.KeyInfo,\n\n  // windowInfo: {\n  //   screenW,\n  //   screenH,\n  //   fontScale: PixelRatio.getFontScale(),\n  //   pixelRatio: PixelRatio.get(),\n  //   screenPxW: PixelRatio.getPixelSizeForLayoutSize(screenW),\n  //   screenPxH: PixelRatio.getPixelSizeForLayoutSize(screenH),\n  // },\n}\n\nglobal.app_event = createAppEventHub()\nglobal.list_event = createListEventHub()\nglobal.dislike_event = createDislikeEventHub()\nglobal.state_event = createStateEventHub()\n"
  },
  {
    "path": "src/config/index.js",
    "content": "// import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'\nimport defaultUrl from '@/resources/medias/Silence02s.mp3'\nimport notificationIcon from '@/resources/images/notification.xhdpi.png'\n// const defaultUrl = resolveAssetSource(resourceDefaultUrl).uri\n\nexport {\n  defaultUrl,\n  notificationIcon,\n}\n// export const defaultUrl = require('@/resources/medias/Silence02s.mp3')\n\n"
  },
  {
    "path": "src/config/migrate.ts",
    "content": "import { filterMusicList, toNewMusicInfo } from '@/utils'\nimport { LIST_IDS, storageDataPrefix, storageDataPrefixOld } from '@/config/constant'\nimport { getAllKeys, getData, getDataMultiple, removeData, saveData } from '@/plugins/storage'\nimport { allMusicList, listDataOverwrite, userLists } from '@/utils/listManage'\nimport { saveListMusics, saveUserList } from '@/utils/data'\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/* export const migrateListData = 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 */\nexport const getAllListData = async(): Promise<{\n  defaultList?: { list: any[] }\n  loveList?: { list: any[] }\n  tempList?: { list: any[] }\n  userList?: OldUserListInfo[]\n}> => {\n  const defaultListKey = storageDataPrefixOld.list + 'default'\n  const loveListKey = storageDataPrefixOld.list + 'love'\n  let defaultList\n  let loveList\n  let userList = []\n  let keys = await getAllKeys()\n  const listKeys: string[] = []\n  for (const key of keys) {\n    if (key.startsWith(storageDataPrefixOld.list)) {\n      listKeys.push(key)\n    }\n  }\n  const listData = await getDataMultiple(listKeys) as Array<[string, any]>\n  for (const [key, value] of listData) {\n    switch (key) {\n      case defaultListKey:\n        defaultList = value\n        break\n      case loveListKey:\n        loveList = value\n        break\n      default:\n        userList.push(value)\n        break\n    }\n  }\n\n  const listSort: Record<string, number> = await getData(storageDataPrefixOld.listSort) ?? {}\n\n  userList.sort((a, b) => {\n    if (listSort[a.id] == null) return listSort[b.id] == null ? -1 : 1\n    return listSort[b.id] == null ? 1 : listSort[a.id] - listSort[b.id]\n  })\n  userList.forEach((list, index) => {\n    if (listSort[list.id] == null) {\n      listSort[list.id] = index\n      delete list.location\n    }\n  })\n\n  return {\n    defaultList,\n    loveList,\n    userList,\n  }\n}\n\n/**\n * 迁移 v1.0.0 之前的 list data\n * @returns\n */\nexport const migrateListData = async() => {\n  const playList = await getAllListData()\n  let listDataAll: LX.List.ListDataFull = {\n    defaultList: [],\n    loveList: [],\n    userList: [],\n    tempList: [],\n  }\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.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  listDataOverwrite(listDataAll)\n  await saveUserList(userLists)\n  const allListIds = [LIST_IDS.DEFAULT, LIST_IDS.LOVE, ...userLists.map(l => l.id)]\n  await saveListMusics([...allListIds.map(id => ({ id, musics: allMusicList.get(id) as LX.List.ListMusics }))])\n  await removeData(storageDataPrefixOld.listSort)\n\n  const listPosition = await getData(storageDataPrefixOld.listPosition)\n  if (listPosition != null) {\n    await saveData(storageDataPrefix.listScrollPosition, listPosition)\n    await removeData(storageDataPrefixOld.listPosition)\n  }\n}\n\nconst timeStr2Intv = (timeStr: string) => {\n  let intvArr = timeStr.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}\nconst migratePlayInfo = async() => {\n  const playInfo = await getData<any>(storageDataPrefixOld.playInfo)\n  if (playInfo == null) return\n  if (playInfo.list !== undefined) delete playInfo.list\n  if (playInfo.maxTime) playInfo.maxTime = timeStr2Intv(playInfo.maxTime as string)\n  await saveData(storageDataPrefix.playInfo, playInfo)\n}\n/**\n * 迁移 v1.0.0 之前的 meta 数据\n * @returns\n */\nexport const migrateMetaData = async() => {\n  await migratePlayInfo()\n  // const [playInfo] = await getDataMultiple([\n  //   storageDataPrefixOld.listPosition,\n  //   storageDataPrefixOld.playInfo,\n  // ])\n\n  // await saveDataMultiple([\n  //   // [storageDataPrefix.listScrollPosition, listPosition[1]],\n  //   [storageDataPrefix.playInfo, migratePlayInfo(playInfo[1])],\n  // ])\n  // await removeDataMultiple([\n  //   storageDataPrefix.listScrollPosition,\n  // ])\n}\n\n"
  },
  {
    "path": "src/config/migrateSetting.ts",
    "content": "import { compareVer } from '@/utils'\n\nexport default (setting: any): Partial<LX.AppSetting> => {\n  setting = { ...setting }\n\n  // 迁移 v1 之前的配置\n  if (compareVer(setting.version as string, '2.0') < 0) {\n    setting['player.startupAutoPlay'] = setting.startupAutoPlay\n    setting['player.togglePlayMethod'] = setting.player?.togglePlayMethod\n    setting['player.isSavePlayTime'] = setting.player?.isSavePlayTime\n    setting['player.cacheSize'] = setting.player?.cacheSize\n    setting['player.timeoutExit'] = setting.player?.timeoutExit\n    setting['player.timeoutExitPlayed'] = setting.player?.timeoutExitPlayed\n    setting['player.isHandleAudioFocus'] = setting.player?.isHandleAudioFocus\n    setting['player.isShowLyricTranslation'] = setting.player?.isShowLyricTranslation\n    setting['player.isShowLyricRoma'] = setting.player?.isShowLyricRoma\n    setting['player.isShowNotificationImage'] = setting.player?.isShowNotificationImage\n    setting['player.isS2t'] = setting.player?.isS2t\n    setting['playDetail.portrait.style.lrcFontSize'] = setting.player?.portrait?.style?.lrcFontSize\n    setting['playDetail.landscape.style.lrcFontSize'] = setting.player?.landscape?.style?.lrcFontSize\n    setting['desktopLyric.enable'] = setting.desktopLyric?.enable\n    setting['desktopLyric.isLock'] = setting.desktopLyric?.isLock\n    setting['desktopLyric.width'] = setting.desktopLyric?.width\n    setting['desktopLyric.maxLineNum'] = setting.desktopLyric?.maxLineNum\n    setting['desktopLyric.isSingleLine'] = setting.desktopLyric?.isSingleLine\n    setting['desktopLyric.showToggleAnima'] = setting.desktopLyric?.showToggleAnima\n    setting['desktopLyric.position.x'] = setting.desktopLyric?.position?.x\n    setting['desktopLyric.position.y'] = setting.desktopLyric?.position?.y\n    setting['desktopLyric.textPosition.x'] = setting.desktopLyric?.textPosition?.x\n    setting['desktopLyric.textPosition.y'] = setting.desktopLyric?.textPosition?.y\n    setting['desktopLyric.style.fontSize'] = setting.desktopLyric?.style?.fontSize\n    setting['desktopLyric.style.opacity'] = setting.desktopLyric?.style?.opacity\n    setting['list.isClickPlayList'] = setting.list?.isClickPlayList\n    setting['list.isShowSource'] = setting.list?.isShowSource\n    setting['list.isSaveScrollLocation'] = setting.list?.isSaveScrollLocation\n    setting['list.addMusicLocationType'] = setting.list?.addMusicLocationType\n    setting['common.themeId'] = setting.themeId\n    setting['common.isAutoTheme'] = setting.isAutoTheme\n    setting['common.langId'] = setting.langId\n    setting['common.apiSource'] = setting.apiSource\n    setting['common.sourceNameType'] = setting.sourceNameType\n    setting['common.shareType'] = setting.shareType\n    setting['common.isAgreePact'] = setting.isAgreePact\n    setting['sync.enable'] = setting.sync?.enable\n    setting['theme.id'] = setting.themeId\n  }\n\n  return setting\n}\n"
  },
  {
    "path": "src/config/setting.ts",
    "content": "import { storageDataPrefix, storageDataPrefixOld } from '@/config/constant'\nimport defaultSetting from '@/config/defaultSetting'\nimport { getData, removeData, saveData } from '@/plugins/storage'\nimport migrateSetting from './migrateSetting'\nimport settingState from '@/store/setting/state'\nimport { migrateMetaData, migrateListData } from './migrate'\nimport { exitApp, tipDialog } from '@/utils/tools'\n\n// 业务相关工具方法\n\n\nconst primitiveType = ['string', 'boolean', 'number']\nconst checkPrimitiveType = (val: any): boolean => val === null || primitiveType.includes(typeof val)\n\nconst 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}\nexport const updateSetting = (setting?: Partial<LX.AppSetting> | null, isInit: boolean = false) => {\n  let originSetting: LX.AppSetting\n  if (isInit) {\n    originSetting = { ...defaultSetting }\n  } else originSetting = settingState.setting\n\n  const result = mergeSetting(originSetting, setting)\n\n  result.setting.version = defaultSetting.version\n\n  return result\n}\n\nexport const initSetting = async() => {\n  let setting: Partial<LX.AppSetting> | null = await getData(storageDataPrefix.setting)\n\n\n  // try migrate setting before v1\n  if (!setting) {\n    const config = await getData<{ setting?: any }>(storageDataPrefixOld.setting)\n    if (config != null) {\n      setting = migrateSetting(config)\n      try {\n        await migrateListData()\n        await migrateMetaData()\n      } catch (err: any) {\n        void tipDialog({\n          title: '数据迁移失败 (Failed to migrate data)',\n          message: `请截图并在 GitHub 反馈。为了防止数据丢失，应用将停止运行。错误信息：\\n${(err.stack ?? err.message) as string}`,\n          btnText: 'Exit',\n          bgClose: false,\n        }).then(() => {\n          exitApp()\n        })\n        throw err\n      }\n      await removeData(storageDataPrefixOld.setting)\n    }\n  }\n\n  // console.log(setting)\n  const updatedSetting = updateSetting(setting, true)\n  void saveData(storageDataPrefix.setting, updatedSetting.setting)\n\n  return updatedSetting\n}\n"
  },
  {
    "path": "src/core/apiSource.ts",
    "content": "// import { setUserApi as setUserApiAction } from '@renderer/utils/ipc'\nimport musicSdk from '@/utils/musicSdk'\n// import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'\nimport { updateSetting } from './common'\nimport settingState from '@/store/setting/state'\nimport { destroyUserApi, setUserApi } from './userApi'\nimport apiSourceInfo from '@/utils/musicSdk/api-source-info'\n\n\nexport const setApiSource = (apiId: string) => {\n  if (global.lx.apiInitPromise[1]) {\n    global.lx.apiInitPromise[0] = new Promise(resolve => {\n      global.lx.apiInitPromise[1] = false\n      global.lx.apiInitPromise[2] = (result: boolean) => {\n        global.lx.apiInitPromise[1] = true\n        resolve(result)\n      }\n    })\n  }\n  if (/^user_api/.test(apiId)) {\n    setUserApi(apiId).catch(err => {\n      if (!global.lx.apiInitPromise[1]) global.lx.apiInitPromise[2](false)\n      console.log(err)\n      let api = apiSourceInfo.find(api => !api.disabled)\n      if (!api) return\n      if (api.id != settingState.setting['common.apiSource']) setApiSource(api.id)\n    })\n  } else {\n    // @ts-expect-error\n    global.lx.qualityList = musicSdk.supportQuality[apiId] ?? {}\n    destroyUserApi()\n    if (!global.lx.apiInitPromise[1]) global.lx.apiInitPromise[2](true)\n    // apiSource.value = apiId\n    // void setUserApiAction(apiId)\n  }\n\n  if (apiId != settingState.setting['common.apiSource']) {\n    updateSetting({ 'common.apiSource': apiId })\n    requestAnimationFrame(() => {\n      global.state_event.apiSourceUpdated(apiId)\n    })\n  }\n}\n\n"
  },
  {
    "path": "src/core/common.ts",
    "content": "import { hideDesktopLyric } from './desktopLyric'\nimport { exitApp as utilExitApp } from '@/utils/nativeModules/utils'\nimport { destroy as destroyPlayer } from '@/plugins/player/utils'\nimport { initSetting as initAppSetting } from '@/config/setting'\nimport { setLanguage as applyLanguage } from '@/lang/i18n'\n\nimport settingActions from '@/store/setting/action'\nimport settingState from '@/store/setting/state'\nimport commonActions from '@/store/common/action'\nimport commonState, { type InitState as CommonStateType } from '@/store/common/state'\n\nimport { storageDataPrefix } from '@/config/constant'\nimport { saveData } from '@/plugins/storage'\nimport { throttle } from '@/utils/common'\nimport { getSelectedManagedFolder, saveFontSize, saveViewPrevState, setSelectedManagedFolder } from '@/utils/data'\nimport { showPactModal as handleShowPactModal } from '@/navigation'\nimport { hideDesktopLyricView } from '@/utils/nativeModules/lyricDesktop'\nimport { getPersistedUriList, selectManagedFolder } from '@/utils/fs'\n\n\nconst throttleSaveSetting = throttle(() => {\n  void saveData(storageDataPrefix.setting, settingState.setting)\n})\n\n/**\n * 初始化设置\n */\nexport const initSetting = async() => {\n  const setting = (await initAppSetting()).setting\n  settingActions.updateSetting(setting)\n  return setting\n}\n\n/**\n * 更新设置\n * @param setting 新设置\n */\nexport const updateSetting = (setting: Partial<LX.AppSetting>) => {\n  settingActions.updateSetting(setting)\n  throttleSaveSetting()\n}\n\nexport const setLanguage = (locale: Parameters<typeof applyLanguage>[0]) => {\n  updateSetting({ 'common.langId': locale })\n  global.state_event.languageChanged(locale)\n  requestAnimationFrame(() => {\n    applyLanguage(locale)\n  })\n}\n\n\nlet isDestroying = false\nexport const exitApp = (reason: string) => {\n  console.log('Handle Exit App, Reason: ' + reason)\n  if (isDestroying) return\n  isDestroying = true\n  void Promise.all([\n    hideDesktopLyric(),\n    destroyPlayer(),\n    hideDesktopLyricView(),\n  ]).finally(() => {\n    isDestroying = false\n    utilExitApp()\n  })\n}\n\nexport const setFontSize = (size: number) => {\n  commonActions.setFontSize(size)\n  void saveFontSize(size)\n}\n\nexport const setStatusbarHeight = (size: number) => {\n  commonActions.setStatusbarHeight(size)\n}\n\nexport const setComponentId = (name: keyof CommonStateType['componentIds'], id: string) => {\n  commonActions.setComponentId(name, id)\n}\nexport const removeComponentId = (name: string) => {\n  commonActions.removeComponentId(name)\n}\n\nexport const setNavActiveId = (id: Parameters<typeof commonActions.setNavActiveId>['0']) => {\n  if (id == commonState.navActiveId) return\n  commonActions.setNavActiveId(id)\n  if (id != 'nav_setting') {\n    commonActions.setLastNavActiveId(id)\n    saveViewPrevState({ id })\n  }\n}\n\nexport const showPactModal = () => {\n  handleShowPactModal()\n}\n\nexport const checkStoragePermissions = async() => {\n  const selectedManagedFolder = await getSelectedManagedFolder()\n  if (selectedManagedFolder) return (await getPersistedUriList()).some(uri => selectedManagedFolder.startsWith(uri))\n  return false\n}\n\nexport const requestStoragePermission = async() => {\n  const isGranted = await checkStoragePermissions()\n  if (isGranted) return isGranted\n\n  const uri = await selectManagedFolder()\n  if (!uri.isDirectory) return false\n  await setSelectedManagedFolder(uri.path)\n  return true\n}\n\nexport const setBgPic = (pic: string | null) => {\n  commonActions.setBgPic(pic)\n}\n"
  },
  {
    "path": "src/core/desktopLyric.ts",
    "content": "import {\n  hideDesktopLyricView,\n  showDesktopLyricView,\n  setSendLyricTextEvent,\n  setLyric,\n  play,\n  pause,\n  setPlaybackRate,\n  toggleTranslation,\n  toggleRoma,\n  toggleLock,\n  setColor,\n  setAlpha,\n  setTextSize,\n  setShowToggleAnima,\n  setSingleLine,\n  setPosition,\n  setMaxLineNum,\n  setWidth,\n  setLyricTextPosition,\n  checkOverlayPermission,\n  openOverlayPermissionActivity,\n  onPositionChange,\n} from '@/utils/nativeModules/lyricDesktop'\nimport settingState from '@/store/setting/state'\nimport playerState from '@/store/player/state'\nimport { tranditionalize } from '@/utils/simplify-chinese-main'\nimport { getPosition } from '@/plugins/player'\nexport {\n  onLyricLinePlay,\n} from '@/utils/nativeModules/lyricDesktop'\n\nexport const showDesktopLyric = async() => {\n  const setting = settingState.setting\n  await showDesktopLyricView({\n    isShowToggleAnima: setting['desktopLyric.showToggleAnima'],\n    isSingleLine: setting['desktopLyric.isSingleLine'],\n    isLock: setting['desktopLyric.isLock'],\n    unplayColor: setting['desktopLyric.style.lyricUnplayColor'],\n    playedColor: setting['desktopLyric.style.lyricPlayedColor'],\n    shadowColor: setting['desktopLyric.style.lyricShadowColor'],\n    opacity: setting['desktopLyric.style.opacity'],\n    textSize: setting['desktopLyric.style.fontSize'],\n    width: setting['desktopLyric.width'],\n    maxLineNum: setting['desktopLyric.maxLineNum'],\n    positionX: setting['desktopLyric.position.x'],\n    positionY: setting['desktopLyric.position.y'],\n    textPositionX: setting['desktopLyric.textPosition.x'],\n    textPositionY: setting['desktopLyric.textPosition.y'],\n  })\n  let lrc = playerState.musicInfo.lrc ?? ''\n  let tlrc = playerState.musicInfo.tlrc ?? ''\n  let rlrc = playerState.musicInfo.rlrc ?? ''\n  if (setting['player.isS2t']) {\n    lrc = tranditionalize(lrc)\n    tlrc = tranditionalize(tlrc)\n  }\n  await setLyric(lrc, tlrc, rlrc)\n  if (playerState.isPlay && !global.lx.gettingUrlId) {\n    void getPosition().then(position => {\n      void play(position * 1000)\n    })\n  }\n}\n\nexport const hideDesktopLyric = async() => {\n  return hideDesktopLyricView()\n}\n\nexport const playDesktopLyric = play\nexport const pauseDesktopLyric = pause\nexport const setDesktopLyric = setLyric\nexport const setDesktopLyricPlaybackRate = setPlaybackRate\nexport const toggleDesktopLyricTranslation = toggleTranslation\nexport const toggleDesktopLyricRoma = toggleRoma\nexport const toggleDesktopLyricLock = toggleLock\nexport const setDesktopLyricColor = async(unplayColor: string | null, playedColor: string | null, shadowColor: string | null) => {\n  return setColor(unplayColor ?? settingState.setting['desktopLyric.style.lyricUnplayColor'],\n    playedColor ?? settingState.setting['desktopLyric.style.lyricPlayedColor'],\n    shadowColor ?? settingState.setting['desktopLyric.style.lyricShadowColor'],\n  )\n}\nexport const setDesktopLyricAlpha = setAlpha\nexport const setDesktopLyricTextSize = setTextSize\nexport const setShowDesktopLyricToggleAnima = setShowToggleAnima\nexport const setDesktopLyricSingleLine = setSingleLine\nexport const setDesktopLyricPosition = setPosition\nexport const setDesktopLyricMaxLineNum = setMaxLineNum\nexport const setDesktopLyricWidth = setWidth\nexport const setDesktopLyricTextPosition = async(x: LX.AppSetting['desktopLyric.textPosition.x'] | null, y: LX.AppSetting['desktopLyric.textPosition.y'] | null) => {\n  return setLyricTextPosition(x ?? settingState.setting['desktopLyric.textPosition.x'], y ?? settingState.setting['desktopLyric.textPosition.y'])\n}\nexport const checkDesktopLyricOverlayPermission = checkOverlayPermission\nexport const openDesktopLyricOverlayPermissionActivity = openOverlayPermissionActivity\nexport const onDesktopLyricPositionChange = onPositionChange\n\n\nexport const showRemoteLyric = async(isSend: boolean) => {\n  await setSendLyricTextEvent(isSend)\n  if (isSend) {\n    let lrc = playerState.musicInfo.lrc ?? ''\n    let tlrc = playerState.musicInfo.tlrc ?? ''\n    let rlrc = playerState.musicInfo.rlrc ?? ''\n    if (settingState.setting['player.isS2t']) {\n      lrc = tranditionalize(lrc)\n      tlrc = tranditionalize(tlrc)\n    }\n    await setLyric(lrc, tlrc, rlrc)\n    if (playerState.isPlay && !global.lx.gettingUrlId) {\n      void getPosition().then(position => {\n        void play(position * 1000)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "src/core/dislikeList.ts",
    "content": "import { action } from '@/store/dislikeList'\n\n\nexport const addDislikeInfo = async(infos: LX.Dislike.DislikeMusicInfo[]) => {\n  await global.dislike_event.dislike_music_add(infos)\n}\n\nexport const overwirteDislikeInfo = async(rules: string) => {\n  await global.dislike_event.dislike_data_overwrite(rules)\n}\n\nexport const clearDislikeInfo = async() => {\n  await global.dislike_event.dislike_music_clear()\n}\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\n\nexport const setDislikeInfo = (info: LX.Dislike.DislikeInfo) => {\n  action.setDislikeInfo(info)\n}\n\nexport { getDislikeInfo } from '@/utils/dislikeManage'\n"
  },
  {
    "path": "src/core/hotSearch.ts",
    "content": "import hotSearchState, { type Source } from '@/store/hotSearch/state'\nimport hotSearchActions, { type Lists } from '@/store/hotSearch/action'\nimport musicSdk from '@/utils/musicSdk'\n\nexport const getList = async(source: Source): Promise<string[]> => {\n  if (source == 'all') {\n    let task = []\n    for (const source of hotSearchState.sources) {\n      if (source == 'all') continue\n      task.push(\n        hotSearchState.sourceList[source]?.length\n          ? Promise.resolve({ source, list: hotSearchState.sourceList[source] })\n          : ((musicSdk[source]?.hotSearch.getList() as Promise<Lists[number]>) ?? 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: Lists) => {\n      return hotSearchActions.setList(source, results)\n    })\n  } else {\n    if (hotSearchState.sourceList[source]?.length) return hotSearchState.sourceList[source]\n    if (!musicSdk[source]?.hotSearch) {\n      hotSearchActions.setList(source, [])\n      return []\n    }\n    return musicSdk[source]?.hotSearch.getList().catch((err: any) => {\n      console.log(err)\n      return { source, list: [] }\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    }).then(data => hotSearchActions.setList(source, data.list))\n  }\n}\n\n\n"
  },
  {
    "path": "src/core/init/common.ts",
    "content": "// import musicSdk from '@/utils/musicSdk'\n// import commonActions from '@/store/common/action'\nimport playerState from '@/store/player/state'\nimport { prefetch } from '@/components/common/ImageBackground'\nimport { setBgPic } from '@/core/common'\n\n// const handleUpdateSourceNmaes = () => {\n//   const prefix = settingState.setting['common.sourceNameType'] == 'real' ? 'source_' : 'source_alias_'\n//   const sourceNames: Record<LX.OnlineSource | 'all', string> = {\n//     kw: 'kw',\n//     tx: 'tx',\n//     kg: 'kg',\n//     mg: 'mg',\n//     wy: 'wy',\n//     // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n//     all: global.i18n.t(prefix + 'all' as any),\n//   }\n//   for (const { id } of musicSdk.sources) {\n//     // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n//     sourceNames[id as LX.OnlineSource] = global.i18n.t(prefix + id as any)\n//   }\n//   commonActions.setSourceNames(sourceNames)\n// }\nconst formatUri = <T extends string | null>(url: T) => {\n  return (typeof url == 'string' && url.startsWith('/')) ? `file://${url}` : url\n}\n\nexport default async(setting: LX.AppSetting) => {\n  // const handleConfigUpdated = (keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) => {\n  //   // if (keys.includes('common.sourceNameType')) handleUpdateSourceNmaes()\n  //   handleConfigUpdate(keys, setting)\n  // }\n\n  let pic = playerState.musicInfo.pic\n  let isDynamicBg = setting['theme.dynamicBg']\n  const handleUpdatePic = (pic: string) => {\n    if (!pic) return\n    const picUrl = formatUri(pic)\n    void prefetch(picUrl).then(() => {\n      if (pic != playerState.musicInfo.pic || !isDynamicBg) return\n      setBgPic(picUrl)\n    })\n  }\n  const handlePicUpdate = () => {\n    if (playerState.musicInfo.pic && playerState.musicInfo.pic != playerState.loadErrorPicUrl) {\n      // if (playerState.musicInfo.pic != playerState.loadErrorPicUrl) {\n      pic = playerState.musicInfo.pic\n      if (!isDynamicBg) return\n      handleUpdatePic(pic)\n      // .catch(() => {\n      //   if (pic != playerState.musicInfo.pic) return\n      //   setBgPic(null)\n      // })\n    }\n    // } else {\n    //   if (!isDynamicBg) return\n    //   setPic(null)\n    // }\n  }\n  const handleConfigUpdate = (keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) => {\n    if (!keys.includes('theme.dynamicBg')) return\n    isDynamicBg = setting['theme.dynamicBg']!\n    if (isDynamicBg) {\n      if (pic) handleUpdatePic(pic)\n    } else setBgPic(null)\n  }\n  handlePicUpdate()\n  global.state_event.on('playerMusicInfoChanged', handlePicUpdate)\n  global.state_event.on('configUpdated', handleConfigUpdate)\n}\n"
  },
  {
    "path": "src/core/init/dataInit.ts",
    "content": "// import { getPlayInfo } from '@/utils/data'\n// import { log } from '@/utils/log'\nimport { init as musicSdkInit } from '@/utils/musicSdk'\nimport { getUserLists, setUserList } from '@/core/list'\nimport { setNavActiveId } from '../common'\nimport { getViewPrevState } from '@/utils/data'\nimport { bootLog } from '@/utils/bootLog'\nimport { getDislikeInfo, setDislikeInfo } from '@/core/dislikeList'\nimport { unlink } from '@/utils/fs'\nimport { TEMP_FILE_PATH } from '@/utils/tools'\n// import { play, playList } from '../player/player'\n\n// const initPrevPlayInfo = async(appSetting: LX.AppSetting) => {\n//   const info = await getPlayInfo()\n//   global.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//   global.lx.restorePlayInfo = info\n//   await playList(info.listId, info.index)\n\n//   if (appSetting['player.startupAutoPlay']) setTimeout(play)\n// }\n\nexport default async(appSetting: LX.AppSetting) => {\n  // await Promise.all([\n  //   initUserApi(), // 自定义API\n  // ]).catch(err => log.error(err))\n  void musicSdkInit() // 初始化音乐sdk\n  bootLog('User list init...')\n  setUserList(await getUserLists()) // 获取用户列表\n  setDislikeInfo(await getDislikeInfo()) // 获取不喜欢列表\n  bootLog('User list inited.')\n  setNavActiveId((await getViewPrevState()).id)\n  void unlink(TEMP_FILE_PATH)\n  // await initPrevPlayInfo(appSetting).catch(err => log.error(err)) // 初始化上次的歌曲播放信息\n}\n"
  },
  {
    "path": "src/core/init/deeplink/fileAction.ts",
    "content": "import { readMetadata } from '@/utils/localMediaMetadata'\nimport { handleImportList } from '@/screens/Home/Views/Setting/settings/Backup/actions'\nimport { handleImportLocalFile } from '@/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/action'\nimport { type FileType } from '@/utils/fs'\nimport { confirmDialog } from '@/utils/tools'\nimport playerState from '@/store/player/state'\nimport { addTempPlayList } from '@/core/player/tempPlayList'\nimport { LIST_IDS } from '@/config/constant'\nimport { playNext } from '@/core/player/player'\nimport { buildLocalMusicInfo, buildLocalMusicInfoByFilePath } from '@/screens/Home/Views/Mylist/MyList/listAction'\n\n\nexport const handleFileLXMCAction = async(file: FileType) => {\n  if (!(await confirmDialog({\n    message: global.i18n.t('deep_link_file_lxmc_confirm_tip', { name: file.name }),\n  }))) return\n\n  handleImportList(file.path)\n}\n\nexport const handleFileMusicAction = async(file: FileType) => {\n  const info = await readMetadata(file.path)\n  const isPlaying = !!playerState.playMusicInfo.musicInfo\n  const musicInfo = info ? buildLocalMusicInfo(file.path, info) : buildLocalMusicInfoByFilePath(file)\n  console.log(musicInfo)\n  addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n  if (isPlaying) void playNext()\n}\n\nexport const handleFileJSAction = async(file: FileType) => {\n  if (!(await confirmDialog({\n    message: global.i18n.t('deep_link_file_js_confirm_tip', { name: file.name }),\n  }))) return\n\n  handleImportLocalFile(file.path)\n}\n"
  },
  {
    "path": "src/core/init/deeplink/index.ts",
    "content": "import { Linking } from 'react-native'\nimport { errorDialog } from './utils'\nimport { handleMusicAction } from './musicAction'\nimport { handlePlayerAction, type PlayerAction } from './playerAction'\nimport { handleSonglistAction } from './songlistAction'\nimport { extname, stat } from '@/utils/fs'\nimport { handleFileMusicAction, handleFileJSAction, handleFileLXMCAction } from './fileAction'\n\n\nconst 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?: string\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 PlayerAction)\n      break\n    // default: throw new Error('Unknown type: ' + type)\n  }\n}\n\nconst handleFileAction = async(link: string) => {\n  const file = await stat(link)\n  // console.log(file)\n  switch (extname(file.name)) {\n    case 'json':\n    case 'lxmc':\n      await handleFileLXMCAction(file)\n      break\n    case 'js':\n      await handleFileJSAction(file)\n      break\n    case 'ogg':\n    case 'flac':\n    case 'wav':\n    case 'mp3':\n      await handleFileMusicAction(file)\n      break\n    default:\n      if (!file.mimeType?.startsWith('audio/')) throw new Error('Unknown file type')\n      await handleFileMusicAction(file)\n      break\n  }\n}\n\n// const handleHttpAction = async(link: string) => {\n// }\n\n\nconst runLinkAction = async(link: string) => {\n  if (link.startsWith('lxmusic://')) {\n    try {\n      await handleLinkAction(link)\n    } catch (err: any) {\n      errorDialog(err.message)\n      // focusWindow()\n    }\n  } else if (link.startsWith('file://') || link.startsWith('content://')) {\n    try {\n      await handleFileAction(link)\n    } catch (err: any) {\n      errorDialog(err.message)\n      // focusWindow()\n    }\n  }\n  //  else if (/^https?:\\/\\//.test(link)) {\n  //   try {\n  //     await handleHttpAction(link)\n  //   } catch (err: any) {\n  //     errorDialog(err.message)\n  //     // focusWindow()\n  //   }\n  // }\n}\n\nexport const initDeeplink = async() => {\n  Linking.addEventListener('url', ({ url }) => {\n    void runLinkAction(url)\n    console.log('deeplink', url)\n  })\n  const initialUrl = await Linking.getInitialURL()\n  if (initialUrl == null) return\n  console.log('deeplink', initialUrl)\n  void runLinkAction(initialUrl)\n}\n"
  },
  {
    "path": "src/core/init/deeplink/musicAction.js",
    "content": "import { decodeName, toNewMusicInfo } from '@/utils'\nimport { dataVerify, qualityFilter } from './utils'\nimport playerState from '@/store/player/state'\nimport { LIST_IDS } from '@/config/constant'\nimport { playNext } from '@/core/player/player'\nimport { getOtherSource } from '@/core/music/utils'\nimport { addTempPlayList } from '@/core/player/tempPlayList'\n\n// const handleSearchMusic = ({ 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// }\n\nconst 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: 64 },\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: 64 },\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: 64 },\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: 64 },\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: 64 },\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}\nconst handlePlayMusic = ({ 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  const isPlaying = !!playerState.playMusicInfo.musicInfo\n  addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n  if (isPlaying) playNext()\n}\n\n\nconst 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: 64 },\n    { key: 'interval', types: ['string'], max: 64 },\n    { key: 'playLater', types: ['boolean'] },\n  ], info)\n}\n\nconst 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}\nconst handleSearchPlayMusic = 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    const isPlaying = !!playerState.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\nexport const handleMusicAction = 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"
  },
  {
    "path": "src/core/init/deeplink/playSonglist.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\nimport { setTempList } from '@/core/list'\nimport { playList } from '@/core/player/player'\nimport { getListDetail, getListDetailAll } from '@/core/songlist'\nimport listState from '@/store/list/state'\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\nconst 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    await playList(LIST_IDS.TEMP, getListPlayIndex(list, playIndex))\n  }\n  list = await getListDetailAll(source, id)\n  if (isPlayingList) {\n    if (listState.tempListMeta.id == id) await setTempList(playListId, list)\n  } else {\n    await setTempList(playListId, list)\n    await playList(LIST_IDS.TEMP, getListPlayIndex(list, playIndex))\n  }\n}\nexport const playSonglist = 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"
  },
  {
    "path": "src/core/init/deeplink/playerAction.ts",
    "content": "import { collectMusic, dislikeMusic, pause, play, playNext, playPrev, togglePlay, uncollectMusic } from '@/core/player/player'\n\nexport type PlayerAction = 'play' | 'pause' | 'skipNext' | 'skipPrev' | 'togglePlay' | 'collect' | 'uncollect' | 'dislike'\n\nexport const handlePlayerAction = async(action: PlayerAction) => {\n  switch (action) {\n    case 'play':\n      play()\n      break\n    case 'pause':\n      void pause()\n      break\n    case 'skipNext':\n      void playNext()\n      break\n    case 'skipPrev':\n      void 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      void dislikeMusic()\n      break\n    // default: throw new Error('Unknown action: ' + (action as any ?? ''))\n  }\n}\n"
  },
  {
    "path": "src/core/init/deeplink/songlistAction.js",
    "content": "import { playSonglist } from './playSonglist'\nimport { dataVerify, sourceVerify } from './utils'\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// const openSonglist = () => {\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// }\n\nconst handlePlaySonglist = 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 playSonglist(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index)\n}\n\nexport const handleSonglistAction = 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"
  },
  {
    "path": "src/core/init/deeplink/utils.js",
    "content": "import { tipDialog } from '@/utils/tools'\n\nexport const errorDialog = message => {\n  tipDialog({\n    message: `${global.i18n.t('deep_link__handle_error_tip', { message })}`,\n    btnText: global.i18n.t('ok'),\n  })\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/core/init/i18n.ts",
    "content": "import { createI18n } from '@/lang/i18n'\nimport type { I18n } from '@/lang/i18n'\nimport { getDeviceLanguage } from '@/utils/tools'\nimport { setLanguage, updateSetting } from '@/core/common'\n\n\nexport default async(setting: LX.AppSetting) => {\n  let lang = setting['common.langId']\n\n  global.i18n = createI18n()\n\n  if (!lang || !global.i18n.availableLocales.includes(lang)) {\n    const deviceLanguage = (await getDeviceLanguage()).toLowerCase()\n    if (typeof deviceLanguage == 'string' && global.i18n.availableLocales.includes(deviceLanguage as I18n['locale'])) {\n      lang = deviceLanguage as I18n['locale']\n    } else {\n      lang = 'en_us'\n    }\n    updateSetting({ 'common.langId': lang })\n  }\n  setLanguage(lang)\n}\n"
  },
  {
    "path": "src/core/init/index.ts",
    "content": "import { initSetting, showPactModal } from '@/core/common'\nimport registerPlaybackService from '@/plugins/player/service'\nimport initTheme from './theme'\nimport initI18n from './i18n'\nimport initUserApi from './userApi'\nimport initPlayer from './player'\nimport dataInit from './dataInit'\nimport initSync from './sync'\nimport initCommonState from './common'\nimport { initDeeplink } from './deeplink'\nimport { setApiSource } from '@/core/apiSource'\nimport commonActions from '@/store/common/action'\nimport settingState from '@/store/setting/state'\nimport { checkUpdate } from '@/core/version'\nimport { bootLog } from '@/utils/bootLog'\nimport { cheatTip } from '@/utils/tools'\n\nlet isFirstPush = true\nconst handlePushedHomeScreen = async() => {\n  await cheatTip()\n  if (settingState.setting['common.isAgreePact']) {\n    if (isFirstPush) {\n      isFirstPush = false\n      void checkUpdate()\n      void initDeeplink()\n    }\n  } else {\n    if (isFirstPush) isFirstPush = false\n    showPactModal()\n  }\n}\n\nlet isInited = false\nexport default async() => {\n  if (isInited) return handlePushedHomeScreen\n  bootLog('Initing...')\n  commonActions.setFontSize(global.lx.fontSize)\n  bootLog('Font size changed.')\n  const setting = await initSetting()\n  bootLog('Setting inited.')\n  // console.log(setting)\n\n  await initTheme(setting)\n  bootLog('Theme inited.')\n  await initI18n(setting)\n  bootLog('I18n inited.')\n\n  await initUserApi(setting)\n  bootLog('User Api inited.')\n\n  setApiSource(setting['common.apiSource'])\n  bootLog('Api inited.')\n\n  registerPlaybackService()\n  bootLog('Playback Service Registered.')\n  await initPlayer(setting)\n  bootLog('Player inited.')\n  await dataInit(setting)\n  bootLog('Data inited.')\n  await initCommonState(setting)\n  bootLog('Common State inited.')\n\n  void initSync(setting)\n  bootLog('Sync inited.')\n\n  // syncSetting()\n\n  isInited ||= true\n\n  return handlePushedHomeScreen\n}\n"
  },
  {
    "path": "src/core/init/player/index.ts",
    "content": "import initPlayer from './player'\nimport initPlayInfo from './playInfo'\nimport initPlayStatus from './playStatus'\nimport initPlayerEvent from './playerEvent'\nimport initWatchList from './watchList'\nimport initPlayProgress from './playProgress'\nimport initPreloadNextMusic from './preloadNextMusic'\nimport initLyric from './lyric'\n\nexport default async(setting: LX.AppSetting) => {\n  await initPlayer(setting)\n  await initLyric(setting)\n  await initPlayInfo(setting)\n  initPlayStatus()\n  initPlayerEvent()\n  initWatchList()\n  initPlayProgress()\n  initPreloadNextMusic()\n}\n"
  },
  {
    "path": "src/core/init/player/lyric.ts",
    "content": "import { init as initLyricPlayer, toggleTranslation, toggleRoma, play, pause, stop, setLyric, setPlaybackRate } from '@/core/lyric'\nimport { updateSetting } from '@/core/common'\nimport { onDesktopLyricPositionChange, showDesktopLyric, onLyricLinePlay, showRemoteLyric } from '@/core/desktopLyric'\nimport playerState from '@/store/player/state'\nimport { updateNowPlayingTitles } from '@/plugins/player/utils'\nimport { setLastLyric } from '@/core/player/playInfo'\nimport { state } from '@/plugins/player/playList'\n\nconst updateRemoteLyric = async(lrc?: string) => {\n  setLastLyric(lrc)\n  if (lrc == null) {\n    void updateNowPlayingTitles((state.prevDuration || 0) * 1000, playerState.musicInfo.name, playerState.musicInfo.singer ?? '', playerState.musicInfo.album ?? '')\n  } else {\n    void updateNowPlayingTitles((state.prevDuration || 0) * 1000, lrc, `${playerState.musicInfo.name}${playerState.musicInfo.singer ? ` - ${playerState.musicInfo.singer}` : ''}`, playerState.musicInfo.album ?? '')\n  }\n}\n\nexport default async(setting: LX.AppSetting) => {\n  await initLyricPlayer()\n  await Promise.all([\n    setPlaybackRate(setting['player.playbackRate']),\n    toggleTranslation(setting['player.isShowLyricTranslation']),\n    toggleRoma(setting['player.isShowLyricRoma']),\n  ])\n\n  if (setting['desktopLyric.enable']) {\n    showDesktopLyric().catch(() => {\n      updateSetting({ 'desktopLyric.enable': false })\n    })\n  }\n  if (setting['player.isShowBluetoothLyric']) {\n    showRemoteLyric(true).catch(() => {\n      updateSetting({ 'player.isShowBluetoothLyric': false })\n    })\n  }\n  onDesktopLyricPositionChange(position => {\n    updateSetting({\n      'desktopLyric.position.x': position.x,\n      'desktopLyric.position.y': position.y,\n    })\n  })\n  onLyricLinePlay(({ text, extendedLyrics }) => {\n    if (!text && !state.isPlaying) {\n      void updateRemoteLyric()\n    } else {\n      void updateRemoteLyric(text)\n    }\n  })\n\n\n  global.app_event.on('play', play)\n  global.app_event.on('pause', pause)\n  global.app_event.on('stop', stop)\n  global.app_event.on('error', pause)\n  global.app_event.on('musicToggled', stop)\n  global.app_event.on('lyricUpdated', setLyric)\n}\n"
  },
  {
    "path": "src/core/init/player/playInfo.ts",
    "content": "import { getPlayInfo } from '@/utils/data'\nimport { getListMusics } from '@/core/list'\nimport { playList, play } from '@/core/player/player'\n\n\nexport default async(setting: LX.AppSetting) => {\n  const info = await getPlayInfo()\n  global.lx.restorePlayInfo = null\n  if (!info?.listId || info.index < 0) return\n\n  const list = await getListMusics(info.listId)\n  if (!list[info.index]) return\n  global.lx.restorePlayInfo = info\n\n  await playList(info.listId, info.index)\n\n  if (setting['player.startupAutoPlay']) setTimeout(play)\n\n\n  // if (!info.list || !info.list[info.index]) {\n  //   const info2 = { ...info }\n  //   if (info2.list) {\n  //     info2.music = info2.list[info2.index]?.name\n  //     info2.list = info2.list.length\n  //   }\n  //   toast('恢复播放数据失败，请去错误日志查看', 'long')\n  //   log.warn('Restore Play Info failed: ', JSON.stringify(info2, null, 2))\n\n  //   return\n  // }\n\n  // let setting = store.getState().common.setting\n  // global.restorePlayInfo = {\n  //   info,\n  //   startupAutoPlay: setting.startupAutoPlay,\n  // }\n\n  // store.dispatch(playerAction.setList({\n  //   list: {\n  //     list: info.list,\n  //     id: info.listId,\n  //   },\n  //   index: info.index,\n  // }))\n}\n"
  },
  {
    "path": "src/core/init/player/playProgress.ts",
    "content": "import { updateListMusics } from '@/core/list'\nimport { setMaxplayTime, setNowPlayTime } from '@/core/player/progress'\nimport { setCurrentTime, getDuration, getPosition } from '@/plugins/player'\nimport { formatPlayTime2 } from '@/utils/common'\nimport { savePlayInfo } from '@/utils/data'\nimport { throttleBackgroundTimer } from '@/utils/tools'\nimport BackgroundTimer from 'react-native-background-timer'\nimport playerState from '@/store/player/state'\nimport settingState from '@/store/setting/state'\nimport { onScreenStateChange } from '@/utils/nativeModules/utils'\nimport { AppState } from 'react-native'\n\nconst delaySavePlayInfo = throttleBackgroundTimer(() => {\n  void savePlayInfo({\n    time: playerState.progress.nowPlayTime,\n    maxTime: playerState.progress.maxPlayTime,\n    listId: playerState.playMusicInfo.listId!,\n    index: playerState.playInfo.playIndex,\n  })\n}, 2000)\n\nexport default () => {\n  // const updateMusicInfo = useCommit('list', 'updateMusicInfo')\n\n  let updateTimeout: number | null = null\n\n  let isScreenOn = true\n\n  const getCurrentTime = () => {\n    let id = playerState.musicInfo.id\n    void getPosition().then(position => {\n      if (!position || id != playerState.musicInfo.id) return\n      setNowPlayTime(position)\n      if (!playerState.isPlay) return\n\n      if (settingState.setting['player.isSavePlayTime'] && !playerState.playMusicInfo.isTempPlay && isScreenOn) {\n        delaySavePlayInfo()\n      }\n    })\n  }\n  const getMaxTime = async() => {\n    setMaxplayTime(await getDuration())\n\n    if (playerState.playMusicInfo.musicInfo && 'source' in playerState.playMusicInfo.musicInfo && !playerState.playMusicInfo.musicInfo.interval) {\n      // console.log(formatPlayTime2(playProgress.maxPlayTime))\n\n      if (playerState.playMusicInfo.listId) {\n        void updateListMusics([{\n          id: playerState.playMusicInfo.listId,\n          musicInfo: {\n            ...playerState.playMusicInfo.musicInfo,\n            interval: formatPlayTime2(playerState.progress.maxPlayTime),\n          },\n        }])\n      }\n    }\n  }\n\n  const clearUpdateTimeout = () => {\n    if (!updateTimeout) return\n    BackgroundTimer.clearInterval(updateTimeout)\n    updateTimeout = null\n  }\n  const startUpdateTimeout = () => {\n    if (!isScreenOn) return\n    clearUpdateTimeout()\n    updateTimeout = BackgroundTimer.setInterval(() => {\n      getCurrentTime()\n    }, 1000 / settingState.setting['player.playbackRate'])\n    getCurrentTime()\n  }\n\n  const setProgress = (time: number, maxTime?: number) => {\n    if (!playerState.musicInfo.id) return\n    // console.log('setProgress', time, maxTime)\n    setNowPlayTime(time)\n    void setCurrentTime(time)\n\n    if (maxTime != null) setMaxplayTime(maxTime)\n\n    // if (!isPlay) audio.play()\n  }\n\n\n  const handlePlay = () => {\n    void getMaxTime()\n    // prevProgressStatus = 'normal'\n    // handleSetTaskBarState(playProgress.progress, prevProgressStatus)\n    startUpdateTimeout()\n  }\n  const handlePause = () => {\n    // prevProgressStatus = 'paused'\n    // handleSetTaskBarState(playProgress.progress, prevProgressStatus)\n    // clearBufferTimeout()\n    clearUpdateTimeout()\n  }\n\n  const handleStop = () => {\n    clearUpdateTimeout()\n    setNowPlayTime(0)\n    setMaxplayTime(0)\n    // prevProgressStatus = 'none'\n    // handleSetTaskBarState(playProgress.progress, prevProgressStatus)\n  }\n\n  const handleError = () => {\n    // if (!restorePlayTime) restorePlayTime = getCurrentTime() // 记录出错的播放时间\n    // console.log('handleError')\n    // prevProgressStatus = 'error'\n    // handleSetTaskBarState(playProgress.progress, prevProgressStatus)\n    clearUpdateTimeout()\n  }\n\n\n  const handleSetPlayInfo = () => {\n    // restorePlayTime = playProgress.nowPlayTime\n    // void setCurrentTime(playerState.progress.nowPlayTime)\n    // setMaxplayTime(playProgress.maxPlayTime)\n    handlePause()\n    if (!playerState.playMusicInfo.isTempPlay) {\n      void savePlayInfo({\n        time: playerState.progress.nowPlayTime,\n        maxTime: playerState.progress.maxPlayTime,\n        listId: playerState.playMusicInfo.listId!,\n        index: playerState.playInfo.playIndex,\n      })\n    }\n  }\n\n  // watch(() => playerState.progress.nowPlayTime, (newValue, oldValue) => {\n  //   if (settingState.setting['player.isSavePlayTime'] && !playMusicInfo.isTempPlay) {\n  //     delaySavePlayInfo({\n  //       time: newValue,\n  //       maxTime: playerState.progress.maxPlayTime,\n  //       listId: playMusicInfo.listId as string,\n  //       index: playInfo.playIndex,\n  //     })\n  //   }\n  // })\n  // watch(() => playerState.progress.maxPlayTime, maxPlayTime => {\n  //   if (!playMusicInfo.isTempPlay) {\n  //     delaySavePlayInfo({\n  //       time: playerState.progress.nowPlayTime,\n  //       maxTime: maxPlayTime,\n  //       listId: playMusicInfo.listId as string,\n  //       index: playInfo.playIndex,\n  //     })\n  //   }\n  // })\n\n  const handleConfigUpdated: typeof global.state_event.configUpdated = (keys, settings) => {\n    if (keys.includes('player.playbackRate')) startUpdateTimeout()\n  }\n\n  const handleScreenStateChanged: Parameters<typeof onScreenStateChange>[0] = (state) => {\n    isScreenOn = state == 'ON'\n    if (isScreenOn) {\n      if (playerState.isPlay) startUpdateTimeout()\n    } else clearUpdateTimeout()\n  }\n\n  // 修复在某些设备上屏幕状态改变事件未触发导致的进度条未更新的问题\n  AppState.addEventListener('change', (state) => {\n    if (state == 'active' && !isScreenOn) handleScreenStateChanged('ON')\n  })\n\n  global.app_event.on('play', handlePlay)\n  global.app_event.on('pause', handlePause)\n  global.app_event.on('stop', handleStop)\n  global.app_event.on('error', handleError)\n  global.app_event.on('setProgress', setProgress)\n  // global.app_event.on(eventPlayerNames.restorePlay, handleRestorePlay)\n  // global.app_event.on('playerLoadeddata', handleLoadeddata)\n  // global.app_event.on('playerCanplay', handleCanplay)\n  // global.app_event.on('playerWaiting', handleWating)\n  // global.app_event.on('playerEmptied', handleEmpied)\n  global.app_event.on('musicToggled', handleSetPlayInfo)\n  global.state_event.on('configUpdated', handleConfigUpdated)\n\n  onScreenStateChange(handleScreenStateChanged)\n}\n"
  },
  {
    "path": "src/core/init/player/playStatus.ts",
    "content": "// import { LIST_ID_LOVE } from '@/config/constant'\n\nimport { updateMetaData } from '@/plugins/player'\nimport playerState from '@/store/player/state'\n\nexport default () => {\n  // const setVisibleDesktopLyric = useCommit('setVisibleDesktopLyric')\n  // const setLockDesktopLyric = useCommit('setLockDesktopLyric')\n\n  const buttons = {\n    empty: true,\n    collect: false,\n    play: false,\n    prev: true,\n    next: true,\n    lrc: false,\n    lockLrc: false,\n  }\n  const setButtons = () => {\n    // setPlayerAction(buttons)\n    if (!playerState.playMusicInfo.musicInfo) return\n    void updateMetaData(playerState.musicInfo, playerState.isPlay, playerState.lastLyric)\n  }\n  // const updateCollectStatus = async() => {\n  //   // let status = !!playMusicInfo.musicInfo && await checkListExistMusic(LIST_ID_LOVE, playerState.playMusicInfo.musicInfo.id)\n  //   // if (buttons.collect == status) return false\n  //   // buttons.collect = status\n  //   return true\n  // }\n\n  const handlePlay = () => {\n    // if (buttons.empty) buttons.empty = false\n    if (buttons.play) return\n    buttons.play = true\n    setButtons()\n  }\n  const handlePause = () => {\n    // if (buttons.empty) buttons.empty = false\n    if (!buttons.play) return\n    buttons.play = false\n    setButtons()\n  }\n  // const handleStop = () => {\n  //   // if (playerState.playMusicInfo.musicInfo != null) return\n  //   // if (buttons.collect) buttons.collect = false\n  //   // buttons.empty = true\n  //   setButtons()\n  // }\n  // const handleSetPlayInfo = () => {\n  //   void updateCollectStatus().then(isExist => {\n  //     if (isExist) setButtons()\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()) setButtons()\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  global.app_event.on('play', handlePlay)\n  global.app_event.on('pause', handlePause)\n  global.app_event.on('stop', handlePause)\n  // global.app_event.on('musicToggled', handleSetPlayInfo)\n  // window.app_event.on(eventTaskbarNames.setTaskbarThumbnailClip, handleSetTaskbarThumbnailClip)\n  // window.app_event.on('myListMusicUpdate', throttleListChange)\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) buttons.empty = false\n    setButtons()\n  }\n}\n"
  },
  {
    "path": "src/core/init/player/player.ts",
    "content": "import { addPlayedList, clearPlayedList } from '@/core/player/playedList'\nimport { pause, playNext } from '@/core/player/player'\nimport { setStatusText, setIsPlay } from '@/core/player/playStatus'\n// import { resetPlayerMusicInfo } from '@/core/player/playInfo'\nimport { setStop } from '@/plugins/player'\nimport { delayUpdateMusicInfo } from '@/plugins/player/playList'\nimport playerState from '@/store/player/state'\nimport settingState from '@/store/setting/state'\n\n\nexport default async(setting: LX.AppSetting) => {\n  const setPlayStatus = () => {\n    setIsPlay(true)\n  }\n  const setPauseStatus = () => {\n    setIsPlay(false)\n    if (global.lx.isPlayedStop) void pause()\n  }\n\n  const handleEnded = () => {\n    // setTimeout(() => {\n    if (global.lx.isPlayedStop) {\n      setStatusText(global.i18n.t('player__end'))\n      return\n    }\n    // resetPlayerMusicInfo()\n    // global.app_event.stop()\n    global.app_event.setProgress(0)\n    setStatusText(global.i18n.t('player__end'))\n    void playNext(true)\n    // })\n  }\n\n  const setStopStatus = () => {\n    setIsPlay(false)\n    setStatusText('')\n    void setStop()\n  }\n\n  const updatePic = () => {\n    if (!settingState.setting['player.isShowNotificationImage']) return\n    if (playerState.playMusicInfo.musicInfo && playerState.musicInfo.pic) {\n      delayUpdateMusicInfo(playerState.musicInfo, playerState.lastLyric)\n    }\n  }\n\n  const handleConfigUpdated: typeof global.state_event.configUpdated = (keys, settings) => {\n    if (keys.includes('player.togglePlayMethod')) {\n      const newValue = settings['player.togglePlayMethod']\n      if (playerState.playedList.length) clearPlayedList()\n      const playMusicInfo = playerState.playMusicInfo\n      if (newValue == 'random' && playMusicInfo.musicInfo && !playMusicInfo.isTempPlay) addPlayedList({ ...(playMusicInfo as LX.Player.PlayMusicInfo) })\n    }\n  }\n\n\n  global.app_event.on('play', setPlayStatus)\n  global.app_event.on('pause', setPauseStatus)\n  global.app_event.on('error', setPauseStatus)\n  global.app_event.on('stop', setStopStatus)\n  global.app_event.on('playerEnded', handleEnded)\n  global.app_event.on('picUpdated', updatePic)\n  global.state_event.on('configUpdated', handleConfigUpdated)\n}\n"
  },
  {
    "path": "src/core/init/player/playerEvent.ts",
    "content": "import { playNext, setMusicUrl } from '@/core/player/player'\nimport { setStatusText } from '@/core/player/playStatus'\nimport { getPosition, isEmpty, setStop } from '@/plugins/player'\nimport { isActive } from '@/utils/tools'\nimport BackgroundTimer from 'react-native-background-timer'\nimport playerState from '@/store/player/state'\nimport { setNowPlayTime } from '@/core/player/progress'\n\n\nexport default () => {\n  let retryNum = 0\n  let prevTimeoutId: string | null = null\n\n  let loadingTimeout: number | null = null\n  let delayNextTimeout: number | null = null\n  const startLoadingTimeout = () => {\n    // console.log('start load timeout')\n    clearLoadingTimeout()\n    loadingTimeout = BackgroundTimer.setTimeout(() => {\n      // if (global.lx.isPlayedStop) {\n      //   prevTimeoutId = null\n      //   setStatusText('')\n      //   return\n      // }\n\n      // 如果加载超时，则尝试刷新URL\n      if (prevTimeoutId == playerState.musicInfo.id) {\n        prevTimeoutId = null\n        void playNext(true)\n      } else {\n        prevTimeoutId = playerState.musicInfo.id\n        if (playerState.playMusicInfo.musicInfo) setMusicUrl(playerState.playMusicInfo.musicInfo, true)\n      }\n    }, 25000)\n  }\n  const clearLoadingTimeout = () => {\n    if (!loadingTimeout) return\n    // console.log('clear load timeout')\n    BackgroundTimer.clearTimeout(loadingTimeout)\n    loadingTimeout = null\n  }\n\n  const clearDelayNextTimeout = () => {\n    // console.log(this.delayNextTimeout)\n    if (!delayNextTimeout) return\n    BackgroundTimer.clearTimeout(delayNextTimeout)\n    delayNextTimeout = null\n  }\n  const addDelayNextTimeout = () => {\n    clearDelayNextTimeout()\n    delayNextTimeout = BackgroundTimer.setTimeout(() => {\n      if (global.lx.isPlayedStop) {\n        setStatusText('')\n        return\n      }\n      void playNext(true)\n    }, 5000)\n  }\n\n  const handleLoadstart = () => {\n    console.log('handleLoadstart', playerState.isPlay)\n    if (global.lx.isPlayedStop || !playerState.isPlay) return\n    startLoadingTimeout()\n    setStatusText(global.i18n.t('player__loading'))\n  }\n\n  // const handleLoadeddata = () => {\n  //   setStatusText(global.i18n.t('player__loading'))\n  // }\n\n  // const handleCanplay = () => {\n  //   setStatusText('')\n  // }\n\n  const handlePlaying = () => {\n    setStatusText('')\n    clearLoadingTimeout()\n  }\n\n  const handleEmpied = () => {\n    clearDelayNextTimeout()\n    clearLoadingTimeout()\n  }\n\n  const handleWating = () => {\n    setStatusText(global.i18n.t('player__buffering'))\n  }\n\n  const handleError = () => {\n    if (!playerState.musicInfo.id) return\n    clearLoadingTimeout()\n    if (global.lx.isPlayedStop) return\n    if (playerState.playMusicInfo.musicInfo && retryNum < 2) { // 若音频URL无效则尝试刷新2次URL\n      let musicInfo = playerState.playMusicInfo.musicInfo\n      void getPosition().then((position) => {\n        if (position) setNowPlayTime(position)\n      }).finally(() => {\n        // console.log(this.retryNum)\n        if (playerState.playMusicInfo.musicInfo !== musicInfo) return\n        retryNum++\n        setMusicUrl(playerState.playMusicInfo.musicInfo, true)\n        setStatusText(global.i18n.t('player__refresh_url'))\n      })\n      return\n    }\n    if (!isEmpty()) void setStop()\n\n    if (isActive()) {\n      setStatusText(global.i18n.t('player__error'))\n      setTimeout(addDelayNextTimeout)\n    } else {\n      console.warn('error skip to next')\n      void playNext(true)\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  global.app_event.on('playerLoadstart', handleLoadstart)\n  // global.app_event.on('playerLoadeddata', handleLoadeddata)\n  // global.app_event.on('playerCanplay', handleCanplay)\n  global.app_event.on('playerPlaying', handlePlaying)\n  global.app_event.on('playerWaiting', handleWating)\n  global.app_event.on('playerEmptied', handleEmpied)\n  global.app_event.on('playerError', handleError)\n  global.app_event.on('musicToggled', handleSetPlayInfo)\n}\n"
  },
  {
    "path": "src/core/init/player/preloadNextMusic.ts",
    "content": "import { getMusicUrl } from '@/core/music'\nimport { getNextPlayMusicInfo, resetRandomNextMusicInfo } from '@/core/player/player'\nimport { checkUrl } from '@/utils/request'\nimport playerState from '@/store/player/state'\nimport { isCached } from '@/plugins/player/utils'\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 [cached, available] = await Promise.all([isCached(url), checkUrl(url).then(() => true).catch(() => false)])\n      if (!cached && !available) {\n        const url = await getMusicUrl({ musicInfo: info.musicInfo, isRefresh: true }).catch(() => '')\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 (!playerState.musicInfo.id) return\n    preloadMusicInfo.preProgress = time\n  }\n\n  const handleSetPlayInfo = () => {\n    resetPreloadInfo()\n  }\n\n  const handleConfigUpdated: typeof global.state_event.configUpdated = (keys, settings) => {\n    if (!keys.includes('player.togglePlayMethod')) return\n    if (!preloadMusicInfo.info || preloadMusicInfo.info.isTempPlay) return\n    resetRandomNextMusicInfo()\n    preloadMusicInfo.info = null\n    preloadMusicInfo.preProgress = playerState.progress.nowPlayTime\n  }\n\n  const handlePlayProgressChanged: typeof global.state_event.playProgressChanged = (progress) => {\n    const duration = progress.maxPlayTime\n    if (duration > 10 && duration - progress.nowPlayTime < 10 && !preloadMusicInfo.info) {\n      void preloadNextMusicUrl(progress.nowPlayTime)\n    }\n  }\n\n  global.app_event.on('setProgress', setProgress)\n  global.app_event.on('musicToggled', handleSetPlayInfo)\n  global.state_event.on('configUpdated', handleConfigUpdated)\n  global.state_event.on('playProgressChanged', handlePlayProgressChanged)\n}\n"
  },
  {
    "path": "src/core/init/player/watchList.ts",
    "content": "import { playNext } from '@/core/player/player'\nimport { updatePlayIndex } from '@/core/player/playInfo'\nimport { throttleBackgroundTimer } from '@/utils/tools'\nimport playerState from '@/store/player/state'\n\nconst changedListIds = new Set<string | null>()\n\nexport default () => {\n  const throttleListChange = throttleBackgroundTimer(() => {\n    const isSkip = !changedListIds.has(playerState.playInfo.playerListId) && !changedListIds.has(playerState.playMusicInfo.listId)\n    changedListIds.clear()\n    if (isSkip) return\n\n    const { playIndex } = updatePlayIndex()\n    if (playIndex < 0) { // 歌曲被移除\n      // if (global.lx.isPlayedStop) {\n      //   stop()\n      //   setTimeout(() => {\n      //     setPlayMusicInfo(null, null)\n      //   })\n      // } else\n      if (!playerState.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  global.app_event.on('myListMusicUpdate', handleListChange)\n  global.app_event.on('downloadListUpdate', handleDownloadListChange)\n}\n"
  },
  {
    "path": "src/core/init/sync.ts",
    "content": "import { connectServer } from '@/plugins/sync'\nimport { updateSetting } from '@/core/common'\nimport { getSyncHost } from '@/plugins/sync/data'\n\n\nexport default async(setting: LX.AppSetting) => {\n  if (!setting['sync.enable']) return\n\n  const host = await getSyncHost()\n  // console.log(host)\n  if (!host) {\n    updateSetting({ 'sync.enable': false })\n    return\n  }\n  void connectServer(host)\n}\n"
  },
  {
    "path": "src/core/init/theme.ts",
    "content": "\nimport { getAppearance, getIsSupportedAutoTheme, onAppearanceChange } from '@/utils/tools'\nimport { setShouldUseDarkColors, applyTheme } from '@/core/theme'\nimport { getTheme } from '@/theme/themes/index'\nimport settingState from '@/store/setting/state'\nimport StatusBar from '@/components/common/StatusBar'\n// import { Dimensions, PixelRatio } from 'react-native'\n\n\nexport default async(setting: LX.AppSetting) => {\n  if (getIsSupportedAutoTheme()) {\n    setShouldUseDarkColors(getAppearance() == 'dark')\n\n    onAppearanceChange(color => {\n      setShouldUseDarkColors((color ?? 'light') == 'dark')\n      if (settingState.setting['common.isAutoTheme']) void getTheme().then(applyTheme)\n    })\n  }\n\n  applyTheme(await getTheme())\n\n  global.state_event.on('themeUpdated', (theme) => {\n    StatusBar.setBarStyle(theme.isDark ? 'light-content' : 'dark-content')\n  })\n  // onDimensionChange(({ window }) => {\n  //   let screenW = window.width\n  //   let screenH = window.height\n  //   if (screenW > screenH) {\n  //     const temp = screenW\n  //     screenW = screenH\n  //     screenH = temp\n  //   }\n  //   global.lx.windowInfo.screenW = screenW\n  //   global.lx.windowInfo.screenH = screenH\n  //   global.lx.windowInfo.screenPxW = PixelRatio.getPixelSizeForLayoutSize(screenW)\n  //   global.lx.windowInfo.screenPxH = PixelRatio.getPixelSizeForLayoutSize(screenH)\n  //   console.log('change', global.lx.windowInfo)\n  // })\n}\n"
  },
  {
    "path": "src/core/init/userApi/index.ts",
    "content": "import { type InitParams, onScriptAction, sendAction, type ResponseParams, type UpdateInfoParams, type RequestParams } from '@/utils/nativeModules/userApi'\nimport { log, setUserApiList, setUserApiStatus } from '@/core/userApi'\nimport settingState from '@/store/setting/state'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { fetchData } from './request'\nimport { getUserApiList } from '@/utils/data'\nimport { confirmDialog, openUrl, tipDialog } from '@/utils/tools'\n\n\nexport default async(setting: LX.AppSetting) => {\n  const userApiRequestMap = new Map<string, { resolve: (value: ResponseParams['result']) => void, reject: (error: Error) => void, timeout: number }>()\n  const scriptRequestMap = new Map<string, { request: Promise<any>, abort: () => void }>()\n\n  const cancelRequest = (requestKey: string, message: string) => {\n    const target = scriptRequestMap.get(requestKey)\n    if (!target) return\n    scriptRequestMap.delete(requestKey)\n    target.abort()\n  }\n  const sendScriptRequest = (requestKey: string, url: string, options: RequestParams['options']) => {\n    let req = fetchData(url, options)\n    req.request.then(response => {\n      // console.log(response)\n      sendAction('response', {\n        error: null,\n        requestKey,\n        response,\n      })\n    }).catch(err => {\n      sendAction('response', {\n        error: err.message,\n        requestKey,\n        response: null,\n      })\n    }).finally(() => {\n      scriptRequestMap.delete(requestKey)\n    })\n    scriptRequestMap.set(requestKey, req)\n  }\n  const sendUserApiRequest = async(data: LX.UserApi.UserApiRequestParams) => {\n    const handleApiUpdate = () => {\n      const target = userApiRequestMap.get(data.requestKey)\n      if (!target) return\n      userApiRequestMap.delete(data.requestKey)\n      BackgroundTimer.clearTimeout(target.timeout)\n      target.reject(new Error('request failed'))\n    }\n    const requestPromise = new Promise<ResponseParams['result']>((resolve, reject) => {\n      userApiRequestMap.set(data.requestKey, {\n        resolve,\n        reject,\n        timeout: BackgroundTimer.setTimeout(() => {\n          const target = userApiRequestMap.get(data.requestKey)\n          if (!target) return\n          userApiRequestMap.delete(data.requestKey)\n          target.reject(new Error('request timeout'))\n        }, 20_000),\n      })\n      sendAction('request', data)\n    }).finally(() => {\n      global.state_event.off('apiSourceUpdated', handleApiUpdate)\n    })\n    global.state_event.on('apiSourceUpdated', handleApiUpdate)\n    return requestPromise\n  }\n  const handleUserApiResponse = ({ status, result, requestKey, errorMessage }: ResponseParams) => {\n    const target = userApiRequestMap.get(requestKey)\n    if (!target) return\n    userApiRequestMap.delete(requestKey)\n    BackgroundTimer.clearTimeout(target.timeout)\n    if (status) target.resolve(result)\n    else target.reject(new Error(errorMessage ?? 'failed'))\n  }\n  const handleStateChange = ({ status, errorMessage, info }: InitParams) => {\n    // console.log(status, message, info)\n    setUserApiStatus(status, errorMessage)\n    if (!info || info.id !== settingState.setting['common.apiSource']) return\n    if (status) {\n      if (info.sources) {\n        let apis: any = {}\n        let qualitys: LX.QualityList = {}\n        for (const [source, { actions, type, qualitys: sourceQualitys }] of Object.entries(info.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(err => {\n                      console.log(err.message)\n                      throw 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        global.lx.qualityList = qualitys\n        global.lx.apis = apis\n        global.state_event.apiSourceUpdated(settingState.setting['common.apiSource'])\n      }\n    } else {\n      if (errorMessage) {\n        void tipDialog({\n          message: `${global.i18n.t('user_api__init_failed_alert', { name: info.name })}\\n${errorMessage}`,\n          // selection: true,\n          btnText: global.i18n.t('ok'),\n        })\n      }\n    }\n    if (!global.lx.apiInitPromise[1]) global.lx.apiInitPromise[2](status)\n  }\n  const showUpdateAlert = ({ name, log, updateUrl }: UpdateInfoParams) => {\n    if (updateUrl) {\n      void confirmDialog({\n        message: `${global.i18n.t('user_api_update_alert', { name })}\\n${log}`,\n        // selection: true,\n        // showCancel: true,\n        confirmButtonText: global.i18n.t('user_api_update_alert_open_url'),\n        cancelButtonText: global.i18n.t('close'),\n      }).then(confirm => {\n        if (!confirm) return\n        setTimeout(() => {\n          void openUrl(updateUrl)\n        }, 300)\n      })\n    } else {\n      void tipDialog({\n        message: `${global.i18n.t('user_api_update_alert', { name })}\\n${log}`,\n        // selection: true,\n        btnText: global.i18n.t('ok'),\n      })\n    }\n  }\n\n  onScriptAction((event) => {\n    // console.log('script actuon: ', event)\n    switch (event.action) {\n      case 'init':\n        if ((event as unknown as { errorMessage?: string }).errorMessage) event.data.errorMessage = (event as unknown as { errorMessage: string }).errorMessage\n        handleStateChange(event.data)\n        break\n      case 'response':\n        handleUserApiResponse(event.data)\n        break\n      case 'request':\n        sendScriptRequest(event.data.requestKey, event.data.url, event.data.options)\n        break\n      case 'cancelRequest':\n        cancelRequest(event.data, 'request canceled')\n        break\n      case 'showUpdateAlert':\n        showUpdateAlert(event.data)\n        break\n      case 'log':\n        switch ((event as unknown as { type: keyof typeof log }).type) {\n          case 'log':\n          case 'info':\n            log.info((event as unknown as { log: string }).log)\n            break\n          case 'error':\n            log.error((event as unknown as { log: string }).log)\n            break\n          case 'warn':\n            log.warn((event as unknown as { log: string }).log)\n            break\n          default:\n            break\n        }\n        break\n      default:\n        break\n    }\n  })\n\n  setUserApiList(await getUserApiList())\n}\n"
  },
  {
    "path": "src/core/init/userApi/request.js",
    "content": "// import needle from 'needle'\n// import progress from 'request-progress'\nimport BackgroundTimer from 'react-native-background-timer'\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\nconst handleRequestData = async({\n  method = 'get',\n  headers = {},\n  format = 'json',\n  credentials = 'omit',\n  cache = 'default',\n  ...options\n}) => {\n  // console.log(url, options)\n  headers = Object.assign({\n    Accept: 'application/json',\n  }, headers)\n  if (method.toLocaleLowerCase() === 'post' && !headers['Content-Type']) {\n    if (options.form) {\n      headers['Content-Type'] = 'application/x-www-form-urlencoded'\n      const formBody = []\n      for (let [key, value] of Object.entries(options.form)) {\n        let encodedKey = encodeURIComponent(key)\n        let encodedValue = encodeURIComponent(value)\n        formBody.push(`${encodedKey}=${encodedValue}`)\n      }\n      options.body = formBody.join('&')\n      delete options.form\n    } else if (options.formData) {\n      headers['Content-Type'] = 'multipart/form-data'\n      const formBody = []\n      for (let [key, value] of Object.entries(options.form)) {\n        let encodedKey = encodeURIComponent(key)\n        let encodedValue = encodeURIComponent(value)\n        formBody.push(`${encodedKey}=${encodedValue}`)\n      }\n      options.body = options.formData\n      delete options.formData\n    } else {\n      headers['Content-Type'] = 'application/json'\n    }\n  }\n  if (headers['Content-Type'] === 'application/json' && options.body) {\n    options.body = JSON.stringify(options.body)\n  }\n\n  return {\n    ...options,\n    method,\n    credentials,\n    cache,\n    headers: Object.assign({}, defaultHeaders, headers),\n  }\n}\n\n// https://stackoverflow.com/a/64945178\nconst blobToBuffer = (blob) => {\n  return new Promise((resolve, reject) => {\n    const reader = new global.FileReader()\n    reader.onerror = reject\n    reader.onload = () => {\n      const data = reader.result.slice(reader.result.indexOf('base64,') + 7)\n      resolve(Buffer.from(data, 'base64'))\n    }\n    reader.readAsDataURL(blob)\n  })\n}\n\nexport const fetchData = (url, { timeout = 13_000, ...options }) => {\n  // console.log('---start---', url)\n\n  const controller = new global.AbortController()\n  let id = BackgroundTimer.setTimeout(() => {\n    id = null\n    controller.abort()\n  }, timeout)\n\n  return {\n    request: handleRequestData(options).then(options => {\n      return global.fetch(url, {\n        ...options,\n        signal: controller.signal,\n      }).then(resp => (options.binary ? resp.blob() : resp.text()).then(text => {\n        // console.log(options, headers, text)\n        return {\n          headers: resp.headers.map,\n          body: text,\n          statusCode: resp.status,\n          statusMessage: resp.statusText,\n          url: resp.url,\n          ok: resp.ok,\n        }\n      })).then(resp => {\n        if (options.binary) {\n          return blobToBuffer(resp.body).then(buffer => {\n            resp.body = buffer\n            return resp\n          })\n        } else {\n          try {\n            resp.body = JSON.parse(resp.body)\n          } catch {}\n          return resp\n        }\n      }).catch(err => {\n        // console.log(err, err.code, err.message)\n        return Promise.reject(err)\n      }).finally(() => {\n        if (id == null) return\n        BackgroundTimer.clearTimeout(id)\n      })\n    }),\n    abort() {\n      controller.abort()\n    },\n  }\n}\n"
  },
  {
    "path": "src/core/leaderboard.ts",
    "content": "import leaderboardState, { type Board, type ListDetailInfo } from '@/store/leaderboard/state'\nimport leaderboardActions from '@/store/leaderboard/action'\nimport { deduplicationList, toNewMusicInfo } from '@/utils'\nimport musicSdk from '@/utils/musicSdk'\n\n/**\n * 获取排行榜内单页歌曲\n * @param id 排行榜id  {souce}__{bangId}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const setListDetailInfo = (id: string) => {\n  clearListDetail()\n  const [source] = id.split('__') as [LX.OnlineSource, string]\n  leaderboardActions.setListDetailInfo(source, id)\n}\nexport const setListDetail = (result: ListDetailInfo, id: string, page: number) => {\n  return leaderboardActions.setListDetail(result, id, page)\n}\n\nexport const clearListDetail = () => {\n  leaderboardActions.clearListDetail()\n}\n\nconst setBoard = (board: Board, source: LX.OnlineSource) => {\n  leaderboardActions.setBoard(board, source)\n}\n\ninterface PageCache { data: ListDetailInfo, sourcePage: number }\ntype CacheValue = Map<string, PageCache | ListDetailInfo['list']>\n\nconst cache = new Map<string, CacheValue>()\nconst LIST_LOAD_LIMIT = 30\n\nexport const getBoardsList = async(source: LX.OnlineSource) => {\n  // const source = (await getLeaderboardSetting()).source as LX.OnlineSource\n  if (leaderboardState.boards[source]) return leaderboardState.boards[source].list\n  const board = await (musicSdk[source]?.leaderboard.getBoards() as Promise<Board>)\n  setBoard(board, source)\n  return leaderboardState.boards[source]!.list\n}\n\n/**\n * 获取排行榜内单页分页歌曲（用于在本地控制每页大小）\n * @param source 源\n * @param bangId 排行榜id\n * @param page 页数\n * @returns\n */\nconst getListLimit = async(source: LX.OnlineSource, bangId: string, page: number): Promise<ListDetailInfo> => {\n  const listKey = `${source}__${bangId}`\n  const prevPageKey = `${source}__${bangId}__${page - 1}`\n  const tempListKey = `${source}__${bangId}__temp`\n\n  let listCache = cache.get(listKey)!\n  if (!listCache) cache.set(listKey, listCache = new Map<string, PageCache | LX.Music.MusicInfoOnline[]>())\n  let sourcePage = 0\n  {\n    const prevPageData = listCache.get(prevPageKey) as PageCache\n    if (prevPageData) sourcePage = prevPageData.sourcePage\n  }\n\n  return musicSdk[source]?.leaderboard.getList(bangId, sourcePage + 1).then((result: ListDetailInfo) => {\n    if (listCache !== cache.get(listKey)) return\n    result.list = deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])\n    let p = page\n    const tempList = listCache.get(tempListKey) as ListDetailInfo['list']\n    if (tempList) {\n      listCache.delete(tempListKey)\n      listCache.set(`${source}__${bangId}__${p}`, {\n        data: {\n          ...result,\n          list: [...tempList, ...result.list.splice(0, LIST_LOAD_LIMIT - tempList.length)],\n          page: p,\n          limit: LIST_LOAD_LIMIT,\n        },\n        sourcePage,\n      })\n      p++\n    }\n    sourcePage++\n    do {\n      if (result.list.length < LIST_LOAD_LIMIT && sourcePage < Math.ceil(result.total / result.limit)) {\n        listCache.set(tempListKey, result.list.splice(0, LIST_LOAD_LIMIT))\n        break\n      }\n      listCache.set(`${source}__${bangId}__${p}`, {\n        data: {\n          ...result,\n          list: result.list.splice(0, LIST_LOAD_LIMIT),\n          page: p,\n          limit: LIST_LOAD_LIMIT,\n        },\n        sourcePage,\n      })\n      p++\n    } while (result.list.length > 0)\n    return (listCache.get(`${source}__${bangId}__${page}`) as PageCache).data\n  }) ?? Promise.reject(new Error('source not found'))\n}\n\n/**\n * 获取排行榜内单页歌曲\n * @param id 排行榜id  {souce}__{bangId}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetail = async(id: string, page: number, isRefresh = false): Promise<ListDetailInfo> => {\n  // console.log(tabId)\n  const [source, bangId] = id.split('__') as [LX.OnlineSource, string]\n  const listKey = `${source}__${bangId}`\n  const pageKey = `${source}__${bangId}__${page}`\n\n  let listCache = cache.get(listKey)\n  if (!listCache || isRefresh) {\n    cache.set(listKey, listCache = new Map<string, PageCache | LX.Music.MusicInfoOnline[]>())\n  }\n\n  let pageCache = listCache.get(pageKey) as PageCache\n  if (pageCache) return pageCache.data\n\n  return getListLimit(source, bangId, page)\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(tabId)\n  const listKey = `${source}__${bangId}`\n  let listCache = cache.get(listKey)!\n  if (!listCache || isRefresh) {\n    cache.set(listKey, listCache = new Map<string, PageCache | LX.Music.MusicInfoOnline[]>())\n  }\n\n  const loadData = async(page: number): Promise<ListDetailInfo> => {\n    const pageKey = `${source}__${bangId}__${page}`\n    let pageCache = listCache.get(pageKey) as PageCache\n    if (pageCache) return pageCache.data\n    return getListLimit(source, bangId, page)\n  }\n  return loadData(1).then(async result => {\n    if (result.total <= result.limit) return result.list\n\n    let maxPage = Math.ceil(result.total / result.limit)\n    const loadDetail = async(loadPage = 2): Promise<LX.Music.MusicInfoOnline[]> => {\n      return loadPage == maxPage\n        ? loadData(loadPage).then(result => result.list)\n        // eslint-disable-next-line @typescript-eslint/promise-function-async\n        : loadData(loadPage).then(result1 => loadDetail(++loadPage).then(result2 => [...result1.list, ...result2]))\n    }\n    return loadDetail().then(result2 => [...result.list, ...result2])\n  }).then(list => deduplicationList(list))\n}\n\n/**\n * 获取并设置排行榜内单页歌曲\n * @param id 排行榜id  {souce}__{id}\n * @param isRefresh 是否跳过缓存\n * @returns\n */\n// export 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 && leaderboardState.listDetailInfo.key == key && leaderboardState.listDetailInfo.list.length) return\n\n//   leaderboardState.listDetailInfo.key = key\n\n//   return getListDetail(id, page, isRefresh).then((result: ListDetailInfo) => {\n//     if (key != leaderboardState.listDetailInfo.key) return\n//     setListDetail(result, id, page)\n//   }).catch((error: any) => {\n//     clearListDetail()\n//     console.log(error)\n//     throw error\n//   })\n// }\n"
  },
  {
    "path": "src/core/list.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\nimport listAction from '@/store/list/action'\nimport listState from '@/store/list/state'\nimport settingState from '@/store/setting/state'\nimport { fixNewMusicInfoQuality } from '@/utils'\nimport { saveListPrevSelectId } from '@/utils/data'\n\n/**\n * 覆盖全部列表数据\n * @param data\n */\nexport const overwriteListFull = async(data: LX.List.ListActionDataOverwrite) => {\n  await global.list_event.list_data_overwrite(data)\n}\n\n\n/**\n * 添加用户列表\n */\nexport const createUserList = async(position: number, listInfos: LX.List.UserListInfo[]) => {\n  await global.list_event.list_create(position, listInfos)\n}\n\n/**\n * 移除用户列表及列表内歌曲\n */\nexport const removeUserList = async(ids: string[]) => {\n  await global.list_event.list_remove(ids)\n}\n\n/**\n * 更新用户列表\n */\nexport const updateUserList = async(listInfos: LX.List.UserListInfo[]) => {\n  await global.list_event.list_update(listInfos)\n}\n\n/**\n * 批量移动用户列表位置\n */\nexport const updateUserListPosition = async(position: number, ids: string[]) => {\n  await global.list_event.list_update_position(position, ids)\n}\n\n\n/**\n * 批量添加歌曲到列表\n */\nexport const addListMusics = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n  await global.list_event.list_music_add(id, musicInfos, addMusicLocationType)\n}\n\n/**\n * 跨列表批量移动歌曲\n */\nexport const moveListMusics = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType) => {\n  await global.list_event.list_music_move(fromId, toId, musicInfos, addMusicLocationType)\n}\n\n/**\n * 批量删除列表内歌曲\n */\nexport const removeListMusics = async(listId: string, ids: string[]) => {\n  await global.list_event.list_music_remove(listId, ids)\n}\n\n/**\n * 批量更新列表内歌曲\n */\nexport const updateListMusics = async(infos: Array<{ id: string, musicInfo: LX.Music.MusicInfo }>) => {\n  await global.list_event.list_music_update(infos)\n}\n\n/**\n * 批量移动列表内歌曲的位置\n */\nexport const updateListMusicPosition = async(listId: string, position: number, ids: string[]) => {\n  await global.list_event.list_music_update_position(listId, position, ids)\n}\n\n/**\n * 覆盖列表内的歌曲\n */\nexport const overwriteListMusics = async(listId: string, musicInfos: LX.Music.MusicInfo[]) => {\n  await global.list_event.list_music_overwrite(listId, musicInfos)\n}\n\n/**\n * 覆盖列表内的歌曲\n */\nexport const clearListMusics = async(ids: string[]) => {\n  await global.list_event.list_music_clear(ids)\n}\n\n/**\n * 覆盖单个列表\n * @param listInfo\n * @param musics\n */\nexport const overwriteList = async(listInfoFull: LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull) => {\n  let userListInfo\n  switch (listInfoFull.id) {\n    case LIST_IDS.DEFAULT:\n    case LIST_IDS.LOVE:\n      break\n\n    default:\n      userListInfo = listInfoFull as LX.List.UserListInfo\n      await updateUserList([\n        {\n          name: userListInfo.name,\n          id: userListInfo.id,\n          source: userListInfo.source,\n          sourceListId: userListInfo.sourceListId,\n          locationUpdateTime: userListInfo.locationUpdateTime,\n        },\n      ])\n      break\n  }\n  await overwriteListMusics(listInfoFull.id, listInfoFull.list.map(m => fixNewMusicInfoQuality(m)))\n}\n/**\n * 覆盖单个列表\n * @param listInfo\n * @param musics\n */\nexport const createList = 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 createUserList(position < 0 ? listState.userList.length : position, [\n    {\n      id,\n      name: name ?? 'list',\n      source,\n      sourceListId,\n      locationUpdateTime: position < 0 ? null : Date.now(),\n    },\n  ])\n  if (list) await addListMusics(id, list, settingState.setting['list.addMusicLocationType'])\n}\n\n/**\n * 设置当前激活的歌曲列表\n * @param id\n */\nexport const setActiveList = (id: string) => {\n  if (listState.activeListId == id) return\n  listAction.setActiveList(id)\n  saveListPrevSelectId(id)\n}\n\n/**\n * 设置歌曲列表\n */\nexport const setUserList = (lists: LX.List.UserListInfo[]) => {\n  listAction.setUserLists(lists)\n}\n\n/**\n * 设置临时列表内歌曲\n * @param id\n * @param list\n */\nexport const setTempList = async(id: string, list: LX.Music.MusicInfoOnline[]) => {\n  await overwriteListMusics(LIST_IDS.TEMP, list)\n  listAction.setTempListMeta({ id })\n}\n\n\nexport const setFetchingListStatus = (id: string, status: boolean) => {\n  listAction.setFetchingListStatus(id, status)\n}\n\n\nexport { getUserLists, getListMusics } from '@/utils/listManage'\n\n"
  },
  {
    "path": "src/core/lyric.ts",
    "content": "import {\n  play as lrcPlay,\n  setLyric as lrcSetLyric,\n  pause as lrcPause,\n  setPlaybackRate as lrcSetPlaybackRate,\n  toggleTranslation as lrcToggleTranslation,\n  toggleRoma as lrcToggleRoma,\n  init as lrcInit,\n} from '@/plugins/lyric'\nimport {\n  playDesktopLyric,\n  setDesktopLyric,\n  pauseDesktopLyric,\n  setDesktopLyricPlaybackRate,\n  toggleDesktopLyricTranslation,\n  toggleDesktopLyricRoma,\n} from '@/core/desktopLyric'\nimport { getPosition } from '@/plugins/player'\nimport playerState from '@/store/player/state'\n// import settingState from '@/store/setting/state'\n\n/**\n * init lyric\n */\nexport const init = async() => {\n  return lrcInit()\n}\n\n/**\n * set lyric\n * @param lyric lyric str\n * @param translation lyric translation\n */\nconst handleSetLyric = async(lyric: string, translation = '', romalrc = '') => {\n  lrcSetLyric(lyric, translation, romalrc)\n  await setDesktopLyric(lyric, translation, romalrc)\n}\n\n/**\n * play lyric\n * @param time play time\n */\nexport const handlePlay = (time: number) => {\n  lrcPlay(time)\n  void playDesktopLyric(time)\n}\n\n/**\n * pause lyric\n */\nexport const pause = () => {\n  lrcPause()\n  void pauseDesktopLyric()\n}\n\n/**\n * stop lyric\n */\nexport const stop = () => {\n  void handleSetLyric('')\n}\n\n/**\n * set playback rate\n * @param playbackRate playback rate\n */\nexport const setPlaybackRate = async(playbackRate: number) => {\n  lrcSetPlaybackRate(playbackRate)\n  await setDesktopLyricPlaybackRate(playbackRate)\n  if (playerState.isPlay) {\n    setTimeout(() => {\n      void getPosition().then((position) => {\n        handlePlay(position * 1000)\n      })\n    })\n  }\n}\n\n/**\n * toggle show translation\n * @param isShowTranslation is show translation\n */\nexport const toggleTranslation = async(isShowTranslation: boolean) => {\n  lrcToggleTranslation(isShowTranslation)\n  await toggleDesktopLyricTranslation(isShowTranslation)\n  if (playerState.isPlay) play()\n}\n\n/**\n * toggle show roma lyric\n * @param isShowLyricRoma is show roma lyric\n */\nexport const toggleRoma = async(isShowLyricRoma: boolean) => {\n  lrcToggleRoma(isShowLyricRoma)\n  await toggleDesktopLyricRoma(isShowLyricRoma)\n  if (playerState.isPlay) play()\n}\n\nexport const play = () => {\n  void getPosition().then((position) => {\n    handlePlay(position * 1000)\n  })\n}\n\n\nexport const setLyric = async() => {\n  if (!playerState.musicInfo.id) return\n  if (playerState.musicInfo.lrc) {\n    let tlrc = ''\n    let rlrc = ''\n    if (playerState.musicInfo.tlrc) tlrc = playerState.musicInfo.tlrc\n    if (playerState.musicInfo.rlrc) rlrc = playerState.musicInfo.rlrc\n    await handleSetLyric(playerState.musicInfo.lrc, tlrc, rlrc)\n  }\n\n  if (playerState.isPlay) play()\n}\n"
  },
  {
    "path": "src/core/music/download.ts",
    "content": "// import { store } from '@/store'\n// 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'\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, appSetting['download.savePath'])\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, appSetting['download.savePath'])\n    // if (path) {\n    //   const pic = await global.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, appSetting['download.savePath'])\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/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/core/music/local.ts",
    "content": "\nimport { saveLyric, saveMusicUrl } from '@/utils/data'\nimport { updateListMusics } from '@/core/list'\nimport {\n  buildLyricInfo,\n  getCachedLyricInfo,\n  getOnlineOtherSourceLyricByLocal,\n  getOnlineOtherSourceLyricInfo,\n  getOnlineOtherSourceMusicUrl,\n  getOnlineOtherSourceMusicUrlByLocal,\n  getOnlineOtherSourcePicByLocal,\n  getOnlineOtherSourcePicUrl,\n  getOtherSource,\n} from './utils'\nimport { getLocalFilePath } from '@/utils/music'\nimport { readLyric, readPic } from '@/utils/localMediaMetadata'\nimport { stat } from '@/utils/fs'\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 = (await stat(musicInfo.meta.filePath).catch(() => ({ name: null }))).name ?? 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  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n  allowToggleSource?: boolean\n}): Promise<string> => {\n  if (!isRefresh) {\n    const path = await getLocalFilePath(musicInfo)\n    // console.log(path)\n    if (path) return 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, skipFilePic, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoLocal\n  listId?: string | null\n  isRefresh: boolean\n  skipFilePic?: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<string> => {\n  if (!isRefresh && !skipFilePic) {\n    let pic = await readPic(musicInfo.meta.filePath).catch(() => null)\n    if (pic) {\n      if (pic.startsWith('/')) pic = `file://${pic}`\n      return pic\n    }\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 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\nconst getMusicFileLyric = async(filePath: string) => {\n  const lyric = await readLyric(filePath).catch(() => null)\n  if (!lyric) return null\n  return parseLyric(lyric)\n}\nexport const getLyricInfo = async({ musicInfo, isRefresh, skipFileLyric, onToggleSource = () => {} }: {\n  musicInfo: LX.Music.MusicInfoLocal\n  skipFileLyric?: boolean\n  isRefresh: boolean\n  onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void\n}): Promise<LX.Player.LyricInfo> => {\n  if (!isRefresh && !skipFileLyric) {\n    // const lyricInfo = await getCachedLyricInfo(musicInfo)\n    // if (lyricInfo?.rawlrcInfo.lyric && lyricInfo.lyric != lyricInfo.rawlrcInfo.lyric) {\n    //   // 存在已编辑歌词\n    //   return buildLyricInfo(lyricInfo)\n    // }\n\n    // 尝试读取文件内歌词\n    const rawlrcInfo = await getMusicFileLyric(musicInfo.meta.filePath)\n    if (rawlrcInfo) return buildLyricInfo(rawlrcInfo)\n\n    const lyricInfo = await getCachedLyricInfo(musicInfo)\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/core/music/online.ts",
    "content": "import {\n  saveLyric,\n  saveMusicUrl,\n  getMusicUrl as getStoreMusicUrl,\n} from '@/utils/data'\nimport { updateListMusics } from '@/core/list'\nimport settingState from '@/store/setting/state'\n\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(settingState.setting['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/core/music/utils.ts",
    "content": "import musicSdk, { findMusic } from '@/utils/musicSdk'\nimport {\n  // getOtherSource as getOtherSourceFromStore,\n  // saveOtherSource as saveOtherSourceFromStore,\n  getMusicUrl as getStoreMusicUrl,\n  getPlayerLyric as getStoreLyric,\n} from '@/utils/data'\nimport { langS2T, toNewMusicInfo, toOldMusicInfo } from '@/utils'\nimport { assertApiSupport } from '@/utils/tools'\nimport settingState from '@/store/setting/state'\nimport { requestMsg } from '@/utils/message'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { apis } from '@/utils/musicSdk/api-source'\n\n\nconst getOtherSourcePromises = new Map()\nexport const existTimeExp = /\\[\\d{1,2}:.*\\d{1,4}\\]/\nconst otherSourceCache = new Map<LX.Music.MusicInfo | LX.Download.ListItem, LX.Music.MusicInfoOnline[]>()\n\nexport const getOtherSource = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListItem, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {\n  // if (!isRefresh) {\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 | number = BackgroundTimer.setTimeout(() => {\n      timeout = null\n      reject(new Error('find music timeout'))\n    }, 12_000)\n    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) BackgroundTimer.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 (!settingState.setting['player.isS2t']) {\n    // @ts-expect-error\n    if (lyricInfo.rawlrcInfo) return lyricInfo\n    return { ...lyricInfo, rawlrcInfo: { ...lyricInfo } }\n  }\n\n  if (settingState.setting['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 = {}\n  if (existTimeExp.test(lrcInfo.lyric) && 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) {\n    //     case 'kg':\n    //     case 'kw':\n    //     case 'mg':\n    //       break\n    //     default:\n    //       return lrcInfo\n    //   }\n    // } else\n    if (lrcInfo.rlyric == null) {\n      if (!['wy', 'kg'].includes(musicInfo.source)) return lrcInfo\n    } else 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 global.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 global.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 global.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 = global.lx.qualityList[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 global.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(settingState.setting['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(global.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 global.lx.apiInitPromise[0]) throw new Error('source init failed')\n  // console.log(musicInfo.source)\n  const targetQuality = quality ?? getPlayQuality(settingState.setting['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.length)\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(global.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.length)\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(global.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  return reqPromise.then(async(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  return reqPromise.then(async(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.length)\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/core/player/playInfo.ts",
    "content": "import playerActions from '@/store/player/action'\nimport playerState from '@/store/player/state'\n\nimport { getListMusicSync } from '@/utils/listManage'\nimport { setProgress } from '@/core/player/progress'\nimport { LIST_IDS } from '@/config/constant'\n\n\nexport const setMusicInfo = (musicInfo: Partial<LX.Player.MusicInfo>) => {\n  playerActions.setMusicInfo(musicInfo)\n}\n\nexport const setLoadErrorPicUrl = (url: string) => {\n  playerActions.setLoadErrorPicUrl(url)\n}\n\nexport const setLastLyric = (lrc?: string) => {\n  playerActions.setLastLyric(lrc)\n}\n\nexport const setPlayListId = (listId: string | null) => {\n  playerActions.setPlayListId(listId)\n}\n\n\n/**\n * 更新播放位置\n * @returns 播放位置\n */\nexport const updatePlayIndex = () => {\n  const indexInfo = getPlayIndex(playerState.playMusicInfo.listId, playerState.playMusicInfo.musicInfo, playerState.playMusicInfo.isTempPlay)\n  // console.log('indexInfo', indexInfo)\n  playerActions.updatePlayIndex(indexInfo.playIndex, indexInfo.playerPlayIndex)\n  return indexInfo\n}\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 { playInfo } = playerState\n  const playerList = getListMusicSync(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 = getListMusicSync(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  playerActions.setPlayMusicInfo(listId, musicInfo, isTempPlay)\n  setPlayerMusicInfo(musicInfo)\n\n  setProgress(0, 0)\n\n  if (musicInfo == null) {\n    playerActions.updatePlayIndex(-1, -1)\n    setPlayListId(null)\n  } else {\n    const { playIndex, playerPlayIndex } = getPlayIndex(listId, musicInfo, isTempPlay)\n\n    playerActions.updatePlayIndex(playIndex, playerPlayIndex)\n    global.app_event.musicToggled()\n  }\n}\n\nexport const getList = (listId: string | null): LX.Music.MusicInfo[] | LX.Download.ListItem[] => {\n  // return listId == LIST_ID_DOWNLOAD ? downloadList : getListMusicSync(listId)\n  return listId == LIST_IDS.DOWNLOAD ? [] : getListMusicSync(listId)\n}\n"
  },
  {
    "path": "src/core/player/playStatus.ts",
    "content": "import playerActions from '@/store/player/action'\nimport playerState from '@/store/player/state'\n\n\nexport const setIsPlay = (val: boolean) => {\n  if (playerState.isPlay == val) return\n  playerActions.setIsPlay(val)\n}\n\n\nexport const setStatusText = (val: string) => {\n  if (playerState.statusText == val) return\n  playerActions.setStatusText(val)\n}\n"
  },
  {
    "path": "src/core/player/playedList.ts",
    "content": "import playerActions from '@/store/player/action'\n\n\n/**\n * 将歌曲添加到已播放列表\n * @param playMusicInfo playMusicInfo对象\n */\nexport const addPlayedList = (playMusicInfo: LX.Player.PlayMusicInfo) => {\n  playerActions.addPlayedList(playMusicInfo)\n}\n/**\n * 将歌曲从已播放列表移除\n * @param index 歌曲位置\n */\nexport const removePlayedList = (index: number) => {\n  playerActions.removePlayedList(index)\n}\n/**\n * 清空已播放列表\n */\nexport const clearPlayedList = () => {\n  playerActions.clearPlayedList()\n}\n"
  },
  {
    "path": "src/core/player/player.ts",
    "content": "import { isInitialized, initial as playerInitial, isEmpty, setPause, setPlay, setResource, setStop, initTrackInfo } from '@/plugins/player'\nimport {\n  setStatusText,\n} from '@/core/player/playStatus'\nimport playerState from '@/store/player/state'\nimport settingState from '@/store/setting/state'\nimport {\n  getList,\n  setPlayMusicInfo,\n  setMusicInfo,\n  setPlayListId,\n} from '@/core/player/playInfo'\nimport {\n  clearPlayedList,\n  addPlayedList,\n  removePlayedList,\n} from '@/core/player/playedList'\nimport {\n  clearTempPlayeList,\n  removeTempPlayList,\n} from '@/core/player/tempPlayList'\nimport { getMusicUrl, getPicPath, getLyricInfo } from '@/core/music'\nimport { requestMsg } from '@/utils/message'\nimport { getRandom } from '@/utils/common'\nimport { filterList } from './utils'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { checkIgnoringBatteryOptimization, checkNotificationPermission, debounceBackgroundTimer } from '@/utils/tools'\nimport { LIST_IDS } from '@/config/constant'\nimport { addListMusics, removeListMusics } from '@/core/list'\nimport { addDislikeInfo } from '@/core/dislikeList'\n\n// import { checkMusicFileAvailable } from '@renderer/utils/music'\n\nconst createDelayNextTimeout = (delay: number) => {\n  let timeout: number | null\n  const clearDelayNextTimeout = () => {\n    // console.log(this.timeout)\n    if (timeout) {\n      BackgroundTimer.clearTimeout(timeout)\n      timeout = null\n    }\n  }\n\n  const addDelayNextTimeout = () => {\n    clearDelayNextTimeout()\n    timeout = BackgroundTimer.setTimeout(() => {\n      timeout = null\n      if (global.lx.isPlayedStop) return\n      console.log('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\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}\n/**\n * 检查音乐信息是否已更改\n */\nconst diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {\n  // return curMusicInfo !== playerState.playMusicInfo.musicInfo || playerState.isPlay\n  return createGettingUrlId(curMusicInfo) != global.lx.gettingUrlId || curMusicInfo.id != playerState.playMusicInfo.musicInfo?.id || playerState.isPlay\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    setStatusText(global.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  setStatusText(global.i18n.t('player__getting_url'))\n  addLoadTimeout()\n\n  // const type = getPlayType(settingState.setting['player.isPlayHighQuality'], 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        setStatusText(global.i18n.t('toggle_source_try'))\n      },\n    })\n  }).then(url => {\n    if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null\n\n    return url\n  }).catch(async err => {\n    // console.log('err', err.message)\n    if (global.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  // addLoadTimeout()\n  if (!diffCurrentMusicInfo(musicInfo)) return\n  if (cancelDelayRetry) cancelDelayRetry()\n  global.lx.gettingUrlId = createGettingUrlId(musicInfo)\n  void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {\n    if (!url) return\n    setResource(musicInfo, url, playerState.progress.nowPlayTime)\n  }).catch((err: any) => {\n    console.log(err)\n    setStatusText(err.message as string)\n    global.app_event.error()\n    addDelayNextTimeout()\n  }).finally(() => {\n    if (musicInfo === playerState.playMusicInfo.musicInfo) {\n      global.lx.gettingUrlId = ''\n      clearLoadTimeout()\n    }\n  })\n}\n\n// 恢复上次播放的状态\nconst handleRestorePlay = async(restorePlayInfo: LX.Player.SavedPlayInfo) => {\n  const musicInfo = playerState.playMusicInfo.musicInfo\n  if (!musicInfo) return\n\n  setTimeout(() => {\n    global.app_event.setProgress(settingState.setting['player.isSavePlayTime'] ? restorePlayInfo.time : 0, restorePlayInfo.maxTime)\n  })\n\n  const playMusicInfo = playerState.playMusicInfo\n\n  void initTrackInfo(musicInfo, playerState.musicInfo)\n\n  void getPicPath({ musicInfo, listId: playMusicInfo.listId }).then((url: string) => {\n    if (\n      musicInfo.id != playMusicInfo.musicInfo?.id ||\n      playerState.musicInfo.pic == url ||\n      playerState.loadErrorPicUrl == url\n    ) return\n    setMusicInfo({ pic: url })\n    global.app_event.picUpdated()\n  })\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    global.app_event.lyricUpdated()\n  }).catch((err) => {\n    console.log(err)\n    if (musicInfo.id != playMusicInfo.musicInfo?.id) return\n    setStatusText(global.i18n.t('lyric__load_error'))\n  })\n\n  if (settingState.setting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList(playMusicInfo as LX.Player.PlayMusicInfo)\n}\n\n\nconst debouncePlay = debounceBackgroundTimer((musicInfo: LX.Player.PlayMusic) => {\n  setMusicUrl(musicInfo)\n\n  void getPicPath({ musicInfo, listId: playerState.playMusicInfo.listId }).then((url: string) => {\n    if (\n      musicInfo.id != playerState.playMusicInfo.musicInfo?.id ||\n      playerState.musicInfo.pic == url ||\n      playerState.loadErrorPicUrl == url) return\n    setMusicInfo({ pic: url })\n    global.app_event.picUpdated()\n  })\n\n  void getLyricInfo({ musicInfo }).then((lyricInfo) => {\n    if (musicInfo.id != playerState.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    global.app_event.lyricUpdated()\n  }).catch((err) => {\n    console.log(err)\n    if (musicInfo.id != playerState.playMusicInfo.musicInfo?.id) return\n    setStatusText(global.i18n.t('lyric__load_error'))\n  })\n}, 200)\n\n// 处理音乐播放\nconst handlePlay = async() => {\n  if (!isInitialized()) {\n    await checkNotificationPermission()\n    void checkIgnoringBatteryOptimization()\n    await playerInitial({\n      volume: settingState.setting['player.volume'],\n      playRate: settingState.setting['player.playbackRate'],\n      cacheSize: settingState.setting['player.cacheSize'] ? parseInt(settingState.setting['player.cacheSize']) : 0,\n      isHandleAudioFocus: settingState.setting['player.isHandleAudioFocus'],\n      isEnableAudioOffload: settingState.setting['player.isEnableAudioOffload'],\n    })\n  }\n\n  global.lx.isPlayedStop &&= false\n  resetRandomNextMusicInfo()\n\n  if (global.lx.restorePlayInfo) {\n    void handleRestorePlay(global.lx.restorePlayInfo)\n    global.lx.restorePlayInfo = null\n    return\n  }\n\n  const playMusicInfo = playerState.playMusicInfo\n  const musicInfo = playMusicInfo.musicInfo\n\n  if (!musicInfo) return\n\n  await setStop()\n  global.app_event.pause()\n\n  clearDelayNextTimeout()\n  clearLoadTimeout()\n\n\n  if (settingState.setting['player.togglePlayMethod'] == 'random' && !playMusicInfo.isTempPlay) addPlayedList(playMusicInfo as LX.Player.PlayMusicInfo)\n\n  debouncePlay(musicInfo)\n}\n\n/**\n * 播放列表内歌曲\n * @param listId 列表id\n * @param id 歌曲id\n */\nexport const playListById = async(listId: string, id: string) => {\n  const prevListId = playerState.playInfo.playerListId\n  setPlayListId(listId)\n  const musicInfo = getList(listId).find(m => m.id == id)\n  if (!musicInfo) return\n  setPlayMusicInfo(listId, musicInfo)\n  if (settingState.setting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList()\n  clearTempPlayeList()\n  await handlePlay()\n}\n\n/**\n * 播放列表内歌曲\n * @param listId 列表id\n * @param index 播放的歌曲位置\n */\nexport const playList = async(listId: string, index: number) => {\n  const prevListId = playerState.playInfo.playerListId\n  setPlayListId(listId)\n  setPlayMusicInfo(listId, getList(listId)[index])\n  if (settingState.setting['player.isAutoCleanPlayedList'] || prevListId != listId) clearPlayedList()\n  clearTempPlayeList()\n  await handlePlay()\n}\n\nconst handleToggleStop = async() => {\n  await stop()\n  setTimeout(() => {\n    setPlayMusicInfo(null, null)\n  })\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 (playerState.tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲\n    const playMusicInfo = playerState.tempPlayList[0]\n    return playMusicInfo\n  }\n\n  if (playerState.playMusicInfo.musicInfo == null) return null\n\n  if (randomNextMusicInfo.info) return randomNextMusicInfo.info\n\n  const playMusicInfo = playerState.playMusicInfo\n  const playInfo = playerState.playInfo\n  // console.log(playInfo.playerListId)\n  const currentListId = playInfo.playerListId\n  if (!currentListId) return null\n  const currentList = getList(currentListId)\n\n  const playedList = playerState.playedList\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 = settingState.setting['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 = async(playMusicInfo: LX.Player.PlayMusicInfo) => {\n  setPlayMusicInfo(playMusicInfo.listId, playMusicInfo.musicInfo, playMusicInfo.isTempPlay)\n  await handlePlay()\n}\n/**\n * 下一曲\n * @param isAutoToggle 是否自动切换\n * @returns\n */\nexport const playNext = async(isAutoToggle = false): Promise<void> => {\n  if (playerState.tempPlayList.length) { // 如果稍后播放列表存在歌曲则直接播放改列表的歌曲\n    const playMusicInfo = playerState.tempPlayList[0]\n    removeTempPlayList(0)\n    await handlePlayNext(playMusicInfo)\n    return\n  }\n\n  const playMusicInfo = playerState.playMusicInfo\n  const playInfo = playerState.playInfo\n  if (playMusicInfo.musicInfo == null) return handleToggleStop()\n\n  // console.log(playInfo.playerListId)\n  const currentListId = playInfo.playerListId\n  if (!currentListId) return handleToggleStop()\n  const currentList = getList(currentListId)\n\n  const playedList = playerState.playedList\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      await handlePlayNext(playedList[index])\n      return\n    }\n  }\n  if (randomNextMusicInfo.info) {\n    await 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) return handleToggleStop()\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 = settingState.setting['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      return\n  }\n  if (nextIndex < 0) return\n\n  await 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  const playMusicInfo = playerState.playMusicInfo\n  if (playMusicInfo.musicInfo == null) return handleToggleStop()\n  const playInfo = playerState.playInfo\n\n  const currentListId = playInfo.playerListId\n  if (!currentListId) return handleToggleStop()\n  const currentList = getList(currentListId)\n\n  const playedList = playerState.playedList\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      await 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) return handleToggleStop()\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 = settingState.setting['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\n  await handlePlayNext({\n    musicInfo: filteredList[nextIndex],\n    listId: currentListId,\n    isTempPlay: false,\n  })\n}\n\n/**\n * 恢复播放\n */\nexport const play = () => {\n  if (playerState.playMusicInfo.musicInfo == null) return\n  if (isEmpty()) {\n    if (createGettingUrlId(playerState.playMusicInfo.musicInfo) != global.lx.gettingUrlId) setMusicUrl(playerState.playMusicInfo.musicInfo)\n    return\n  }\n  void setPlay()\n}\n\n/**\n * 暂停播放\n */\nexport const pause = async() => {\n  await setPause()\n}\n\n/**\n * 停止播放\n */\nexport const stop = async() => {\n  await setStop()\n  setTimeout(() => {\n    global.app_event.stop()\n  })\n}\n\n/**\n * 播放、暂停播放切换\n */\nexport const togglePlay = () => {\n  global.lx.isPlayedStop &&= false\n  if (playerState.isPlay) {\n    void pause()\n  } else {\n    play()\n  }\n}\n\n/**\n * 收藏当前播放的歌曲\n */\nexport const collectMusic = () => {\n  if (!playerState.playMusicInfo.musicInfo) return\n  void addListMusics(LIST_IDS.LOVE, [\n    'progress' in playerState.playMusicInfo.musicInfo\n      ? playerState.playMusicInfo.musicInfo.metadata.musicInfo\n      : playerState.playMusicInfo.musicInfo,\n  ], settingState.setting['list.addMusicLocationType'])\n}\n\n/**\n * 取消收藏当前播放的歌曲\n */\nexport const uncollectMusic = () => {\n  if (!playerState.playMusicInfo.musicInfo) return\n  void removeListMusics(LIST_IDS.LOVE, [\n    'progress' in playerState.playMusicInfo.musicInfo\n      ? playerState.playMusicInfo.musicInfo.metadata.musicInfo.id\n      : playerState.playMusicInfo.musicInfo.id,\n  ])\n}\n\n/**\n * 不喜欢当前播放的歌曲\n */\nexport const dislikeMusic = async() => {\n  if (!playerState.playMusicInfo.musicInfo) return\n  const minfo = 'progress' in playerState.playMusicInfo.musicInfo ? playerState.playMusicInfo.musicInfo.metadata.musicInfo : playerState.playMusicInfo.musicInfo\n  await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }])\n  await playNext(true)\n}\n\n"
  },
  {
    "path": "src/core/player/progress.ts",
    "content": "import playerActions from '@/store/player/action'\n\nexport const setNowPlayTime = (time: number) => {\n  playerActions.setNowPlayTime(time)\n}\n\nexport const setMaxplayTime = (time: number) => {\n  playerActions.setMaxplayTime(time)\n}\n\nexport const setProgress = (currentTime: number, totalTime: number) => {\n  playerActions.setProgress(currentTime, totalTime)\n}\n\n"
  },
  {
    "path": "src/core/player/tempPlayList.ts",
    "content": "import playerActions from '@/store/player/action'\nimport playerState from '@/store/player/state'\nimport { playNext } from './player'\n\n\n/**\n * 添加歌曲到稍后播放列表\n * @param list 歌曲列表\n */\nexport const addTempPlayList = (list: LX.Player.TempPlayListItem[]) => {\n  playerActions.addTempPlayList(list)\n  if (!playerState.playMusicInfo.musicInfo) void playNext()\n}\n/**\n * 从稍后播放列表移除歌曲\n * @param index 歌曲位置\n */\nexport const removeTempPlayList = (index: number) => {\n  playerActions.removeTempPlayList(index)\n}\n/**\n * 清空稍后播放列表\n */\nexport const clearTempPlayeList = () => {\n  playerActions.clearTempPlayeList()\n}\n"
  },
  {
    "path": "src/core/player/timeoutExit.ts",
    "content": "import { useEffect, useState } from 'react'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { exitApp } from '@/core/common'\nimport playerState from '@/store/player/state'\nimport settingState from '@/store/setting/state'\n\ntype Hook = (time: number, isPlayedStop: boolean) => void\n\nconst timeoutTools = {\n  bgTimeout: null as number | null,\n  timeout: null as NodeJS.Timer | null,\n  startTime: 0,\n  time: -1,\n  timeHooks: [] as Hook[],\n  exit() {\n    if (settingState.setting['player.timeoutExitPlayed'] && playerState.isPlay) {\n      global.lx.isPlayedStop = true\n      this.callHooks()\n    } else {\n      exitApp('Timeout Exit')\n    }\n  },\n  getTime() {\n    return Math.max(this.time - Math.round((performance.now() - this.startTime) / 1000), -1)\n  },\n  callHooks() {\n    const time = this.getTime()\n    for (const hook of this.timeHooks) {\n      hook(time, global.lx.isPlayedStop)\n    }\n  },\n  clearTimeout() {\n    if (!this.bgTimeout) return\n    BackgroundTimer.clearTimeout(this.bgTimeout)\n    clearInterval(this.timeout!)\n    this.bgTimeout = null\n    this.timeout = null\n    this.time = -1\n    this.callHooks()\n  },\n  start(time: number) {\n    this.clearTimeout()\n    this.time = time\n    this.startTime = performance.now()\n    this.bgTimeout = BackgroundTimer.setTimeout(() => {\n      this.clearTimeout()\n      this.exit()\n    }, time * 1000)\n    this.timeout = setInterval(() => {\n      this.callHooks()\n    }, 1000)\n  },\n  addTimeHook(hook: Hook) {\n    this.timeHooks.push(hook)\n    hook(this.getTime(), global.lx.isPlayedStop)\n  },\n  removeTimeHook(hook: Hook) {\n    this.timeHooks.splice(this.timeHooks.indexOf(hook), 1)\n  },\n}\n\n\nexport const startTimeoutExit = (time: number) => {\n  timeoutTools.start(time)\n}\nexport const stopTimeoutExit = () => {\n  timeoutTools.clearTimeout()\n}\n\nexport const getTimeoutExitTime = () => {\n  return timeoutTools.time\n}\n\nexport const useTimeoutExitTimeInfo = () => {\n  const [info, setInfo] = useState({ time: 0, isPlayedStop: false })\n  useEffect(() => {\n    const hook: Hook = (time, isPlayedStop) => {\n      setInfo({ time, isPlayedStop })\n    }\n    timeoutTools.addTimeHook(hook)\n    return () => { timeoutTools.removeTimeHook(hook) }\n  }, [setInfo])\n\n  return info\n}\n\nexport const onTimeUpdate = (handler: Hook) => {\n  timeoutTools.addTimeHook(handler)\n\n  return () => {\n    timeoutTools.removeTimeHook(handler)\n  }\n}\n\n\nexport const cancelTimeoutExit = () => {\n  global.lx.isPlayedStop = false\n  timeoutTools.callHooks()\n}\n"
  },
  {
    "path": "src/core/player/utils.ts",
    "content": "import { clearPlayedList } from './playedList'\nimport { SPLIT_CHAR } from '@/config/constant'\nimport { state } from '@/store/dislikeList'\n\n/**\n * 过滤列表中已播放的歌曲\n */\nexport const filterMusicList = ({ playedList, listId, list, playerMusicInfo, dislikeInfo, isNext }: {\n  /**\n   * 已播放列表\n   */\n  playedList: LX.Player.PlayMusicInfo[] | readonly 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   */\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\n/**\n * 过滤列表中已播放的歌曲\n */\nexport const filterList = async({ playedList, listId, list, playerMusicInfo, isNext }: {\n  playedList: LX.Player.PlayMusicInfo[] | readonly 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 } = filterMusicList({\n    listId,\n    list,\n    playedList,\n    // savePath: global.lx.setting['download.savePath'],\n    playerMusicInfo,\n    dislikeInfo: { names: state.dislikeInfo.names, musicNames: state.dislikeInfo.musicNames, singerNames: state.dislikeInfo.singerNames },\n    isNext,\n  })\n\n  if (!filteredList.length && playedList.length) {\n    clearPlayedList()\n    return { filteredList: canPlayList, playerIndex }\n  }\n  return { filteredList, playerIndex }\n}\n\n"
  },
  {
    "path": "src/core/search/music.ts",
    "content": "import searchMusicState, { type Source } from '@/store/search/music/state'\nimport searchMusicActions, { type SearchResult } from '@/store/search/music/action'\nimport musicSdk from '@/utils/musicSdk'\n\nexport const setSource: typeof searchMusicActions['setSource'] = (source) => {\n  searchMusicActions.setSource(source)\n}\nexport const setSearchText: typeof searchMusicActions['setSearchText'] = (text) => {\n  searchMusicActions.setSearchText(text)\n}\nexport const setListInfo: typeof searchMusicActions.setListInfo = (result, id, page) => {\n  return searchMusicActions.setListInfo(result, id, page)\n}\n\nexport const clearListInfo: typeof searchMusicActions.clearListInfo = (source) => {\n  searchMusicActions.clearListInfo(source)\n}\n\n\nexport const search = async(text: string, page: number, sourceId: Source): Promise<LX.Music.MusicInfoOnline[]> => {\n  const listInfo = searchMusicState.listInfos[sourceId]!\n  if (!text) return []\n  const key = `${page}__${text}`\n  if (sourceId == 'all') {\n    listInfo.key = key\n    let task = []\n    for (const source of searchMusicState.sources) {\n      if (source == 'all') continue\n      task.push(((musicSdk[source]?.musicSearch.search(text, page, searchMusicState.listInfos.all.limit) as Promise<SearchResult>) ?? 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      setSearchText(text)\n      setSource(sourceId)\n      return setListInfo(results, page, text)\n    })\n  } else {\n    if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list\n    listInfo.key = key\n    return (musicSdk[sourceId]?.musicSearch.search(text, page, listInfo.limit).then((data: SearchResult) => {\n      if (key != listInfo.key) return []\n      return setListInfo(data, page, text)\n    }) ?? Promise.reject(new Error('source not found: ' + sourceId))).catch((err: any) => {\n      if (listInfo.list.length && page == 1) clearListInfo(sourceId)\n      throw err\n    })\n  }\n}\n\n"
  },
  {
    "path": "src/core/search/search.ts",
    "content": "import searchState from '@/store/search/state'\nimport searchActions from '@/store/search/action'\nimport { getSearchHistory as getSearchHistoryFromStore, saveSearchHistory } from '@/utils/data'\nimport settingState from '@/store/setting/state'\n\n\nexport const setSearchType: typeof searchActions['setSearchType'] = (type) => {\n  searchActions.setSearchType(type)\n}\nexport const setSearchText: typeof searchActions['setSearchText'] = (text) => {\n  searchActions.setSearchText(text)\n}\nexport const setTipListInfo: typeof searchActions['setTipListInfo'] = (text, source) => {\n  searchActions.setTipListInfo(text, source)\n}\nexport const setTipList: typeof searchActions['setTipList'] = (list) => {\n  searchActions.setTipList(list)\n}\n\nexport const getSearchHistory = async() => {\n  if (!searchState.historyList.length) searchActions.setHistoryWord(await getSearchHistoryFromStore())\n  return searchState.historyList\n}\nexport const addHistoryWord = async(word: string) => {\n  if (!settingState.setting['search.isShowHistorySearch'] || !word) return\n  if (!searchState.historyList.length) searchActions.setHistoryWord(await getSearchHistoryFromStore())\n  const list = searchActions.addHistoryWord(word)\n  if (!list) return\n  void saveSearchHistory(list)\n}\nexport const removeHistoryWord = (index: number) => {\n  const list = searchActions.removeHistoryWord(index)\n  void saveSearchHistory(list)\n}\nexport const clearHistoryList = () => {\n  const list = searchActions.clearHistoryList()\n  void saveSearchHistory(list)\n}\n\n"
  },
  {
    "path": "src/core/search/songlist.ts",
    "content": "import searchSonglistState, { type Source, type ListInfoItem } from '@/store/search/songlist/state'\nimport searchSonglistActions, { type SearchResult } from '@/store/search/songlist/action'\nimport musicSdk from '@/utils/musicSdk'\n\nexport const setSource: typeof searchSonglistActions['setSource'] = (source) => {\n  searchSonglistActions.setSource(source)\n}\nexport const setSearchText: typeof searchSonglistActions['setSearchText'] = (text) => {\n  searchSonglistActions.setSearchText(text)\n}\nconst setListInfo: typeof searchSonglistActions.setListInfo = (result, page, text) => {\n  return searchSonglistActions.setListInfo(result, page, text)\n}\n\nexport const clearListInfo: typeof searchSonglistActions.clearListInfo = (source) => {\n  searchSonglistActions.clearListInfo(source)\n}\n\n\nexport const search = async(text: string, page: number, sourceId: Source): Promise<ListInfoItem[]> => {\n  const listInfo = searchSonglistState.listInfos[sourceId]!\n  // if (!text) return []\n  const key = `${page}__${sourceId}__${text}`\n  if (listInfo.key == key && listInfo.list.length) return listInfo.list\n  if (sourceId == 'all') {\n    listInfo.key = key\n    let task = []\n    for (const source of searchSonglistState.sources) {\n      if (source == 'all' || (page > 1 && page > (searchSonglistState.maxPages[source]!))) continue\n      task.push(((musicSdk[source]?.songList.search(text, page, searchSonglistState.listInfos.all.limit) as Promise<SearchResult>) ?? Promise.reject(new Error('source not found: ' + source))).catch((error: any) => {\n        console.log(error)\n        return {\n          list: [],\n          total: 0,\n          limit: searchSonglistState.listInfos.all.limit,\n          source,\n        }\n      }))\n    }\n    return Promise.all(task).then((results: SearchResult[]) => {\n      if (key != listInfo.key) return []\n      setSearchText(text)\n      setSource(sourceId)\n      return setListInfo(results, page, text)\n    })\n  } else {\n    if (listInfo?.key == key && listInfo?.list.length) return listInfo?.list\n    listInfo.key = key\n    return ((musicSdk[sourceId]?.songList.search(text, page, listInfo.limit) as Promise<SearchResult>).then((data: SearchResult) => {\n      if (key != listInfo.key) return []\n      return setListInfo(data, page, text)\n    }) ?? Promise.reject(new Error('source not found: ' + sourceId))).catch((err: any) => {\n      if (listInfo.list.length && page == 1) clearListInfo(sourceId)\n      throw err\n    })\n  }\n}\n"
  },
  {
    "path": "src/core/songlist.ts",
    "content": "import songlistState, { type TagInfo, type ListDetailInfo, type ListInfo } from '@/store/songlist/state'\nimport songlistActions from '@/store/songlist/action'\nimport { deduplicationList, toNewMusicInfo } from '@/utils'\nimport musicSdk from '@/utils/musicSdk'\n\n\ninterface DetailPageCache { data: ListDetailInfo, sourcePage: number }\ntype LimitDetailCache = Map<string, DetailPageCache | ListDetailInfo['list']>\ntype CacheValue = LimitDetailCache | ListInfo\n\nconst cache = new Map<string, CacheValue>()\nconst LIST_LOAD_LIMIT = 30\n\n\n/**\n * 获取排序列表\n * @param source\n * @returns\n */\nexport const getSortList = (source: LX.OnlineSource) => {\n  return songlistState.sortList[source]!\n}\n\n/**\n * 获取标签列表\n * @param source\n * @returns\n */\nexport const getTags = async<T extends LX.OnlineSource>(source: T) => {\n  if (songlistState.tags[source]) return songlistState.tags[source] as TagInfo<T>\n  const info = await (musicSdk[source]?.songList.getTags() as Promise<TagInfo<T>>)\n  songlistActions.setTags(info, source)\n  return info\n}\n\n/**\n * 设置列表加载加载前的基本信息（用于加载失败后的重新加载）\n * @param source\n * @param tagId\n * @param sortId\n */\nexport const setListInfo: typeof songlistActions.setListInfo = (source, tagId, sortId) => {\n  clearList()\n  songlistActions.setListInfo(source, tagId, sortId)\n}\n/**\n * 设置列表信息\n * @param result\n * @param tagId\n * @param sortId\n * @param page\n * @returns\n */\nexport const setList: typeof songlistActions.setList = (result, tagId, sortId, page) => {\n  return songlistActions.setList(result, tagId, sortId, page)\n}\n\nexport const clearList = () => {\n  songlistActions.clearList()\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 getList = async(source: LX.OnlineSource, tabId: string, sortId: string, page: number, isRefresh = false): Promise<ListInfo> => {\n  let pageKey = `slist__${source}__${sortId}__${tabId}__${page}`\n\n  let listCache = cache.get(pageKey) as ListInfo\n  if (listCache) {\n    if (isRefresh) cache.delete(pageKey)\n    else return listCache\n  }\n\n  return musicSdk[source]?.songList.getList(sortId, tabId, page).then((result: ListInfo) => {\n    cache.set(pageKey, result)\n    return result\n    // if (pageKey != listInfo.key) return\n    // setList(result, tabId, sortId, page)\n  })\n}\n\n\n/**\n * 获取歌单详情内单页分页歌曲（用于在本地控制每页大小）\n * @param source 源\n * @param id 歌单id\n * @param page 页数\n * @returns\n */\nconst getListDetailLimit = async(source: LX.OnlineSource, id: string, page: number): Promise<ListDetailInfo> => {\n  const listKey = `sdetail__${source}__${id}`\n  const prevPageKey = `sdetail__${source}__${id}__${page - 1}`\n  const tempListKey = `sdetail__${source}__${id}__temp`\n\n  let listCache = cache.get(listKey) as LimitDetailCache\n  if (!listCache) cache.set(listKey, listCache = new Map())\n  let sourcePage = 0\n  {\n    const prevPageData = listCache.get(prevPageKey) as DetailPageCache\n    if (prevPageData) sourcePage = prevPageData.sourcePage\n  }\n\n  return musicSdk[source]?.songList.getListDetail(id, sourcePage + 1).then((result: ListDetailInfo) => {\n    if (listCache !== cache.get(listKey)) return\n    result.list = deduplicationList(result.list.map(m => toNewMusicInfo(m)) as LX.Music.MusicInfoOnline[])\n    let p = page\n    const tempList = listCache.get(tempListKey) as ListDetailInfo['list']\n    if (tempList) {\n      listCache.delete(tempListKey)\n      listCache.set(`sdetail__${source}__${id}__${p}`, {\n        data: {\n          ...result,\n          list: [...tempList, ...result.list.splice(0, LIST_LOAD_LIMIT - tempList.length)],\n          page: p,\n          limit: LIST_LOAD_LIMIT,\n        },\n        sourcePage,\n      })\n      p++\n    }\n    sourcePage++\n    do {\n      if (result.list.length < LIST_LOAD_LIMIT && sourcePage < Math.ceil(result.total / result.limit)) {\n        listCache.set(tempListKey, result.list.splice(0, LIST_LOAD_LIMIT))\n        break\n      }\n      listCache.set(`sdetail__${source}__${id}__${p}`, {\n        data: {\n          ...result,\n          list: result.list.splice(0, LIST_LOAD_LIMIT),\n          page: p,\n          limit: LIST_LOAD_LIMIT,\n        },\n        sourcePage,\n      })\n      p++\n    } while (result.list.length > 0)\n    return (listCache.get(`sdetail__${source}__${id}__${page}`) as DetailPageCache).data\n  }) ?? Promise.reject(new Error('source not found'))\n}\n\n/**\n * 设置列表加载加载前的基本信息（用于加载失败后的重新加载）\n * @param source\n * @param tagId\n * @param sortId\n */\nexport const setListDetailInfo: typeof songlistActions.setListDetailInfo = (source, id) => {\n  clearListDetail()\n  songlistActions.setListDetailInfo(source, id)\n}\nexport const setListDetail: typeof songlistActions.setListDetail = (result, id, page) => {\n  return songlistActions.setListDetail(result, id, page)\n}\n\nexport const clearListDetail = () => {\n  songlistActions.clearListDetail()\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  const listKey = `sdetail__${source}__${id}`\n  const pageKey = `sdetail__${source}__${id}__${page}`\n\n  let listCache = cache.get(listKey) as LimitDetailCache\n  if (!listCache || isRefresh) {\n    cache.set(listKey, listCache = new Map())\n  }\n\n  let pageCache = listCache.get(pageKey) as DetailPageCache\n  if (pageCache) return pageCache.data\n\n  return getListDetailLimit(source, id, page)\n}\n\n/**\n * 获取歌单内全部歌曲\n * @param id 歌单id\n * @param source 歌单源\n * @param isRefresh 是否跳过缓存\n * @returns\n */\nexport const getListDetailAll = async(source: LX.OnlineSource, id: string, isRefresh = false): Promise<LX.Music.MusicInfoOnline[]> => {\n  // console.log(tabId)\n  const listKey = `sdetail__${source}__${id}`\n  let listCache = cache.get(listKey) as LimitDetailCache\n  if (!listCache || isRefresh) {\n    cache.set(listKey, listCache = new Map())\n  }\n\n  const loadData = async(page: number): Promise<ListDetailInfo> => {\n    const pageKey = `sdetail__${source}__${id}__${page}`\n    let pageCache = listCache.get(pageKey) as DetailPageCache\n    if (pageCache) return pageCache.data\n    return getListDetailLimit(source, id, page)\n  }\n  return loadData(1).then(async result => {\n    if (result.total <= result.limit) return result.list\n\n    let maxPage = Math.ceil(result.total / result.limit)\n    const loadDetail = async(loadPage = 2): Promise<LX.Music.MusicInfoOnline[]> => {\n      return loadPage == maxPage\n        ? loadData(loadPage).then(result => result.list)\n        // eslint-disable-next-line @typescript-eslint/promise-function-async\n        : loadData(loadPage).then(result1 => loadDetail(++loadPage).then(result2 => [...result1.list, ...result2]))\n    }\n    return loadDetail().then(result2 => [...result.list, ...result2])\n  }).then(list => deduplicationList(list))\n}\n"
  },
  {
    "path": "src/core/sync.ts",
    "content": "import { dismissOverlay, onModalDismissed, showSyncModeModal } from '@/navigation'\nimport syncState from '@/store/sync/state'\nimport syncActions from '@/store/sync/action'\n\ntype RemoveListener = (() => void) | null\nlet removeEvent: RemoveListener\n\nexport const setSyncStatus = (status: LX.Sync.Status) => {\n  syncActions.setStatus(status)\n}\n\nexport const setSyncMessage = (message: LX.Sync.Status['message']) => {\n  syncActions.setMessage(message)\n}\n\nexport const setSyncModeComponentId = (id: string) => {\n  syncActions.setSyncModeComponentId(id)\n}\n\nconst closeSyncModeModal = () => {\n  if (syncState.syncModeComponentId) {\n    void dismissOverlay(syncState.syncModeComponentId)\n    syncActions.setSyncModeComponentId('')\n  }\n}\nexport const selectSyncMode = async<T extends keyof LX.Sync.ModeTypes>(serverName: string, type: T) => new Promise<LX.Sync.ModeTypes[T]>((resolve, reject) => {\n  removeSyncModeEvent()\n  syncActions.setServerInfo(serverName, type)\n  showSyncModeModal()\n\n  const removeListeners = () => {\n    removeListener!()\n    removeListener = null\n    removeEvent = null\n    global.app_event.off('selectSyncMode', handleSelectMode)\n  }\n\n  const handleSelectMode = ({ mode }: LX.Sync.ModeType) => {\n    removeListeners()\n    closeSyncModeModal()\n    resolve(mode as LX.Sync.ModeTypes[T])\n  }\n\n  removeEvent = () => {\n    removeListeners()\n    reject(new Error('cancel'))\n  }\n\n  global.app_event.on('selectSyncMode', handleSelectMode)\n\n  let removeListener: RemoveListener = onModalDismissed(syncState.syncModeComponentId, () => {\n    syncActions.setSyncModeComponentId('')\n    removeEvent?.()\n  })\n})\n\nexport const removeSyncModeEvent = () => {\n  if (!removeEvent) return\n  removeEvent()\n  removeEvent = null\n  closeSyncModeModal()\n}\n"
  },
  {
    "path": "src/core/syncSourceList.ts",
    "content": "// import { dateFormat } from '@/utils/common'\nimport { setListUpdateTime } from '@/utils/data'\nimport { overwriteListMusics, setFetchingListStatus } from './list'\nimport { getListDetailAll } from '@/core/songlist'\nimport { getListDetailAll as getBoardListAll } from '@/core/leaderboard'\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(source, sourceListId, 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(targetListInfo.id, list)\n  const now = Date.now()\n  void setListUpdateTime(targetListInfo.id, now)\n  // TODO\n  // setUpdateTime(targetListInfo.id, dateFormat(now))\n}\n"
  },
  {
    "path": "src/core/theme.ts",
    "content": "import themeActions from '@/store/theme/action'\nimport { getTheme } from '@/theme/themes'\nimport { updateSetting } from './common'\nimport themeState from '@/store/theme/state'\n\nexport const setShouldUseDarkColors = (shouldUseDarkColors: boolean) => {\n  themeActions.setShouldUseDarkColors(shouldUseDarkColors)\n}\n\nexport const applyTheme = (theme: LX.Theme) => {\n  themeActions.setTheme(theme)\n}\n\nexport const setTheme = (id: string) => {\n  updateSetting({ 'theme.id': id })\n  void getTheme().then(theme => {\n    if (theme.id == themeState.theme.id) return\n    applyTheme(theme)\n  })\n}\n"
  },
  {
    "path": "src/core/userApi.ts",
    "content": "import { action, state } from '@/store/userApi'\nimport { addUserApi, getUserApiScript, removeUserApi as removeUserApiFromStore, setUserApiAllowShowUpdateAlert as setUserApiAllowShowUpdateAlertFromStore } from '@/utils/data'\nimport { destroy, loadScript } from '@/utils/nativeModules/userApi'\nimport { log as writeLog } from '@/utils/log'\n\n\nexport const setUserApi = async(apiId: string) => {\n  global.lx.qualityList = {}\n  setUserApiStatus(false, 'initing')\n\n  const target = state.list.find(api => api.id === apiId)\n  if (!target) throw new Error('api not found')\n  const script = await getUserApiScript(target.id)\n  loadScript({ ...target, script })\n}\n\nexport const destroyUserApi = () => {\n  destroy()\n}\n\n\nexport const setUserApiStatus: typeof action['setStatus'] = (status, message) => {\n  action.setStatus(status, message)\n}\n\nexport const setUserApiList: typeof action['setUserApiList'] = (list) => {\n  action.setUserApiList(list)\n}\n\nexport const importUserApi = async(script: string) => {\n  const info = await addUserApi(script)\n  action.addUserApi(info)\n}\n\nexport const removeUserApi = async(ids: string[]) => {\n  const list = await removeUserApiFromStore(ids)\n  action.setUserApiList(list)\n}\n\nexport const setUserApiAllowShowUpdateAlert = async(id: string, enable: boolean) => {\n  await setUserApiAllowShowUpdateAlertFromStore(id, enable)\n  action.setUserApiAllowShowUpdateAlert(id, enable)\n}\n\nexport const log = {\n  r_info(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    writeLog.info(...params)\n  },\n  r_warn(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    writeLog.warn(...params)\n  },\n  r_error(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    writeLog.error(...params)\n  },\n  log(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    if (global.lx.isEnableUserApiLog) writeLog.info(...params)\n  },\n  info(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    if (global.lx.isEnableUserApiLog) writeLog.info(...params)\n  },\n  warn(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    if (global.lx.isEnableUserApiLog) writeLog.warn(...params)\n  },\n  error(...params: any[]) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    if (global.lx.isEnableUserApiLog) writeLog.error(...params)\n  },\n}\n"
  },
  {
    "path": "src/core/version.ts",
    "content": "import { compareVer } from '@/utils'\nimport { downloadNewVersion, getVersionInfo } from '@/utils/version'\nimport versionActions from '@/store/version/action'\nimport versionState, { type InitState } from '@/store/version/state'\nimport { getIgnoreVersion, getIgnoreVersionFailTipTime, saveIgnoreVersion, saveIgnoreVersionFailTipTime } from '@/utils/data'\nimport { showVersionModal } from '@/navigation'\nimport { Navigation } from 'react-native-navigation'\nimport { toast } from '@/utils/tools'\n\nexport const showModal = () => {\n  if (versionState.showModal) return\n  versionActions.setVisibleModal(true)\n  showVersionModal()\n}\n\nexport const hideModal = (componentId: string) => {\n  if (!versionState.showModal) return\n  versionActions.setVisibleModal(false)\n  void Navigation.dismissOverlay(componentId)\n}\n\nexport const checkUpdate = async() => {\n  versionActions.setVersionInfo({ status: 'checking' })\n  let versionInfo: InitState['versionInfo'] = { ...versionState.versionInfo }\n  try {\n    const { version, desc, history } = await getVersionInfo()\n    versionInfo.newVersion = {\n      version,\n      desc,\n      history,\n    }\n  } catch (err) {\n    versionInfo.newVersion = {\n      version: '0.0.0',\n      desc: '',\n      history: [],\n    }\n  }\n  // const versionInfo = {\n  //   version: '1.9.0',\n  //   desc: '- 更新xxx\\n- 修复xxx123的萨达修复xxx123的萨达修复xxx123的萨达修复xxx123的萨达修复xxx123的萨达',\n  //   history: [{ version: '1.8.0', desc: '- 更新xxx22\\n- 修复xxx22' }, { version: '1.7.0', desc: '- 更新xxx22\\n- 修复xxx22' }],\n  // }\n  if (versionInfo.newVersion.version == '0.0.0') {\n    versionInfo.isUnknown = true\n    versionInfo.status = 'error'\n  } else {\n    versionInfo.status = 'idle'\n    versionInfo.isUnknown = false\n    if (compareVer(versionInfo.version, versionInfo.newVersion.version) != -1) {\n      versionInfo.isLatest = true\n    }\n  }\n\n  versionActions.setVersionInfo(versionInfo)\n\n  if (!versionInfo.isLatest) {\n    if (versionInfo.isUnknown) {\n      const time = await getIgnoreVersionFailTipTime()\n      if (Date.now() - time < 7 * 86400000) return\n      saveIgnoreVersionFailTipTime(Date.now())\n      toast(global.i18n.t('version_tip_unknown'))\n    } else if (versionInfo.newVersion.version != await getIgnoreVersion()) {\n      showModal()\n    }\n  }\n  // console.log(compareVer(process.versions.app, versionInfo.version))\n  // console.log(process.versions.app, versionInfo.version)\n}\n\nexport const downloadUpdate = () => {\n  versionActions.setVersionInfo({ status: 'downloading' })\n  versionActions.setProgress({ total: 0, current: 0 })\n\n  downloadNewVersion(versionState.versionInfo.newVersion!.version, (total: number, current: number) => {\n    // console.log(total, current)\n    versionActions.setProgress({ total, current })\n  }).then(() => {\n    versionActions.setVersionInfo({ status: 'downloaded' })\n  }).catch(() => {\n    versionActions.setVersionInfo({ status: 'error' })\n    // console.log(err)\n  })\n}\n\n\nexport const setIgnoreVersion = (version: InitState['ignoreVersion']) => {\n  versionActions.setIgnoreVersion(version)\n  saveIgnoreVersion(version)\n}\n"
  },
  {
    "path": "src/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    setImmediate(() => {\n      let targetListeners = this.listeners.get(eventName)\n      if (!targetListeners) return\n      for (const listener of targetListeners) {\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n        listener(...args)\n      }\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/event/appEvent.ts",
    "content": "import { setNavActiveId } from '@/core/common'\nimport Event from './Event'\nimport commonState from '@/store/common/state'\nimport { type Source as SonglistSource } from '@/store/songlist/state'\nimport { type SearchType } from '@/store/search/state'\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() {\n  //   this.emit('configUpdate')\n  // }\n\n  focus() {\n    this.emit('focus')\n  }\n\n  /**\n   * 我的列表更新\n   */\n  mylistUpdated(lists: Array<LX.List.MyDefaultListInfo | LX.List.MyLoveListInfo | LX.List.UserListInfo>) {\n    this.emit('mylistUpdated', lists)\n  }\n\n  /**\n   * 我的列表切换\n   */\n  mylistToggled(id: string) {\n    this.emit('listToggled', id)\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 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() {\n    this.emit('error')\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() {\n    this.emit('playerError')\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\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  myListMusicUpdate(ids: string[]) {\n    if (!ids.length) return\n    this.emit('myListMusicUpdate', 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  changeMenuVisible(visible: boolean) {\n    this.emit('changeMenuVisible', visible)\n  }\n\n  /**\n   * 搜索类型改变事件\n   * @param type\n   */\n  searchTypeChanged(type: SearchType) {\n    this.emit('searchTypeChanged', type)\n  }\n\n  jumpListPosition() {\n    if (commonState.navActiveId == 'nav_love') {\n      this.emit('jumpListPosition')\n    } else {\n      global.lx.jumpMyListPosition = true\n      setNavActiveId('nav_love')\n      setTimeout(() => {\n        this.emit('jumpListPosition')\n      }, 200)\n    }\n  }\n\n  changeLoveListVisible(visible: boolean) {\n    this.emit('changeLoveListVisible', visible)\n  }\n\n  showSonglistTagList(source: SonglistSource, activeId: string) {\n    this.emit('showSonglistTagList', source, activeId)\n  }\n\n  hideSonglistTagList() {\n    this.emit('hideSonglistTagList')\n  }\n\n  songlistTagInfoChange(name: string, id: string) {\n    this.emit('songlistTagInfoChange', name, id)\n  }\n\n  selectSyncMode(mode: LX.Sync.ModeType) {\n    this.emit('selectSyncMode', mode)\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/event/dislikeEvent.ts",
    "content": "import { addDislikeInfo, clearDislikeInfo, overwirteDislikeInfo } from '@/utils/dislikeManage'\nimport Event from './Event'\nimport { saveDislikeListRules } from '@/utils/data'\nimport { setDislikeInfo } from '@/core/dislikeList'\n\nconst updateList = async(dislikeInfo: LX.Dislike.DislikeInfo) => {\n  await saveDislikeListRules(dislikeInfo.rules)\n  setDislikeInfo(dislikeInfo)\n}\n\nexport class DislikeEvent extends Event {\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    const dislikeInfo = await overwirteDislikeInfo(dislikeData)\n    await updateList(dislikeInfo)\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 dislikeInfo = await addDislikeInfo(musicInfo)\n    await updateList(dislikeInfo)\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 dislikeInfo = await clearDislikeInfo()\n    await updateList(dislikeInfo)\n    this.emit('dislike_music_clear', isRemote)\n    this.dislike_changed()\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends DislikeEvent {\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 DislikeEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\nexport const createDislikeEventHub = (): DislikeEventTypes => {\n  return new DislikeEvent()\n}\n\n"
  },
  {
    "path": "src/event/listEvent.ts",
    "content": "import Event from './Event'\n\nimport { saveUserList, removeListMusics, saveListMusics } from '@/utils/data'\nimport {\n  userLists,\n  userListCreate,\n  userListsUpdate,\n  userListsRemove,\n  userListsUpdatePosition,\n  listDataOverwrite,\n  listMusicOverwrite,\n  listMusicAdd,\n  listMusicMove,\n  listMusicRemove,\n  listMusicUpdateInfo,\n  listMusicUpdatePosition,\n  listMusicClear,\n  allMusicList,\n} from '@/utils/listManage'\nimport { LIST_IDS } from '@/config/constant'\nimport { setActiveList, setUserList } from '@/core/list'\nimport listState from '@/store/list/state'\n\nconst updateUserList = async(userLists: LX.List.UserListInfo[]) => {\n  await saveUserList(userLists)\n  setUserList(userLists)\n}\n\nconst checkListExist = (changedIds: string[]) => {\n  const index = changedIds.indexOf(listState.activeListId)\n  if (index < 0 || listState.allList.some(l => l.id == listState.activeListId)) return\n  setActiveList(LIST_IDS.DEFAULT)\n}\n\nexport const checkUpdateList = async(changedIds: string[]) => {\n  if (!changedIds.length) return\n  await saveListMusics(changedIds.map(id => ({ id, musics: allMusicList.get(id) as LX.List.ListMusics })))\n  global.app_event.myListMusicUpdate(changedIds)\n}\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\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\n\nexport class ListEvent extends Event {\n  /**\n   * 现有歌曲列表更改时触发的事件\n   * @param ids\n   */\n  // list_music_changed(ids: string[]) {\n  //   this.emit('list_music_changed', ids)\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    const oldIds = userLists.map(l => l.id)\n    const changedIds = listDataOverwrite(listData)\n    await updateUserList(userLists)\n    // await checkUpdateList(changedIds)\n    const removedList = oldIds.filter(id => !allMusicList.has(id))\n    if (removedList.length) await removeListMusics(removedList)\n    const allListIds = [LIST_IDS.DEFAULT, LIST_IDS.LOVE, ...userLists.map(l => l.id)]\n    if (changedIds.includes(LIST_IDS.TEMP)) allListIds.push(LIST_IDS.TEMP)\n    await saveListMusics([...allListIds.map(id => ({ id, musics: allMusicList.get(id) as LX.List.ListMusics }))])\n\n    global.app_event.myListMusicUpdate(changedIds)\n    this.emit('list_data_overwrite', listData, isRemote)\n    checkListExist(changedIds)\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    // const changedIds: string[] = []\n    fixListIdType(lists)\n    for (const list of lists) {\n      userListCreate({ ...list, position })\n      // changedIds.push(list.id)\n    }\n    await updateUserList(userLists)\n    this.emit('list_create', position, lists, isRemote)\n  }\n\n  /**\n   * 批量删除列表及列表内歌曲\n   * @param ids 列表ids\n   * @param isRemote 是否属于远程操作\n   */\n  async list_remove(ids: string[], isRemote: boolean = false) {\n    const changedIds = userListsRemove(ids)\n    await updateUserList(userLists)\n    await removeListMusics(ids)\n    this.emit('list_remove', ids, isRemote)\n    global.app_event.myListMusicUpdate(changedIds)\n\n    checkListExist(changedIds)\n  }\n\n  /**\n   * 批量更新列表信息\n   * @param lists 列表信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_update(lists: LX.List.UserListInfo[], isRemote: boolean = false) {\n    userListsUpdate(lists)\n    await updateUserList(userLists)\n    this.emit('list_update', lists, isRemote)\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    userListsUpdatePosition(position, ids)\n    await updateUserList(userLists)\n    this.emit('list_update_position', position, ids, isRemote)\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    const changedIds = await listMusicOverwrite(listId, musicInfos)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_overwrite', listId, musicInfos, isRemote)\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    const changedIds = await listMusicAdd(listId, musicInfos, addMusicLocationType)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_add', listId, musicInfos, addMusicLocationType, isRemote)\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    const changedIds = await listMusicMove(fromId, toId, musicInfos, addMusicLocationType)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_move', fromId, toId, musicInfos, addMusicLocationType, isRemote)\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    const changedIds = await listMusicRemove(listId, ids)\n    // console.log(changedIds)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_remove', listId, ids, isRemote)\n  }\n\n  /**\n   * 批量更新歌曲信息\n   * @param musicInfos 歌曲&列表信息\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_update(musicInfos: LX.List.ListActionMusicUpdate, isRemote: boolean = false) {\n    const changedIds = await listMusicUpdateInfo(musicInfos)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_update', musicInfos, isRemote)\n  }\n\n  /**\n   * 清空列表内的歌曲\n   * @param ids 列表Id\n   * @param isRemote 是否属于远程操作\n   */\n  async list_music_clear(ids: string[], isRemote: boolean = false) {\n    const changedIds = await listMusicClear(ids)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_clear', ids, isRemote)\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    const changedIds = await listMusicUpdatePosition(listId, position, ids)\n    await checkUpdateList(changedIds)\n    this.emit('list_music_update_position', listId, position, ids, isRemote)\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends ListEvent {\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 ListEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\nexport const createListEventHub = (): ListEventTypes => {\n  return new ListEvent()\n}\n\n"
  },
  {
    "path": "src/event/stateEvent.ts",
    "content": "import Event from './Event'\nimport type { InitState as CommonState } from '@/store/common/state'\nimport type { InitState as ListState } from '@/store/list/state'\nimport type { InitState as PlayerState } from '@/store/player/state'\nimport type { InitState as VersionState } from '@/store/version/state'\nimport { type I18n } from '@/lang'\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 StateEvent extends Event {\n  configUpdated(keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) {\n    this.emit('configUpdated', keys, setting)\n  }\n\n  languageChanged(locale: I18n['locale']) {\n    this.emit('languageChanged', locale)\n  }\n\n  fontSizeUpdated(size: number) {\n    this.emit('fontSizeUpdated', size)\n  }\n\n  statusbarHeightUpdated(size: number) {\n    this.emit('statusbarHeightUpdated', size)\n  }\n\n  apiSourceUpdated(source: LX.AppSetting['common.apiSource']) {\n    this.emit('apiSourceUpdated', source)\n  }\n\n  themeUpdated(theme: LX.ActiveTheme) {\n    this.emit('themeUpdated', theme)\n  }\n\n  bgPicUpdated(bgPic: string | null) {\n    this.emit('bgPicUpdated', bgPic)\n  }\n\n  playerMusicInfoChanged(musicInfo: PlayerState['musicInfo']) {\n    this.emit('playerMusicInfoChanged', musicInfo)\n  }\n\n  playMusicInfoChanged(playMusicInfo: PlayerState['playMusicInfo']) {\n    this.emit('playMusicInfoChanged', playMusicInfo)\n  }\n\n  playInfoChanged(playInfo: PlayerState['playInfo']) {\n    this.emit('playInfoChanged', playInfo)\n  }\n\n  playStateTextChanged(text: PlayerState['statusText']) {\n    this.emit('playStateTextChanged', text)\n  }\n\n  playStateChanged(state: PlayerState['isPlay']) {\n    this.emit('playStateChanged', state)\n  }\n\n  playProgressChanged(progress: PlayerState['progress']) {\n    this.emit('playProgressChanged', progress)\n  }\n\n  playPlayedListChanged(playedList: PlayerState['playedList']) {\n    this.emit('playPlayedListChanged', playedList)\n  }\n\n  playTempPlayListChanged(tempPlayList: PlayerState['tempPlayList']) {\n    this.emit('playTempPlayListChanged', tempPlayList)\n  }\n\n  /**\n   * 我的列表更新\n   */\n  mylistUpdated(lists: Array<LX.List.MyDefaultListInfo | LX.List.MyLoveListInfo | LX.List.UserListInfo>) {\n    this.emit('mylistUpdated', lists)\n  }\n\n  /**\n   * 我的列表切换\n   */\n  mylistToggled(id: string) {\n    this.emit('mylistToggled', id)\n  }\n\n  fetchingListStatusUpdated(fetchingListStatus: ListState['fetchingListStatus']) {\n    this.emit('fetchingListStatusUpdated', fetchingListStatus)\n  }\n\n  syncStatusUpdated(status: LX.Sync.Status) {\n    this.emit('syncStatusUpdated', status)\n  }\n\n  versionInfoUpdated(info: VersionState['versionInfo']) {\n    this.emit('versionInfoUpdated', info)\n  }\n\n  versionInfoIgnoreVersionUpdated(version: VersionState['ignoreVersion']) {\n    this.emit('versionInfoIgnoreVersionUpdated', version)\n  }\n\n  versionDownloadProgressUpdated(progress: VersionState['progress']) {\n    this.emit('versionDownloadProgressUpdated', progress)\n  }\n\n  componentIdsUpdated(ids: CommonState['componentIds']) {\n    this.emit('componentIdsUpdated', ids)\n  }\n\n  navActiveIdUpdated(id: CommonState['navActiveId']) {\n    this.emit('navActiveIdUpdated', id)\n  }\n\n  sourceNamesUpdated(names: CommonState['sourceNames']) {\n    this.emit('sourceNamesUpdated', names)\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends StateEvent {\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 StateEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\nexport const createStateEventHub = (): StateEventTypes => {\n  return new StateEvent()\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  \"add_to\": \"Add to ...\",\n  \"agree\": \"Sure\",\n  \"agree_go\": \"Turn it on\",\n  \"agree_to\": \"Go set it\",\n  \"back\": \"Back\",\n  \"back_home\": \"Back to Desktop\",\n  \"cancel\": \"Cancel\",\n  \"cancel_button_text_2\": \"No, no, wrong click\",\n  \"change_position\": \"Adjust Position\",\n  \"change_position_list_title\": \"Adjust the position of the list\",\n  \"change_position_music_multi_title\": \"Adjust the position of the selected {num} song to\",\n  \"change_position_music_title\": \"Adjust the position of \\\"{name}\\\" to\",\n  \"change_position_tip\": \"Please enter a new position\",\n  \"close\": \"Close\",\n  \"collect\": \"Collect\",\n  \"collect_songlist\": \"Collect\",\n  \"collect_success\": \"Successfully collected\",\n  \"collect_toplist\": \"Collect Top playlist\",\n  \"comment_hide_text\": \"Fold\",\n  \"comment_not support\": \"Unable to get comments for this song.\",\n  \"comment_refresh\": \"This is already the comment for \\\"{name}\\\"\",\n  \"comment_show_image\": \"Show Picture\",\n  \"comment_show_text\": \"Expand\",\n  \"comment_tab_hot\": \"Top {total}\",\n  \"comment_tab_new\": \"Latest {total}\",\n  \"comment_title\": \"Comments for \\\"{name}\\\"\",\n  \"confirm\": \"Confirm\",\n  \"confirm_button_text\": \"Yes\",\n  \"confirm_tip\": \"Just to double check, do you really want to do this?\",\n  \"copy_name\": \"Share Song\",\n  \"copy_name_tip\": \"Copied\",\n  \"create_new_folder\": \"Create new folder\",\n  \"create_new_folder_error_tip\": \"The name entered is invalid\",\n  \"create_new_folder_tip\": \"Please enter a new folder name\",\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\": \"Call failed: {message}\",\n  \"deep_link_file_js_confirm_tip\": \"Are you sure you want to import the \\\"{name}\\\" Music API?\",\n  \"deep_link_file_lxmc_confirm_tip\": \"Are you sure you want to import the \\\"{name}\\\" list file?\",\n  \"delete\": \"Remove\",\n  \"dialog_cancel\": \"No\",\n  \"dialog_confirm\": \"OK\",\n  \"disagree\": \"Deny\",\n  \"disagree_tip\": \"Cancelled...\",\n  \"dislike\": \"Dislike\",\n  \"duplicate_list_tip\": \"You have collected this list \\\"{name}\\\", do you need to update the songs in it?\",\n  \"edit_metadata\": \"Edit Metadata\",\n  \"exit_app_tip\": \"Are you sure you want to exit the app?\",\n  \"ignoring_battery_optimization_check_tip\": \"LX Music is \\\"Restricted\\\" or \\\"Optimized\\\" in \\\"App battery usage\\\", which may cause LX Music to be prevented by the system when playing music in the background. Do you need to set LX Music to \\\"Unrestricted\\\"?\",\n  \"ignoring_battery_optimization_check_title\": \"Background Running Permission Reminder\",\n  \"input_error\": \"Don't type indiscriminately 😡\",\n  \"list_add_btn_title\": \"Add the song(s) to \\\"{name}\\\"\",\n  \"list_add_tip_exists\": \"This song already exists in the list, don't click me again~😡\",\n  \"list_add_title_first_add\": \"Add\",\n  \"list_add_title_first_move\": \"Move\",\n  \"list_add_title_last\": \"to...\",\n  \"list_create\": \"Create List\",\n  \"list_create_input_placeholder\": \"What name do you want...\",\n  \"list_duplicate_tip\": \"A list with the same name already exists. Do you want to continue creating it?\",\n  \"list_edit_action_tip_add_failed\": \"Failed to add\",\n  \"list_edit_action_tip_add_success\": \"Successfully added\",\n  \"list_edit_action_tip_exist\": \"This song already exists in this list\",\n  \"list_edit_action_tip_move_failed\": \"Failed to move\",\n  \"list_edit_action_tip_move_success\": \"Successfully moved\",\n  \"list_edit_action_tip_remove_success\": \"Successfully removed\",\n  \"list_end\": \"In the end~\",\n  \"list_error\": \"Loading failed😥, click to try to reload\",\n  \"list_export\": \"Export\",\n  \"list_export_part_desc\": \"Choose where to save the list file\",\n  \"list_import\": \"Import\",\n  \"list_import_part_button_cancel\": \"No\",\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_confirm_tip\": \"When the list in the backup file has the same ID as the existing list, the songs in the existing list will be overwritten. Will it continue? \\nIf you are not sure about the consequences, it is recommended to back up the existing list first and then continue!\",\n  \"list_import_part_desc\": \"Choose list file\",\n  \"list_import_tip__alldata\": \"This is an \\\"All Data\\\" backup file. You need to go here to import:\\n\\n\\\"Settings -> Backup & Restore -> List Data -> Import lists\\\"\",\n  \"list_import_tip__failed\": \"Failed to import\",\n  \"list_import_tip__playlist\": \"This is a \\\"List\\\" backup file. You need to go here to import:\\n\\n\\\"Settings -> Backup & Restore -> List Data -> Import lists\\\"\",\n  \"list_import_tip__playlist_part\": \"This is a \\\"List-only\\\" backup file. You need to go here to import:\\n\\n\\\"Your Library -> Click the button to the right of any list name -> Click \\\"Import\\\" in the menu\\\"\",\n  \"list_import_tip__setting\": \"This is a \\\"Settings\\\" backup file. the mobile terminal does not support importing such files\",\n  \"list_import_tip__unknown\": \"Unknown file type. Please try to upgrade the app to the latest version and try again.\",\n  \"list_loading\": \"Loading...\",\n  \"list_multi_add_title_first_add\": \"Add the selected\",\n  \"list_multi_add_title_first_move\": \"Move the selected\",\n  \"list_multi_add_title_last\": \"songs to ...\",\n  \"list_name_default\": \"Default\",\n  \"list_name_love\": \"Loved\",\n  \"list_name_temp\": \"Temp List\",\n  \"list_remove\": \"Remove\",\n  \"list_remove_music_multi_tip\": \"Do you really want to remove the selected {num} songs?\",\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_rename_title\": \"Rename List\",\n  \"list_select_all\": \"Select All\",\n  \"list_select_cancel\": \"Cancel\",\n  \"list_select_local_file\": \"Add Local Songs\",\n  \"list_select_local_file_desc\": \"Choose local song folder\",\n  \"list_select_local_file_empty_tip\": \"No songs found in current folder\",\n  \"list_select_local_file_result_failed_tip\": \"Found {total} song(s), successfully added {success} song(s), failed to add {failed} song(s). View the error log for details.\",\n  \"list_select_local_file_result_tip\": \"Found {Total} song(s), all added!\",\n  \"list_select_local_file_temp_add_tip\": \"Found {total} matching files, quickly added to the current list, will now start the file metadata reading process. Please do not exit the app!\",\n  \"list_select_range\": \"Range\",\n  \"list_select_single\": \"Single\",\n  \"list_select_unall\": \"Select none\",\n  \"list_sort\": \"Sort Songs\",\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_sync\": \"Update\",\n  \"list_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  \"list_update_error\": \"Failed to update \\\"{name}\\\"\",\n  \"list_update_success\": \"Successfully updated \\\"{name}\\\"\",\n  \"list_updating\": \"Updating\",\n  \"lists__duplicate\": \"Duplicate Songs\",\n  \"lists_dislike_music_add_tip\": \"Added\",\n  \"lists_dislike_music_singer_tip\": \"Do you really dislike {singer}'s \\\"{name}\\\"?\",\n  \"lists_dislike_music_tip\": \"Do you really dislike \\\"{name}\\\"?\",\n  \"load_failed\": \"Ah, the loading failed 😥\",\n  \"loading\": \"Loading...\",\n  \"location\": \"From {location}\",\n  \"lyric__load_error\": \"Failed to get lyrics\",\n  \"metadata_edit_modal_confirm\": \"Save\",\n  \"metadata_edit_modal_failed\": \"Failed to save. Please view the error log for details.\",\n  \"metadata_edit_modal_file_name\": \"File Name\",\n  \"metadata_edit_modal_file_path\": \"File Path\",\n  \"metadata_edit_modal_form_album_name\": \"Album\",\n  \"metadata_edit_modal_form_lyric\": \"LRC Lyric\",\n  \"metadata_edit_modal_form_match_lyric\": \"Matching online\",\n  \"metadata_edit_modal_form_match_lyric_failed\": \"Failed to match lyrics online\",\n  \"metadata_edit_modal_form_match_lyric_success\": \"Successfully matched lyric🎉\",\n  \"metadata_edit_modal_form_match_pic\": \"Matching online\",\n  \"metadata_edit_modal_form_match_pic_failed\": \"Failed to match cover online\",\n  \"metadata_edit_modal_form_match_pic_success\": \"Successfully matched cover🎉\",\n  \"metadata_edit_modal_form_name\": \"Title\",\n  \"metadata_edit_modal_form_parse_name\": \"Parse song title and artist from file name\",\n  \"metadata_edit_modal_form_parse_name_singer\": \"Title - Artist\",\n  \"metadata_edit_modal_form_parse_singer_name\": \"Artist - Title\",\n  \"metadata_edit_modal_form_pic\": \"Album Cover\",\n  \"metadata_edit_modal_form_remove_lyric\": \"Clear\",\n  \"metadata_edit_modal_form_remove_pic\": \"Remove image\",\n  \"metadata_edit_modal_form_select_pic\": \"Choose image\",\n  \"metadata_edit_modal_form_select_pic_title\": \"Choose album cover image\",\n  \"metadata_edit_modal_form_singer\": \"Artist\",\n  \"metadata_edit_modal_processing\": \"Writing...\",\n  \"metadata_edit_modal_success\": \"Successfully saved\",\n  \"metadata_edit_modal_tip\": \"Song title cannot be empty\",\n  \"metadata_edit_modal_title\": \"Edit Song Metadata\",\n  \"move_to\": \"Move to...\",\n  \"music_source_detail\": \"Song Detail\",\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  \"name\": \"Title: {name}\",\n  \"nav_exit\": \"Exit App\",\n  \"nav_love\": \"Your Library\",\n  \"nav_search\": \"Search\",\n  \"nav_setting\": \"Settings\",\n  \"nav_songlist\": \"Playlists\",\n  \"nav_top\": \"Charts\",\n  \"never_show\": \"Never show again\",\n  \"no_item\": \"The list is empty...\",\n  \"notifications_check_tip\": \"You have not allowed LX Music to show notifications, or the \\\"MusicService\\\" notification category in the LX Music notification settings is disabled, which prevents you from using the notification for actions such as pausing, switching songs, and so on. Do you want to enable it?\",\n  \"notifications_check_title\": \"Notification Permission Reminder\",\n  \"ok\": \"OK\",\n  \"open_storage_error_tip\": \"The entered path is illegal\",\n  \"open_storage_not_found_title\": \"External memory card not found. Manually enter the path to specify external memory in the following\",\n  \"open_storage_select_managed_folder_failed_tip\": \"Failed to select storage path: {msg}\",\n  \"open_storage_select_path\": \"Open storage path\",\n  \"open_storage_select_path_tip\": \"TIP: For external storage, if you still cannot access it after granting storage permissions, you can click the following button to select the path to allow access.\",\n  \"open_storage_tip\": \"Enter storage path\",\n  \"open_storage_title\": \"Manually enter the path to specify external memory in the following\",\n  \"parent_dir_name\": \"Parent folder\",\n  \"pause\": \"Pause\",\n  \"play\": \"Play\",\n  \"play_all\": \"Play all\",\n  \"play_detail_setting_lrc_align\": \"Lyric Alignment\",\n  \"play_detail_setting_lrc_align_center\": \"Center\",\n  \"play_detail_setting_lrc_align_left\": \"Left\",\n  \"play_detail_setting_lrc_align_right\": \"Right\",\n  \"play_detail_setting_lrc_font_size\": \"Lyric Font Size\",\n  \"play_detail_setting_playback_rate\": \"Playback Rate\",\n  \"play_detail_setting_playback_rate_reset\": \"Reset\",\n  \"play_detail_setting_show_lyric_progress_setting\": \"Allow to adjust playback progress by drag-and-drop lyrics\",\n  \"play_detail_setting_title\": \"Player Options\",\n  \"play_detail_setting_volume\": \"Volume\",\n  \"play_detail_todo_tip\": \"What do you want? No, this function has not been implemented yet 😛, But you can try to locate the currently playing song by long pressing (Only valid for playing songs in \\\"Your Library\\\")\",\n  \"play_later\": \"Play Later\",\n  \"play_list_loop\": \"Repeat Playlist\",\n  \"play_list_order\": \"In order\",\n  \"play_list_random\": \"Shuffle\",\n  \"play_next\": \"Next Song\",\n  \"play_prev\": \"Prev Song\",\n  \"play_single\": \"Disable\",\n  \"play_single_loop\": \"Repeat\",\n  \"player__buffering\": \"Buffering...\",\n  \"player__end\": \"Finished\",\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__loading\": \"Music loading...\",\n  \"player__refresh_url\": \"Music URL expired, refreshing...\",\n  \"player_cache_migrating\": \"Song cache is being migrated, please wait ⌛️\",\n  \"quality_high_quality\": \"HQ\",\n  \"quality_lossless\": \"SQ\",\n  \"quality_lossless_24bit\": \"24bit\",\n  \"search__welcome\": \"Search what I want~~😉\",\n  \"search_history_search\": \"Search History\",\n  \"search_hot_search\": \"Top Searches\",\n  \"search_type_music\": \"Song\",\n  \"search_type_songlist\": \"Playlist\",\n  \"setting__other_dislike_list\": \"Dislike Song Rule\",\n  \"setting__other_dislike_list_label\": \"Number of rules: {num}\",\n  \"setting__other_dislike_list_saved_tip\": \"Saved\",\n  \"setting__other_lyric_raw_clear_btn\": \"Clear Lyric Cache\",\n  \"setting__other_lyric_raw_label\": \"Number of lyrics:\",\n  \"setting__other_meta_cache\": \"Other Cache Management\",\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_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_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_part\": \"List Data (Compatible with the desktop edition of list backup files)\",\n  \"setting_backup_part_export_list\": \"Export lists\",\n  \"setting_backup_part_export_list_desc\": \"Save the lists to ...\",\n  \"setting_backup_part_export_list_tip_failed\": \"Failed to export playlists\",\n  \"setting_backup_part_export_list_tip_success\": \"Successfully exported\",\n  \"setting_backup_part_export_list_tip_zip\": \"📦The file is being packaged...\\nIf the file is too large, it may take some time⏳\",\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_desc\": \"Choose a list backup file\",\n  \"setting_backup_part_import_list_tip_error\": \"Failed to import list 😕\",\n  \"setting_backup_part_import_list_tip_running\": \"🚀I am trying to import...\\nIf the list is too big, it may take some time⏳\",\n  \"setting_backup_part_import_list_tip_success\": \"Successfully imported 🎉\",\n  \"setting_backup_part_import_list_tip_unzip\": \"📦File parsing...\\nIf the file is too large, it may take some time⏳\",\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_allow_progress_bar_seek\": \"Allow to adjust playback progress by progress bar in bottom\",\n  \"setting_basic_always_keep_statusbar_height\": \"Always keep status bar height\",\n  \"setting_basic_always_keep_statusbar_height_tip\": \"By default, the app dynamically determines whether spacing needs to be kept for the system status bar, but if there is a situation on your device where the app's interactable content overlaps with the display of the status bar content, you can enable this option to always keep space for the system status bar.\",\n  \"setting_basic_animation\": \"Randomize pop-up animation\",\n  \"setting_basic_auto_hide_play_bar\": \"Hide playbar when keyboard pops up\",\n  \"setting_basic_drawer_layout_position\": \"Direction of Navigation & List Popup\",\n  \"setting_basic_drawer_layout_position_left\": \"Left Side\",\n  \"setting_basic_drawer_layout_position_right\": \"Right Side\",\n  \"setting_basic_font_size\": \"Font Size (Effective After Restart)\",\n  \"setting_basic_font_size_100\": \"Standard\",\n  \"setting_basic_font_size_110\": \"Big\",\n  \"setting_basic_font_size_120\": \"Larger\",\n  \"setting_basic_font_size_130\": \"Oversize\",\n  \"setting_basic_font_size_80\": \"Smaller\",\n  \"setting_basic_font_size_90\": \"Small\",\n  \"setting_basic_font_size_preview\": \"LX Music Font Size Preview\",\n  \"setting_basic_home_page_scroll\": \"Allow horizontal scrolling on vertical homepage\",\n  \"setting_basic_lang\": \"Language\",\n  \"setting_basic_share_type\": \"Share\",\n  \"setting_basic_share_type_clipboard\": \"Copy to clipboard\",\n  \"setting_basic_share_type_system\": \"Use system share\",\n  \"setting_basic_show_animation\": \"Show animation\",\n  \"setting_basic_show_back_btn\": \"Show \\\"Back to Desktop\\\" button\",\n  \"setting_basic_show_exit_btn\": \"Show \\\"Exit App\\\" button\",\n  \"setting_basic_source\": \"Music API\",\n  \"setting_basic_source_direct\": \"Trial 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_title\": \"Choose a Music API\",\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_startup_auto_play\": \"Automatically play music on startup\",\n  \"setting_basic_startup_push_play_detail_screen\": \"Open the playback detail page on startup\",\n  \"setting_basic_theme\": \"Theme\",\n  \"setting_basic_theme_auto_theme\": \"Follow system light & dark modes to switch themes\",\n  \"setting_basic_theme_dynamic_bg\": \"Use dynamic backgrounds\",\n  \"setting_basic_theme_font_shadow\": \"Enable font shadow\",\n  \"setting_basic_theme_hide_bg_dark\": \"Hide the background of the black theme\",\n  \"setting_basic_theme_more_btn_show\": \"More themes\",\n  \"setting_basic_use_system_file_selector\": \"Use the system file selector\",\n  \"setting_basic_use_system_file_selector_tip\": \"When this option is enabled, operations such as importing backup files, Music APIs, etc. will not need to request storage permissions, but may not be available on some systems.\\n\\nYou can turn off the option to fallback to the app's built-in file selector if you are unable to import files after enabling the option.\",\n  \"setting_dislike_list_input_tip\": \"song_name@artist_name\\nsong_name\\n@artist_name\",\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_list\": \"List\",\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 clicking a song in the list (Only valid for \\\"Playlists\\\" and \\\"Charts\\\" page)\",\n  \"setting_list_show interval\": \"Show song length\",\n  \"setting_list_show_album_name\": \"Show song album name\",\n  \"setting_lyric_desktop\": \"Desktop Lyric\",\n  \"setting_lyric_desktop_enable\": \"Show lyric window\",\n  \"setting_lyric_desktop_lock\": \"Lock lyric window\",\n  \"setting_lyric_desktop_maxlineNum\": \"Maximum Number of Lines\",\n  \"setting_lyric_desktop_permission_tip\": \"To use this feature, you need to grant LX Music the permission to display hover windows in the system permission settings.\\n\\nDo you go to the relevant page to grant this permission?\",\n  \"setting_lyric_desktop_single_line\": \"Do not wrap lyrics\",\n  \"setting_lyric_desktop_text_opacity\": \"Lyric Font Transparency\",\n  \"setting_lyric_desktop_text_size\": \"Lyric Font Size\",\n  \"setting_lyric_desktop_text_x\": \"Lyric Horizontal Alignment\",\n  \"setting_lyric_desktop_text_x_center\": \"Center\",\n  \"setting_lyric_desktop_text_x_left\": \"Left\",\n  \"setting_lyric_desktop_text_x_right\": \"Right\",\n  \"setting_lyric_desktop_text_y\": \"Lyric Vertical Alignment\",\n  \"setting_lyric_desktop_text_y_bottom\": \"Bottom\",\n  \"setting_lyric_desktop_text_y_center\": \"Center\",\n  \"setting_lyric_desktop_text_y_top\": \"Top\",\n  \"setting_lyric_desktop_theme\": \"Lyric Theme Color\",\n  \"setting_lyric_desktop_toggle_anima\": \"Show lyric switching animation\",\n  \"setting_lyric_desktop_view_width\": \"Percentage of Window Width\",\n  \"setting_other\": \"Extras\",\n  \"setting_other_cache\": \"Cache Management (including the cache of songs, lyrics, error logs, etc., it is not recommended to clean up if there is no problem related to song playback)\",\n  \"setting_other_cache_clear_btn\": \"Clear Cache\",\n  \"setting_other_cache_clear_success_tip\": \"Cache cleanup complete\",\n  \"setting_other_cache_getting\": \"Cache being counted...\",\n  \"setting_other_cache_size\": \"The app has used cache size: \",\n  \"setting_other_dislike_list_show_btn\": \"Edit Rules\",\n  \"setting_other_log\": \"Error log (When abnormal operation occurs)\",\n  \"setting_other_log_btn_clean\": \"Clear\",\n  \"setting_other_log_btn_hide\": \"Close\",\n  \"setting_other_log_btn_show\": \"View log\",\n  \"setting_other_log_sync_log\": \"Logging sync log\",\n  \"setting_other_log_tip_clean_success\": \"Log cleanup complete\",\n  \"setting_other_log_tip_null\": \"The log is empty~\",\n  \"setting_other_log_user_api_log\": \"Logging Music API log\",\n  \"setting_play_audio_offload\": \"Enable audio offload\",\n  \"setting_play_audio_offload_tip\": \"You can enable this option to save power consumption, but on some devices there may be an issue where all songs prompt \\\"Error loading music\\\" or \\\"Unable to play the whole song in full\\\", this is due to a bug in the current system. \\n\\nFor those experiencing this issue you can turn off the option and restart the app completely and try again.\",\n  \"setting_play_auto_clean_played_list\": \"Automatically empty the played list\",\n  \"setting_play_auto_clean_played_list_tip\": \"In the shuffle mode, when switching between songs by clicking on a song in the same list as playlist, if \\\"Automatically empty played list\\\" is enabled, the songs in the list will participate in the random again.\",\n  \"setting_play_cache_size\": \"Maximum Cache Size (MB)\",\n  \"setting_play_cache_size_no_cache\": \"Cache Disabled\",\n  \"setting_play_cache_size_save_tip\": \"The cache is set and takes effect after restarting the app.\",\n  \"setting_play_handle_audio_focus\": \"Automatically pause playback when other apps playing sound\",\n  \"setting_play_handle_audio_focus_tip\": \"Take effect after restarting the app\",\n  \"setting_play_lyric_transition\": \"Show translated lyrics\",\n  \"setting_play_play_quality\": \"Prioritize Sound Quality for Playback If Available\",\n  \"setting_play_s2t\": \"Convert Chinese lyrics that are playing to traditional\",\n  \"setting_play_save_play_time\": \"Remember playback progress\",\n  \"setting_play_show_bluetooth_lyric\": \"Show lyrics from bluetooth\",\n  \"setting_play_show_notification_image\": \"Show album cover in notification bar\",\n  \"setting_play_show_roma\": \"Show romanized lyrics if available\",\n  \"setting_play_show_translation\": \"Show translated lyrics if available\",\n  \"setting_player\": \"Playback\",\n  \"setting_player_save_play_time\": \"Remember playback progress\",\n  \"setting_search\": \"Search\",\n  \"setting_search_show_history_search\": \"Enable Search History\",\n  \"setting_search_show_hot_search\": \"Enable Top Searches\",\n  \"setting_sync\": \"Sync\",\n  \"setting_sync_address\": \"Current device address: {address}\",\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_code_input_tip\": \"Please enter the connection code\",\n  \"setting_sync_code_label\": \"Need to enter the connection code for the first connection\",\n  \"setting_sync_enable\": \"Enable sync\",\n  \"setting_sync_history\": \"History address\",\n  \"setting_sync_history_empty\": \"Nothing here\",\n  \"setting_sync_history_title\": \"Connection history\",\n  \"setting_sync_host_label\": \"Synch service address\",\n  \"setting_sync_host_value_error_tip\": \"The address needs to start with \\\"http://\\\" or \\\"https://\\\"!\",\n  \"setting_sync_host_value_tip\": \"http://<Host>:<Port>\",\n  \"setting_sync_port_label\": \"Sync service port number\",\n  \"setting_sync_port_tip\": \"Please enter a port number\",\n  \"setting_sync_status\": \"Status: {status}\",\n  \"setting_sync_status_enabled\": \"connected\",\n  \"setting_theme\": \"Theme\",\n  \"setting_version\": \"Update\",\n  \"setting_version_show_ver_modal\": \"Open Update Window 🚀\",\n  \"share_card_title_music\": \"Share \\\"{name}\\\" to...\",\n  \"share_title_music\": \"Song Sharing\",\n  \"singer\": \"Artist: {name}\",\n  \"songlist_hot\": \"Top\",\n  \"songlist_hot_collect\": \"Top Collect\",\n  \"songlist_new\": \"Latest\",\n  \"songlist_open\": \"Open\",\n  \"songlist_open_input_placeholder\": \"Enter a playlist link/ID\",\n  \"songlist_open_input_tip\": \"1. Cross-service playlists are not supported. Please confirm whether the playlist to be opened corresponds to the current chosen service\\n2. If you encounter a playlist link that cannot be opened. Please send us your feedback\\n3. 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_recommend\": \"Recommend\",\n  \"songlist_rise\": \"Rise\",\n  \"songlist_tag_default\": \"Default\",\n  \"songlist_tag_hot\": \"Top\",\n  \"songlist_tags\": \"Playlist Category\",\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_real_all\": \"Aggregated\",\n  \"source_real_bd\": \"Baidu\",\n  \"source_real_kg\": \"Kugou\",\n  \"source_real_kw\": \"Kuwo\",\n  \"source_real_mg\": \"Migu\",\n  \"source_real_tx\": \"Tencent\",\n  \"source_real_wy\": \"NetEase\",\n  \"stop\": \"Stop\",\n  \"stopped\": \"Stopped\",\n  \"storage_file_no_match\": \"The selected file does not meet the requirements!\",\n  \"storage_file_no_select_file_failed_tip\": \"Failed to select a file using the system file selector. Do you want to fallback to the app's built-in file selector?\",\n  \"storage_permission_tip_disagree\": \"The user rejects it!\",\n  \"storage_permission_tip_disagree_ask_again\": \"The feature is unavailable because you have permanently denied LX Music access to the internal storage.\\n\\nTo continue, you need to navigate to LX Music's \\\"Permissions\\\" option in the system settings and allow LX Music to access the files.\",\n  \"storage_permission_tip_request\": \"To use this feature you need to allow LX Music to access the internal storage, do you agree and continue?\",\n  \"sync__dislike_mode_merge_tip_desc\": \"Merge the content of the two lists and remove the duplicates.\",\n  \"sync__dislike_mode_other_tip_desc\": \"\\\"Cancel Sync\\\" will not sync the \\\"dislike song\\\" list.\",\n  \"sync__dislike_mode_overwrite_tip_desc\": \"The list of overwritten parties will be replaced with the list of overwriting parties.\",\n  \"sync__dislike_mode_title\": \"Choose how to sync with {name}'s \\\"dislike song\\\" list\",\n  \"sync__list_mode_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_mode_other_tip_desc\": \"\\\"Cancel Sync\\\" will not sync the list.\",\n  \"sync__list_mode_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_mode_title\": \"Choose how to synchronize the list with \\\"{name}\\\"\",\n  \"sync__mode_merge_btn_local_remote\": \"\\\"Local List\\\" Merge \\\"Remote List\\\"\",\n  \"sync__mode_merge_btn_remote_local\": \"\\\"Remote List\\\" Merge \\\"Local List\\\"\",\n  \"sync__mode_merge_tip\": \"Merge: \",\n  \"sync__mode_other_label\": \"Other\",\n  \"sync__mode_other_tip\": \"Other: \",\n  \"sync__mode_overwrite\": \"Full Overwrite\",\n  \"sync__mode_overwrite_btn_cancel\": \"Cancel Sync\",\n  \"sync__mode_overwrite_btn_local_remote\": \"\\\"Local List\\\" Overwrite \\\"Remote List\\\"\",\n  \"sync__mode_overwrite_btn_remote_local\": \"\\\"Remote List\\\" Overwrite \\\"Local List\\\"\",\n  \"sync__mode_overwrite_label\": \"Overwrite\",\n  \"sync__mode_overwrite_tip\": \"Overwrite: \",\n  \"sync_status_disabled\": \"not connected\",\n  \"theme_black\": \"Black\",\n  \"theme_blue\": \"Blue\",\n  \"theme_blue2\": \"Purple Blue\",\n  \"theme_blue_plus\": \"Blue Plus\",\n  \"theme_brown\": \"Brown\",\n  \"theme_china_ink\": \"China Ink\",\n  \"theme_green\": \"Green\",\n  \"theme_grey\": \"Grey\",\n  \"theme_happy_new_year\": \"New Year\",\n  \"theme_mid_autumn\": \"Mid-Autumn\",\n  \"theme_ming\": \"Ming\",\n  \"theme_naruto\": \"Naruto\",\n  \"theme_orange\": \"Orange\",\n  \"theme_pink\": \"Pink\",\n  \"theme_purple\": \"Purple\",\n  \"theme_red\": \"Red\",\n  \"timeout_exit_btn_cancel\": \"Cancel timing\",\n  \"timeout_exit_btn_update\": \"Update timing\",\n  \"timeout_exit_btn_wait_cancel\": \"Cancel exit\",\n  \"timeout_exit_btn_wait_tip\": \"Timeout expired, waiting to exit...\",\n  \"timeout_exit_input_tip\": \"Enter countdown minutes\",\n  \"timeout_exit_label_isPlayed\": \"Wait for the song to finish playing and then stop playing\",\n  \"timeout_exit_min\": \"Minutes\",\n  \"timeout_exit_tip_cancel\": \"Timeout stop playing has been cancelled\",\n  \"timeout_exit_tip_max\": \"You can only set up to {num} minutes\",\n  \"timeout_exit_tip_off\": \"Set timer to stop playing\",\n  \"timeout_exit_tip_on\": \"Stop playing after {time}\",\n  \"toggle_source\": \"Change Source\",\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  \"understand\": \"Already understood 👌\",\n  \"user_api__init_failed_alert\": \"Failed to initialize Music API \\\"{name}\\\":\",\n  \"user_api_add_failed_tip\": \"Invalid Music API file\",\n  \"user_api_allow_show_update_alert\": \"Allow show update popups\",\n  \"user_api_btn_import\": \"Import\",\n  \"user_api_btn_import_local\": \"From Local\",\n  \"user_api_btn_import_online\": \"From Network\",\n  \"user_api_btn_import_online_input_confirm\": \"Import\",\n  \"user_api_btn_import_online_input_loading\": \"Importing...\",\n  \"user_api_btn_import_online_input_tip\": \"Please enter an HTTP link\",\n  \"user_api_empty\": \"It's actually empty here 😲\",\n  \"user_api_import_desc\": \"Choose Music API script file\",\n  \"user_api_import_failed_tip\": \"Failed to import Music API: \\n{message}\",\n  \"user_api_import_success_tip\": \"Successfully imported 🎉\",\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_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_remove_tip\": \"Do you really want to remove \\\"{name}\\\"?\",\n  \"user_api_title\": \"Music API Management (EXPERIMENTAL)\",\n  \"user_api_update_alert\": \"Music API \\\"{name}\\\" found new version\",\n  \"user_api_update_alert_open_url\": \"Open update address\",\n  \"version_btn_close\": \"Close\",\n  \"version_btn_downloading\": \"I am trying to download... {current}/{total} ({progress}%)\",\n  \"version_btn_failed\": \"Retry\",\n  \"version_btn_ignore\": \"Ignore\",\n  \"version_btn_ignore_cancel\": \"Cancel ignore\",\n  \"version_btn_min\": \"Background download\",\n  \"version_btn_new\": \"Update\",\n  \"version_btn_unknown\": \"Project Homepage\",\n  \"version_btn_update\": \"Install\",\n  \"version_label_change_log\": \"Changelog: \",\n  \"version_label_current_ver\": \"Current: \",\n  \"version_label_history\": \"History: \",\n  \"version_label_latest_ver\": \"Latest: \",\n  \"version_tip_checking\": \"Checking for updates...⏳\",\n  \"version_tip_downloaded\": \"The apk has been downloaded.\",\n  \"version_tip_failed\": \"Failed to download the apk. You can either retry or go to the project address and download the new update manually.\",\n  \"version_tip_latest\": \"The app is up to date, please enjoy it~🥂\",\n  \"version_tip_min\": \"The download has been switched to background. You can navigate to \\\"Settings → Update\\\" to re-open this pop-up window.\",\n  \"version_tip_unknown\": \"Failed to get the latest version information. Recommended to manually go to the project address to check for a new version.\",\n  \"version_title_checking\": \"⏳ Checking for updates ⏳\",\n  \"version_title_failed\": \"❌ Failed to download ❌\",\n  \"version_title_latest\": \"🎉 The current version is already the latest 🎊\",\n  \"version_title_new\": \"🌟 New version found 🌟\",\n  \"version_title_unknown\": \"❓ Failed to get the latest version information ❓\",\n  \"version_title_update\": \"🚀 App update 🚀\"\n}\n"
  },
  {
    "path": "src/lang/i18n.ts",
    "content": "import { useState, useEffect, useCallback } from 'react'\nimport type { Messages, Message } from './index'\nimport { messages } from './index'\n\n\ntype TranslateValues = Record<string, string | number | boolean>\n\ntype Langs = keyof Messages\n\ntype Hook = (locale: Langs) => void\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\nlet locale: Langs = 'zh_cn'\n\nlet i18n: I18n\n\n\nconst hookTools = {\n  hooks: [] as Hook[],\n  add(hook: Hook) {\n    this.hooks.push(hook)\n  },\n  remove(hook: Hook) {\n    this.hooks.splice(this.hooks.indexOf(hook), 1)\n  },\n  update(locale: Parameters<Hook>[0]) {\n    for (const hook of this.hooks) hook(locale)\n  },\n}\n\nconst useI18n = () => {\n  const [locale, updateLocale] = useState(i18n?.locale ?? 'en_us')\n  // console.log('hook run')\n  useEffect(() => {\n    const hook: Hook = (locale) => {\n      updateLocale(locale)\n    }\n    hookTools.add(hook)\n    return () => { hookTools.remove(hook) }\n  }, [])\n\n  return useCallback((key: keyof Message, val?: TranslateValues): string => {\n    return i18n?.getMessage(key, val) ?? ''\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [locale])\n}\n\nconst setLanguage = (lang: Langs) => {\n  i18n.setLanguage(lang)\n}\n\nconst createI18n = (_locale: Langs = locale): I18n => {\n  locale = _locale\n\n  return i18n = {\n    locale,\n    fallbackLocale: 'zh_cn',\n    availableLocales: Object.keys(messages) as Langs[],\n    messages,\n    message: messages[locale],\n    setLanguage(_locale: Langs) {\n      this.locale = _locale\n      this.message = messages[_locale]\n      hookTools.update(_locale)\n    },\n    fillMessage(message: string, vals: TranslateValues): string {\n      for (const [key, val] of Object.entries(vals)) {\n        message = message.replace(new RegExp('{' + key + '}', 'g'), 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] ?? ''\n      return val ? this.fillMessage(targetMessage, val) : targetMessage\n    },\n    t(key: keyof Message, val?: TranslateValues): string {\n      return this.getMessage(key, val)\n    },\n  }\n}\n\n\nexport {\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\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-hant',\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: (typeof langs)[number]['locale']\n  // alternate?: string\n}> = []\ntype Messages = Record<(typeof langs)[number]['locale'], Message>\n\n// @ts-expect-error\nconst messages: Messages = {}\n\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/zh-cn.json",
    "content": "{\n  \"add_to\": \"添加到...\",\n  \"agree\": \"行行行\",\n  \"agree_go\": \"去开启\",\n  \"agree_to\": \"去设置\",\n  \"back\": \"返回\",\n  \"back_home\": \"返回桌面\",\n  \"cancel\": \"取消\",\n  \"cancel_button_text_2\": \"不不不，点错了\",\n  \"change_position\": \"调整位置\",\n  \"change_position_list_title\": \"调整列表位置\",\n  \"change_position_music_multi_title\": \"将已选的 {num} 首歌曲的位置调整到\",\n  \"change_position_music_title\": \"将「{name}」的位置调整到\",\n  \"change_position_tip\": \"请输入新的位置\",\n  \"close\": \"关闭\",\n  \"collect\": \"收藏\",\n  \"collect_songlist\": \"收藏歌单\",\n  \"collect_success\": \"收藏成功\",\n  \"collect_toplist\": \"收藏排行榜\",\n  \"comment_hide_text\": \"收起评论\",\n  \"comment_not support\": \"该歌曲不支持获取评论\",\n  \"comment_refresh\": \"这已经是「{name}」的评论啦\",\n  \"comment_show_image\": \"显示图片\",\n  \"comment_show_text\": \"展开评论\",\n  \"comment_tab_hot\": \"热门 {total}\",\n  \"comment_tab_new\": \"最新 {total}\",\n  \"comment_title\": \"「{name}」的评论\",\n  \"confirm\": \"确认\",\n  \"confirm_button_text\": \"是的\",\n  \"confirm_tip\": \"再确认一下，你真的要这样做吗？\",\n  \"copy_name\": \"分享歌曲\",\n  \"copy_name_tip\": \"已复制\",\n  \"create_new_folder\": \"新建文件夹\",\n  \"create_new_folder_error_tip\": \"输入的名称不合法\",\n  \"create_new_folder_tip\": \"请输入新文件夹名\",\n  \"date_format_hour\": \"{num} 小时前\",\n  \"date_format_minute\": \"{num} 分钟前\",\n  \"date_format_second\": \"{num} 秒前\",\n  \"deep_link__handle_error_tip\": \"调用失败：{message}\",\n  \"deep_link_file_js_confirm_tip\": \"确认要导入自定义源文件「{name}」吗？\",\n  \"deep_link_file_lxmc_confirm_tip\": \"确认要导入列表文件「{name}」吗？\",\n  \"delete\": \"移除\",\n  \"dialog_cancel\": \"我不\",\n  \"dialog_confirm\": \"好的\",\n  \"disagree\": \"我就不\",\n  \"disagree_tip\": \"那算了...🙄\",\n  \"dislike\": \"不喜欢\",\n  \"duplicate_list_tip\": \"你之前已收藏过列表「{name}」，是否需要更新里面的歌曲？\",\n  \"edit_metadata\": \"编辑标签\",\n  \"exit_app_tip\": \"确定要退出应用吗？\",\n  \"ignoring_battery_optimization_check_tip\": \"LX Music 没有在「忽略电池优化」的白名单中，这可能会导致在后台播放音乐时被系统暂停。是否将 LX Music 加入该白名单中？\",\n  \"ignoring_battery_optimization_check_title\": \"后台运行权限设置提醒\",\n  \"input_error\": \"不要乱输好吧😡\",\n  \"list_add_btn_title\": \"把该歌曲添加到「{name}」\",\n  \"list_add_tip_exists\": \"列表已经存在这首歌啦，不要再点我啦~😡\",\n  \"list_add_title_first_add\": \"添加\",\n  \"list_add_title_first_move\": \"移动\",\n  \"list_add_title_last\": \"到...\",\n  \"list_create\": \"新建列表\",\n  \"list_create_input_placeholder\": \"你想起啥名...\",\n  \"list_duplicate_tip\": \"已存在同名列表，是否继续创建？\",\n  \"list_edit_action_tip_add_failed\": \"添加失败\",\n  \"list_edit_action_tip_add_success\": \"添加成功\",\n  \"list_edit_action_tip_exist\": \"该列表已经有这首歌啦\",\n  \"list_edit_action_tip_move_failed\": \"移动失败\",\n  \"list_edit_action_tip_move_success\": \"移动成功\",\n  \"list_edit_action_tip_remove_success\": \"移除成功\",\n  \"list_end\": \"到底啦~\",\n  \"list_error\": \"加载失败😥，点击尝试重新加载\",\n  \"list_export\": \"导出\",\n  \"list_export_part_desc\": \"选择列表文件保存位置\",\n  \"list_import\": \"导入\",\n  \"list_import_part_button_cancel\": \"不要啊\",\n  \"list_import_part_button_confirm\": \"覆盖掉\",\n  \"list_import_part_confirm\": \"导入的列表「{importName}」与本地列表「{localName}」的 ID 相同，是否覆盖本地列表？\",\n  \"list_import_part_confirm_tip\": \"备份文件中列表与现有列表 ID 相同时，现有列表内歌曲将被覆盖，是否继续？\\n若你不确定后果，建议先备份现有列表再来继续！\",\n  \"list_import_part_desc\": \"选择列表文件\",\n  \"list_import_tip__alldata\": \"这是一个「所有数据」备份文件，你需要去这里导入：\\n\\n设置 → 备份与恢复 → 列表数据 → 导入列表\",\n  \"list_import_tip__failed\": \"导入失败\",\n  \"list_import_tip__playlist\": \"这是一个「列表」备份文件，你需要去这里导入：\\n\\n设置 → 备份与恢复 → 列表数据 → 导入列表\",\n  \"list_import_tip__playlist_part\": \"这是一个「单列表」文件，你需要去这里导入：\\n\\n我的列表 → 点击任意一个列表名右侧的菜单按钮 → 在弹出的菜单中选择「导入」\",\n  \"list_import_tip__setting\": \"这是一个「设置」备份文件，移动版不支持导入此类文件\",\n  \"list_import_tip__unknown\": \"未知的文件类型，请尝试升级到最新版本后再试\",\n  \"list_loading\": \"加载中...\",\n  \"list_multi_add_title_first_add\": \"添加已选的\",\n  \"list_multi_add_title_first_move\": \"移动已选的\",\n  \"list_multi_add_title_last\": \"首歌曲到...\",\n  \"list_name_default\": \"试听列表\",\n  \"list_name_love\": \"我的收藏\",\n  \"list_name_temp\": \"临时列表\",\n  \"list_remove\": \"移除\",\n  \"list_remove_music_multi_tip\": \"你真的想要移除所选的 {num} 首歌曲吗？\",\n  \"list_remove_tip\": \"你真的想要移除「{name}」吗？\",\n  \"list_remove_tip_button\": \"是的，没错\",\n  \"list_rename\": \"重命名\",\n  \"list_rename_title\": \"重命名列表\",\n  \"list_select_all\": \"全选\",\n  \"list_select_cancel\": \"取消\",\n  \"list_select_local_file\": \"添加本地歌曲\",\n  \"list_select_local_file_desc\": \"选择本地歌曲文件夹\",\n  \"list_select_local_file_empty_tip\": \"没有在当前文件夹中发现歌曲\",\n  \"list_select_local_file_result_failed_tip\": \"共发现 {total} 首歌曲，成功添加 {success} 首，失败 {failed} 首，可到错误日志查看添加失败的歌曲\",\n  \"list_select_local_file_result_tip\": \"共发现 {total} 首歌曲，已全部添加！\",\n  \"list_select_local_file_temp_add_tip\": \"共找到 {total} 个符合要求的文件，已快速添加到当前列表，现在将进入文件标签读取流程，请勿退出应用！\",\n  \"list_select_range\": \"区间\",\n  \"list_select_single\": \"单选\",\n  \"list_select_unall\": \"反选\",\n  \"list_sort\": \"排序歌曲\",\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_sync\": \"更新\",\n  \"list_sync_confirm_tip\": \"这将会把「{name}」内的歌曲替换成在线列表的歌曲，你确认要更新吗？\",\n  \"list_update_error\": \"「{name}」更新失败\",\n  \"list_update_success\": \"「{name}」更新成功\",\n  \"list_updating\": \"更新中\",\n  \"lists__duplicate\": \"重复歌曲\",\n  \"lists_dislike_music_add_tip\": \"已添加\",\n  \"lists_dislike_music_singer_tip\": \"你真的不喜欢「{singer}」的「{name}」吗？\",\n  \"lists_dislike_music_tip\": \"你真的不喜欢「{name}」吗？\",\n  \"load_failed\": \"啊~加载失败了😥\",\n  \"loading\": \"加载中...\",\n  \"location\": \"来自{location}\",\n  \"lyric__load_error\": \"歌词获取失败\",\n  \"metadata_edit_modal_confirm\": \"保存\",\n  \"metadata_edit_modal_failed\": \"保存失败，详情可查看错误日志\",\n  \"metadata_edit_modal_file_name\": \"文件名\",\n  \"metadata_edit_modal_file_path\": \"文件路径\",\n  \"metadata_edit_modal_form_album_name\": \"专辑名\",\n  \"metadata_edit_modal_form_lyric\": \"LRC 歌词\",\n  \"metadata_edit_modal_form_match_lyric\": \"在线匹配\",\n  \"metadata_edit_modal_form_match_lyric_failed\": \"在线歌词匹配失败\",\n  \"metadata_edit_modal_form_match_lyric_success\": \"歌词匹配成功🎉\",\n  \"metadata_edit_modal_form_match_pic\": \"在线匹配\",\n  \"metadata_edit_modal_form_match_pic_failed\": \"在线匹配封面失败\",\n  \"metadata_edit_modal_form_match_pic_success\": \"封面匹配成功🎉\",\n  \"metadata_edit_modal_form_name\": \"歌曲名\",\n  \"metadata_edit_modal_form_parse_name\": \"从文件名解析歌曲名、艺术家\",\n  \"metadata_edit_modal_form_parse_name_singer\": \"歌曲名 - 艺术家\",\n  \"metadata_edit_modal_form_parse_singer_name\": \"艺术家 - 歌曲名\",\n  \"metadata_edit_modal_form_pic\": \"歌曲专辑封面\",\n  \"metadata_edit_modal_form_remove_lyric\": \"清空\",\n  \"metadata_edit_modal_form_remove_pic\": \"移除图片\",\n  \"metadata_edit_modal_form_select_pic\": \"选择图片\",\n  \"metadata_edit_modal_form_select_pic_title\": \"选择歌曲专辑封面图片\",\n  \"metadata_edit_modal_form_singer\": \"艺术家\",\n  \"metadata_edit_modal_processing\": \"写入中...\",\n  \"metadata_edit_modal_success\": \"保存成功\",\n  \"metadata_edit_modal_tip\": \"歌曲名不能为空\",\n  \"metadata_edit_modal_title\": \"编辑歌曲标签\",\n  \"move_to\": \"移动到...\",\n  \"music_source_detail\": \"歌曲详情页\",\n  \"music_toggle__confirm\": \"确认\",\n  \"music_toggle__duplicate_tip\": \"列表中已存在相同的歌曲，是否将其移除并继续？\",\n  \"name\": \"歌曲名：{name}\",\n  \"nav_exit\": \"退出应用\",\n  \"nav_love\": \"我的列表\",\n  \"nav_search\": \"搜索\",\n  \"nav_setting\": \"设置\",\n  \"nav_songlist\": \"歌单\",\n  \"nav_top\": \"排行榜\",\n  \"never_show\": \"不再提醒\",\n  \"no_item\": \"列表竟然是空的...\",\n  \"notifications_check_tip\": \"你没有允许 LX Music 显示通知，或 LX Music 通知设置里的「MusicService」通知被禁用，这将无法使用通知栏进行暂停、切歌等操作，是否去开启？\",\n  \"notifications_check_title\": \"通知权限提醒\",\n  \"ok\": \"我知道了\",\n  \"open_storage_error_tip\": \"输入的路径不合法\",\n  \"open_storage_not_found_title\": \"未找到外置存储卡，请手动在下方输入路径以指定外置存储器\",\n  \"open_storage_select_managed_folder_failed_tip\": \"选择存储路径失败：{msg}\",\n  \"open_storage_select_path\": \"打开存储路径\",\n  \"open_storage_select_path_tip\": \"提示：对于外部存储，在授予存储权限后仍然无法访问时，可以点击下面的按钮选择允许访问的路径。\",\n  \"open_storage_tip\": \"输入存储路径\",\n  \"open_storage_title\": \"请手动在下方输入路径以指定外置存储器\",\n  \"parent_dir_name\": \"上一级文件夹\",\n  \"pause\": \"暂停\",\n  \"play\": \"播放\",\n  \"play_all\": \"播放全部\",\n  \"play_detail_setting_lrc_align\": \"歌词对齐方式\",\n  \"play_detail_setting_lrc_align_center\": \"居中\",\n  \"play_detail_setting_lrc_align_left\": \"居左\",\n  \"play_detail_setting_lrc_align_right\": \"居右\",\n  \"play_detail_setting_lrc_font_size\": \"歌词字体大小\",\n  \"play_detail_setting_playback_rate\": \"播放速率\",\n  \"play_detail_setting_playback_rate_reset\": \"重置\",\n  \"play_detail_setting_show_lyric_progress_setting\": \"允许通过拖拽歌词调整播放进度\",\n  \"play_detail_setting_title\": \"播放器设置\",\n  \"play_detail_setting_volume\": \"音量大小\",\n  \"play_detail_todo_tip\": \"你想干嘛？不可以的，这个功能还没有实现哦😛，不过你可以试着长按来定位当前播放的歌曲（仅对播放「我的列表」里的歌曲有效哦）\",\n  \"play_later\": \"稍后播放\",\n  \"play_list_loop\": \"列表循环播放\",\n  \"play_list_order\": \"顺序播放\",\n  \"play_list_random\": \"随机播放\",\n  \"play_next\": \"下一曲\",\n  \"play_prev\": \"上一曲\",\n  \"play_single\": \"禁用歌曲切换\",\n  \"play_single_loop\": \"单曲循环播放\",\n  \"player__buffering\": \"缓冲中...\",\n  \"player__end\": \"播放完毕\",\n  \"player__error\": \"音频加载出错，5 秒后切换下一首\",\n  \"player__getting_url\": \"歌曲链接获取中...\",\n  \"player__getting_url_delay_retry\": \"服务器繁忙，{time} 秒后重试...\",\n  \"player__loading\": \"音乐加载中...\",\n  \"player__refresh_url\": \"URL 过期，正在刷新 URL...\",\n  \"player_cache_migrating\": \"歌曲缓存迁移中，请稍等 ⌛️\",\n  \"quality_high_quality\": \"HQ\",\n  \"quality_lossless\": \"SQ\",\n  \"quality_lossless_24bit\": \"24bit\",\n  \"search__welcome\": \"搜我所想~~😉\",\n  \"search_history_search\": \"历史搜索\",\n  \"search_hot_search\": \"热门搜索\",\n  \"search_type_music\": \"歌曲\",\n  \"search_type_songlist\": \"歌单\",\n  \"setting__other_dislike_list\": \"「不喜欢的歌曲」规则\",\n  \"setting__other_dislike_list_label\": \"规则数量：{num}\",\n  \"setting__other_dislike_list_saved_tip\": \"已保存\",\n  \"setting__other_lyric_raw_clear_btn\": \"清理歌词缓存\",\n  \"setting__other_lyric_raw_label\": \"歌词数量：\",\n  \"setting__other_meta_cache\": \"其他缓存管理\",\n  \"setting__other_music_url_clear_btn\": \"清理歌曲 URL 缓存\",\n  \"setting__other_music_url_label\": \"歌曲 URL 数量：\",\n  \"setting__other_other_source_clear_btn\": \"清理换源歌曲缓存\",\n  \"setting__other_other_source_label\": \"换源歌曲信息数量：\",\n  \"setting__other_resource_cache\": \"资源缓存管理（包括歌曲、图片缓存）\",\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_part\": \"列表数据（与桌面版列表备份文件通用）\",\n  \"setting_backup_part_export_list\": \"导出列表\",\n  \"setting_backup_part_export_list_desc\": \"选择歌单备份文件保存位置\",\n  \"setting_backup_part_export_list_tip_failed\": \"歌单导出失败\",\n  \"setting_backup_part_export_list_tip_success\": \"导出成功\",\n  \"setting_backup_part_export_list_tip_zip\": \"📦文件打包中...\\n若文件太大可能需要一些时间⏳\",\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_desc\": \"选择列表备份文件\",\n  \"setting_backup_part_import_list_tip_error\": \"列表导入失败😕\",\n  \"setting_backup_part_import_list_tip_running\": \"🚀正在努力导入中...\\n若列表太大可能需要一些时间⏳\",\n  \"setting_backup_part_import_list_tip_success\": \"导入成功🎉\",\n  \"setting_backup_part_import_list_tip_unzip\": \"📦文件解析中...\\n若文件太大可能需要一些时间⏳\",\n  \"setting_backup_part_import_setting\": \"导入设置\",\n  \"setting_backup_part_import_setting_desc\": \"选择配置文件\",\n  \"setting_basic\": \"基本设置\",\n  \"setting_basic_allow_progress_bar_seek\": \"允许通过底栏进度条调整播放进度\",\n  \"setting_basic_always_keep_statusbar_height\": \"总是保留状态栏高度\",\n  \"setting_basic_always_keep_statusbar_height_tip\": \"默认情况下，软件会动态判断是否需要为系统状态栏保留间距，但如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况，可以启用该选项以始终为系统状态栏保留空间。\",\n  \"setting_basic_animation\": \"弹出层随机动画\",\n  \"setting_basic_auto_hide_play_bar\": \"弹出键盘时自动隐藏播放栏\",\n  \"setting_basic_drawer_layout_position\": \"导航、收藏列表弹出方向\",\n  \"setting_basic_drawer_layout_position_left\": \"左侧\",\n  \"setting_basic_drawer_layout_position_right\": \"右侧\",\n  \"setting_basic_font_size\": \"字体大小设置（重启后生效）\",\n  \"setting_basic_font_size_100\": \"标准\",\n  \"setting_basic_font_size_110\": \"大\",\n  \"setting_basic_font_size_120\": \"更大\",\n  \"setting_basic_font_size_130\": \"非常大\",\n  \"setting_basic_font_size_80\": \"更小\",\n  \"setting_basic_font_size_90\": \"小\",\n  \"setting_basic_font_size_preview\": \"LX Music 字体大小预览\",\n  \"setting_basic_home_page_scroll\": \"启用竖屏首页横向滚动\",\n  \"setting_basic_lang\": \"语言\",\n  \"setting_basic_share_type\": \"分享方式\",\n  \"setting_basic_share_type_clipboard\": \"复制到剪贴板\",\n  \"setting_basic_share_type_system\": \"使用系统分享\",\n  \"setting_basic_show_animation\": \"显示动画效果\",\n  \"setting_basic_show_back_btn\": \"显示返回桌面按钮\",\n  \"setting_basic_show_exit_btn\": \"显示退出应用按钮\",\n  \"setting_basic_source\": \"自定义源\",\n  \"setting_basic_source_direct\": \"试听接口（这是最后的选择...）\",\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_title\": \"选择自定义源\",\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_startup_auto_play\": \"启动后自动播放音乐\",\n  \"setting_basic_startup_push_play_detail_screen\": \"启动后打开播放详情页\",\n  \"setting_basic_theme\": \"主题颜色\",\n  \"setting_basic_theme_auto_theme\": \"跟随系统亮、暗模式切换主题\",\n  \"setting_basic_theme_dynamic_bg\": \"使用动态背景\",\n  \"setting_basic_theme_font_shadow\": \"启用字体阴影\",\n  \"setting_basic_theme_hide_bg_dark\": \"隐藏黑色主题背景\",\n  \"setting_basic_theme_more_btn_show\": \"更多主题\",\n  \"setting_basic_use_system_file_selector\": \"使用系统文件选择器\",\n  \"setting_basic_use_system_file_selector_tip\": \"启用该选项后，导入备份文件、自定义源等操作将不需要申请存储权限，但可能在某些系统上不可用。\\n\\n若启用该选项后无法导入文件，则可关闭该选项，回退到软件内置文件选择器。\",\n  \"setting_dislike_list_input_tip\": \"歌曲名@艺术家\\n歌曲名\\n@艺术家\",\n  \"setting_dislike_list_tips\": \"1. 每条一行，若歌曲或者艺术家名字中存在「@」符号，需要将其替换成「#」\\n2. 指定某艺术家的某首歌：歌曲名@艺术家\\n3. 指定某首歌：歌曲名\\n4. 指定某艺术家：@艺术家\",\n  \"setting_list\": \"列表设置\",\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_show interval\": \"显示歌曲时长\",\n  \"setting_list_show_album_name\": \"显示歌曲专辑名\",\n  \"setting_lyric_desktop\": \"桌面歌词\",\n  \"setting_lyric_desktop_enable\": \"显示歌词\",\n  \"setting_lyric_desktop_lock\": \"锁定歌词\",\n  \"setting_lyric_desktop_maxlineNum\": \"最大行数\",\n  \"setting_lyric_desktop_permission_tip\": \"桌面歌词功能需要在系统权限设置中授予 LX Music 显示悬浮窗口的权限才能使用，是否去相关界面授予该权限？\",\n  \"setting_lyric_desktop_single_line\": \"使用单行歌词\",\n  \"setting_lyric_desktop_text_opacity\": \"歌词字体透明度\",\n  \"setting_lyric_desktop_text_size\": \"歌词字体大小\",\n  \"setting_lyric_desktop_text_x\": \"歌词水平对齐方式\",\n  \"setting_lyric_desktop_text_x_center\": \"居中\",\n  \"setting_lyric_desktop_text_x_left\": \"居左\",\n  \"setting_lyric_desktop_text_x_right\": \"居右\",\n  \"setting_lyric_desktop_text_y\": \"歌词垂直对齐方式\",\n  \"setting_lyric_desktop_text_y_bottom\": \"居底\",\n  \"setting_lyric_desktop_text_y_center\": \"居中\",\n  \"setting_lyric_desktop_text_y_top\": \"居顶\",\n  \"setting_lyric_desktop_theme\": \"歌词主题色\",\n  \"setting_lyric_desktop_toggle_anima\": \"显示歌词切换动画\",\n  \"setting_lyric_desktop_view_width\": \"窗口百分比宽度\",\n  \"setting_other\": \"其他\",\n  \"setting_other_cache\": \"缓存管理（包括歌曲、歌词、错误日志等缓存，若没有歌曲播放相关的问题则不建议清理）\",\n  \"setting_other_cache_clear_btn\": \"清理缓存\",\n  \"setting_other_cache_clear_success_tip\": \"缓存清理完成\",\n  \"setting_other_cache_getting\": \"统计缓存中...\",\n  \"setting_other_cache_size\": \"当前已用缓存大小：\",\n  \"setting_other_dislike_list_show_btn\": \"编辑规则\",\n  \"setting_other_log\": \"错误日志（运行发生异常时的日志）\",\n  \"setting_other_log_btn_clean\": \"清空\",\n  \"setting_other_log_btn_hide\": \"关闭\",\n  \"setting_other_log_btn_show\": \"查看日志\",\n  \"setting_other_log_sync_log\": \"记录同步日志\",\n  \"setting_other_log_tip_clean_success\": \"日志清理完成\",\n  \"setting_other_log_tip_null\": \"日志是空的哦~\",\n  \"setting_other_log_user_api_log\": \"记录自定义源日志\",\n  \"setting_play_audio_offload\": \"启用音频卸载\",\n  \"setting_play_audio_offload_tip\": \"启用音频卸载可以节省耗电量，但在某些设备上可能会出现所有歌曲都提示「音频加载出错」或「无法完整播放整首歌」的问题，这是由于当前系统的 bug 导致的。\\n\\n对于遇到这个问题的人可以关闭该选项后完全重启应用再试。\",\n  \"setting_play_auto_clean_played_list\": \"自动清空已播放列表\",\n  \"setting_play_auto_clean_played_list_tip\": \"随机播放模式下，通过点击「与播放列表相同的列表」内的歌曲切歌时，若启用「自动清空已播放列表」，则已播放的歌曲将重新参与随机播放。\",\n  \"setting_play_cache_size\": \"最大缓存大小（MB）\",\n  \"setting_play_cache_size_no_cache\": \"禁用缓存\",\n  \"setting_play_cache_size_save_tip\": \"缓存设置完毕，重启应用后生效\",\n  \"setting_play_handle_audio_focus\": \"其他应用播放声音时，自动暂停播放\",\n  \"setting_play_handle_audio_focus_tip\": \"重启应用后生效\",\n  \"setting_play_lyric_transition\": \"显示歌词翻译\",\n  \"setting_play_play_quality\": \"优先播放的音质（如果可用）\",\n  \"setting_play_s2t\": \"将播放的中文歌词转换为繁体\",\n  \"setting_play_save_play_time\": \"记住播放进度\",\n  \"setting_play_show_bluetooth_lyric\": \"显示蓝牙歌词\",\n  \"setting_play_show_notification_image\": \"在通知栏显示歌曲专辑封面图片\",\n  \"setting_play_show_roma\": \"显示歌词罗马音（如果可用）\",\n  \"setting_play_show_translation\": \"显示歌词翻译（如果可用）\",\n  \"setting_player\": \"播放设置\",\n  \"setting_player_save_play_time\": \"记住播放进度\",\n  \"setting_search\": \"搜索设置\",\n  \"setting_search_show_history_search\": \"显示历史搜索记录\",\n  \"setting_search_show_hot_search\": \"显示热门搜索\",\n  \"setting_sync\": \"数据同步\",\n  \"setting_sync_address\": \"当前设备地址：{address}\",\n  \"setting_sync_code_blocked_ip\": \"当前设备的 IP 已被服务端封禁！\",\n  \"setting_sync_code_fail\": \"连接码无效\",\n  \"setting_sync_code_input_tip\": \"请输入连接码\",\n  \"setting_sync_code_label\": \"首次连接需要输入连接码\",\n  \"setting_sync_enable\": \"启用同步\",\n  \"setting_sync_history\": \"历史地址\",\n  \"setting_sync_history_empty\": \"这里啥也没有😮\",\n  \"setting_sync_history_title\": \"连接历史\",\n  \"setting_sync_host_label\": \"同步服务地址\",\n  \"setting_sync_host_value_error_tip\": \"地址需要以「http://」或「https://」开头！\",\n  \"setting_sync_host_value_tip\": \"http://<IP 地址>:<端口号>\",\n  \"setting_sync_port_label\": \"同步服务端口号\",\n  \"setting_sync_port_tip\": \"请输入同步服务端口号\",\n  \"setting_sync_status\": \"状态：{status}\",\n  \"setting_sync_status_enabled\": \"已连接\",\n  \"setting_theme\": \"主题设置\",\n  \"setting_version\": \"软件更新\",\n  \"setting_version_show_ver_modal\": \"打开更新窗口 🚀\",\n  \"share_card_title_music\": \"将「{name}」分享到...\",\n  \"share_title_music\": \"歌曲分享\",\n  \"singer\": \"艺术家：{name}\",\n  \"songlist_hot\": \"最热\",\n  \"songlist_hot_collect\": \"热藏\",\n  \"songlist_new\": \"最新\",\n  \"songlist_open\": \"打开\",\n  \"songlist_open_input_placeholder\": \"输入歌单链接或歌单 ID\",\n  \"songlist_open_input_tip\": \"1. 不支持跨源打开歌单，请确认要打开的歌单与当前选择的歌单来源是否对应\\n2. 若遇到无法打开的歌单链接，欢迎反馈\\n3. 酷狗源歌单不支持用歌单 ID 或概念版链接打开，但支持用普通版链接或酷狗码打开\",\n  \"songlist_recommend\": \"推荐\",\n  \"songlist_rise\": \"飙升\",\n  \"songlist_tag_default\": \"默认\",\n  \"songlist_tag_hot\": \"热门\",\n  \"songlist_tags\": \"歌单类别\",\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_real_all\": \"聚合搜索\",\n  \"source_real_bd\": \"百度音乐\",\n  \"source_real_kg\": \"酷狗音乐\",\n  \"source_real_kw\": \"酷我音乐\",\n  \"source_real_mg\": \"咪咕音乐\",\n  \"source_real_tx\": \"企鹅音乐\",\n  \"source_real_wy\": \"网易音乐\",\n  \"stop\": \"停止\",\n  \"stopped\": \"已停止\",\n  \"storage_file_no_match\": \"选择的文件不符合要求！\",\n  \"storage_file_no_select_file_failed_tip\": \"使用系统文件选择器选择文件失败，是否回退到软件内置文件选择器？\",\n  \"storage_permission_tip_disagree\": \"你个骗纸，刚刚问你，你都说同意的，最后又拒绝，哼 🥺\",\n  \"storage_permission_tip_disagree_ask_again\": \"该功能无法使用，因为你已经永久拒绝洛雪访问手机存储😫。\\n若想继续，你需要去👉系统权限管理👈将洛雪的存储权限设置为「允许」\",\n  \"storage_permission_tip_request\": \"使用该功能需要允许洛雪访问手机存储，是否同意并继续？\",\n  \"sync__dislike_mode_merge_tip_desc\": \"合并两边列表内容并去重\",\n  \"sync__dislike_mode_other_tip_desc\": \"「取消同步」将不使用「不喜欢的歌曲」列表同步功能\",\n  \"sync__dislike_mode_overwrite_tip_desc\": \"被覆盖者的列表将被替换成覆盖者的列表\",\n  \"sync__dislike_mode_title\": \"选择与「{name}」的「不喜欢的歌曲」列表同步方式\",\n  \"sync__list_mode_merge_tip_desc\": \"将两边的列表合并到一起，相同的歌曲将被去掉（去掉的是被合并者的歌曲），不同的歌曲将被添加。\",\n  \"sync__list_mode_other_tip_desc\": \"“取消同步”将不使用列表同步功能。\",\n  \"sync__list_mode_overwrite_tip_desc\": \"被覆盖者与覆盖者列表 ID 相同的列表将被删除后替换成覆盖者的列表（列表 ID 不同的列表将被合并到一起）。若勾选「完全覆盖」，则被覆盖者的所有列表将被移除，然后替换成覆盖者的列表。\",\n  \"sync__list_mode_title\": \"选择与「{name}」的列表同步方式\",\n  \"sync__mode_merge_btn_local_remote\": \"「本机列表」合并「远程列表」\",\n  \"sync__mode_merge_btn_remote_local\": \"「远程列表」合并「本机列表」\",\n  \"sync__mode_merge_tip\": \"合并：\",\n  \"sync__mode_other_label\": \"其他\",\n  \"sync__mode_other_tip\": \"其他：\",\n  \"sync__mode_overwrite\": \"完全覆盖\",\n  \"sync__mode_overwrite_btn_cancel\": \"取消同步\",\n  \"sync__mode_overwrite_btn_local_remote\": \"「本机列表」覆盖「远程列表」\",\n  \"sync__mode_overwrite_btn_remote_local\": \"「远程列表」覆盖「本机列表」\",\n  \"sync__mode_overwrite_label\": \"覆盖\",\n  \"sync__mode_overwrite_tip\": \"覆盖：\",\n  \"sync_status_disabled\": \"未连接\",\n  \"theme_black\": \"黑灯瞎火\",\n  \"theme_blue\": \"蓝田生玉\",\n  \"theme_blue2\": \"清热版蓝\",\n  \"theme_blue_plus\": \"蛋雅深蓝\",\n  \"theme_brown\": \"泥牛入海\",\n  \"theme_china_ink\": \"近墨者黑\",\n  \"theme_green\": \"绿意盎然\",\n  \"theme_grey\": \"灰常美丽\",\n  \"theme_happy_new_year\": \"新年快乐\",\n  \"theme_mid_autumn\": \"月里嫦娥\",\n  \"theme_ming\": \"青出于黑\",\n  \"theme_naruto\": \"木叶之村\",\n  \"theme_orange\": \"橙黄橘绿\",\n  \"theme_pink\": \"粉装玉琢\",\n  \"theme_purple\": \"重斤球紫\",\n  \"theme_red\": \"热情似火\",\n  \"timeout_exit_btn_cancel\": \"取消定时\",\n  \"timeout_exit_btn_update\": \"更新定时\",\n  \"timeout_exit_btn_wait_cancel\": \"取消退出\",\n  \"timeout_exit_btn_wait_tip\": \"计时已结束，正在等待退出...\",\n  \"timeout_exit_input_tip\": \"输入倒计时分钟数\",\n  \"timeout_exit_label_isPlayed\": \"等待歌曲播放完毕再停止播放\",\n  \"timeout_exit_min\": \"分钟\",\n  \"timeout_exit_tip_cancel\": \"已取消定时停止播放\",\n  \"timeout_exit_tip_max\": \"最多只能设置 {num} 分钟哦\",\n  \"timeout_exit_tip_off\": \"设置定时停止播放\",\n  \"timeout_exit_tip_on\": \"{time} 后停止播放\",\n  \"toggle_source\": \"歌曲换源\",\n  \"toggle_source_failed\": \"换源失败，请尝试手动在搜索页指定其他来源搜索该歌曲播放\",\n  \"toggle_source_try\": \"尝试切换到其他来源...\",\n  \"understand\": \"已了解 👌\",\n  \"user_api__init_failed_alert\": \"自定义源「{name}」初始化失败：\",\n  \"user_api_add_failed_tip\": \"无效的自定义源文件\",\n  \"user_api_allow_show_update_alert\": \"允许显示更新弹窗\",\n  \"user_api_btn_import\": \"导入\",\n  \"user_api_btn_import_local\": \"本地导入\",\n  \"user_api_btn_import_online\": \"在线导入\",\n  \"user_api_btn_import_online_input_confirm\": \"导入\",\n  \"user_api_btn_import_online_input_loading\": \"导入中...\",\n  \"user_api_btn_import_online_input_tip\": \"请输入 HTTP 链接\",\n  \"user_api_empty\": \"这里竟然是空的 😲\",\n  \"user_api_import_desc\": \"选择自定义源文件\",\n  \"user_api_import_failed_tip\": \"自定义源导入失败：\\n{message}\",\n  \"user_api_import_success_tip\": \"导入成功 🎉\",\n  \"user_api_max_tip\": \"最多只能同时存在 20 个源哦🤪\\n想要继续导入的话，请先移除一些旧的源腾出位置吧\",\n  \"user_api_note\": \"提示：虽然我们已经尽可能地隔离了脚本的运行环境，但导入包含恶意行为的脚本仍可能会影响你的系统，请谨慎导入。\",\n  \"user_api_readme\": \"源编写说明：\",\n  \"user_api_remove_tip\": \"你真的要移除「{name}」吗？\",\n  \"user_api_title\": \"自定义源管理（实验性）\",\n  \"user_api_update_alert\": \"自定义源「{name}」发现新版本\",\n  \"user_api_update_alert_open_url\": \"打开更新地址\",\n  \"version_btn_close\": \"关闭\",\n  \"version_btn_downloading\": \"正在努力下载中... {current}/{total} ({progress}%)\",\n  \"version_btn_failed\": \"重试\",\n  \"version_btn_ignore\": \"忽略\",\n  \"version_btn_ignore_cancel\": \"取消忽略\",\n  \"version_btn_min\": \"后台下载\",\n  \"version_btn_new\": \"更新\",\n  \"version_btn_unknown\": \"项目首页\",\n  \"version_btn_update\": \"安装\",\n  \"version_label_change_log\": \"更新说明：\",\n  \"version_label_current_ver\": \"当前版本：\",\n  \"version_label_history\": \"历史版本：\",\n  \"version_label_latest_ver\": \"最新版本：\",\n  \"version_tip_checking\": \"检查更新中...⏳\",\n  \"version_tip_downloaded\": \"安装包已经下载完毕。\",\n  \"version_tip_failed\": \"安装包下载失败，你可以重试或者去项目地址手动下载新版更新。\\n💡提示：一般多点几次重试即可正常更新！\",\n  \"version_tip_latest\": \"软件已是最新，尽情地体验吧~🥂\",\n  \"version_tip_min\": \"已切换到后台下载，你可以去「设置 → 软件更新」重新打开本弹窗哦\",\n  \"version_tip_unknown\": \"获取最新版本信息失败，建议手动去项目地址检查是否有新版本\",\n  \"version_title_checking\": \"⏳ 检查更新中 ⏳\",\n  \"version_title_failed\": \"❌ 下载失败 ❌\",\n  \"version_title_latest\": \"🎉 当前版本已是最新 🎊\",\n  \"version_title_new\": \"🌟 发现新版本 🌟\",\n  \"version_title_unknown\": \"❓ 获取最新版本信息失败 ❓\",\n  \"version_title_update\": \"🚀 软件更新 🚀\"\n}\n"
  },
  {
    "path": "src/lang/zh-tw.json",
    "content": "{\n  \"add_to\": \"添加到...\",\n  \"agree\": \"好好好\",\n  \"agree_go\": \"去開啟\",\n  \"agree_to\": \"去設定\",\n  \"back\": \"返回\",\n  \"back_home\": \"返回桌布\",\n  \"cancel\": \"取消\",\n  \"cancel_button_text_2\": \"不不不，點錯了\",\n  \"change_position\": \"調整位置\",\n  \"change_position_list_title\": \"調整清單位置\",\n  \"change_position_music_multi_title\": \"將已選的 {num} 首歌曲的位置調整到\",\n  \"change_position_music_title\": \"將「{name}」的位置調整到\",\n  \"change_position_tip\": \"請輸入新的位置\",\n  \"close\": \"關閉\",\n  \"collect\": \"收藏\",\n  \"collect_songlist\": \"收藏歌單\",\n  \"collect_success\": \"收藏成功\",\n  \"collect_toplist\": \"收藏排行榜\",\n  \"comment_hide_text\": \"收起評論\",\n  \"comment_not support\": \"該歌曲不支援獲取評論\",\n  \"comment_refresh\": \"這已經是「{name}」的評論啦\",\n  \"comment_show_image\": \"顯示圖片\",\n  \"comment_show_text\": \"展開評論\",\n  \"comment_tab_hot\": \"熱門 {total}\",\n  \"comment_tab_new\": \"最新 {total}\",\n  \"comment_title\": \"「{name}」的評論\",\n  \"confirm\": \"確認\",\n  \"confirm_button_text\": \"是的\",\n  \"confirm_tip\": \"再確認一下，你真的要這樣做嗎？\",\n  \"copy_name\": \"分享歌曲\",\n  \"copy_name_tip\": \"已複製\",\n  \"create_new_folder\": \"建立資料夾\",\n  \"create_new_folder_error_tip\": \"輸入的名稱不合法\",\n  \"create_new_folder_tip\": \"請輸入新資料夾名\",\n  \"date_format_hour\": \"{num} 小時前\",\n  \"date_format_minute\": \"{num} 分鐘前\",\n  \"date_format_second\": \"{num} 秒前\",\n  \"deep_link__handle_error_tip\": \"呼叫失敗：{message}\",\n  \"deep_link_file_js_confirm_tip\": \"確認要匯入自訂來源 API「{name}」嗎？\",\n  \"deep_link_file_lxmc_confirm_tip\": \"確認要匯入清單檔案「{name}」嗎？\",\n  \"delete\": \"移除\",\n  \"dialog_cancel\": \"我不\",\n  \"dialog_confirm\": \"好的\",\n  \"disagree\": \"我就不\",\n  \"disagree_tip\": \"那算了...🙄\",\n  \"dislike\": \"不喜歡\",\n  \"duplicate_list_tip\": \"你之前已收藏過清單「{name}」，是否需要更新裡面的歌曲？\",\n  \"edit_metadata\": \"編輯標籤\",\n  \"exit_app_tip\": \"確定要退出應用嗎？\",\n  \"ignoring_battery_optimization_check_tip\": \"LX Music 沒有在「忽略電池效能最佳化」的白名單中，這可能會導致在後台播放音樂時被系統暫停。是否將 LX Music 加入該白名單中？\",\n  \"ignoring_battery_optimization_check_title\": \"後台執行權限設定提醒\",\n  \"input_error\": \"不要亂寫好吧😡\",\n  \"list_add_btn_title\": \"把該歌曲添加到「{name}」\",\n  \"list_add_tip_exists\": \"清單已經存在這首歌啦，不要再點我啦~😡\",\n  \"list_add_title_first_add\": \"添加\",\n  \"list_add_title_first_move\": \"移動\",\n  \"list_add_title_last\": \"到...\",\n  \"list_create\": \"建立清單\",\n  \"list_create_input_placeholder\": \"你想起什麼名...\",\n  \"list_duplicate_tip\": \"已存在同名清單，是否繼續建立？\",\n  \"list_edit_action_tip_add_failed\": \"添加失敗\",\n  \"list_edit_action_tip_add_success\": \"添加成功\",\n  \"list_edit_action_tip_exist\": \"該清單已經有這首歌啦\",\n  \"list_edit_action_tip_move_failed\": \"移動失敗\",\n  \"list_edit_action_tip_move_success\": \"移動成功\",\n  \"list_edit_action_tip_remove_success\": \"移除成功\",\n  \"list_end\": \"到底啦~\",\n  \"list_error\": \"載入失敗😥，點擊嘗試重新載入\",\n  \"list_export\": \"匯出\",\n  \"list_export_part_desc\": \"選取清單檔案儲存位置\",\n  \"list_import\": \"匯入\",\n  \"list_import_part_button_cancel\": \"不要啊\",\n  \"list_import_part_button_confirm\": \"覆蓋掉\",\n  \"list_import_part_confirm\": \"匯入的清單「{importName}」與本機清單「{localName}」的 ID 相同，是否覆蓋本機清單？\",\n  \"list_import_part_confirm_tip\": \"備份文件中列表與現有列表 ID 相同時，現有列表內歌曲將被覆蓋，是否繼續？ \\n若你不確定後果，建議先備份現有列表再來繼續！\",\n  \"list_import_part_desc\": \"選取清單檔案\",\n  \"list_import_tip__alldata\": \"這是一個「所有資料」備份檔案，你需要去這裡匯入：\\n\\n設定 → 備份與復原 → 清單資料 → 匯入清單\",\n  \"list_import_tip__failed\": \"匯入失敗\",\n  \"list_import_tip__playlist\": \"這是一個「清單」備份檔案，你需要去這裡匯入：\\n\\n設定 → 備份與復原 → 清單資料 → 匯入清單\",\n  \"list_import_tip__playlist_part\": \"這是一個「僅清單」檔案，你需要去這裡匯入：\\n\\n我的清單 → 點擊任意一個清單名右側的選單按鈕 → 在彈出的選單中選取「匯入」\",\n  \"list_import_tip__setting\": \"這是一個「設定」備份檔案，行動版不支援匯入此類檔案\",\n  \"list_import_tip__unknown\": \"未知的檔案類型，請嘗試升級到最新版本後再試\",\n  \"list_loading\": \"載入中...\",\n  \"list_multi_add_title_first_add\": \"添加已選的\",\n  \"list_multi_add_title_first_move\": \"移動已選的\",\n  \"list_multi_add_title_last\": \"首歌曲到...\",\n  \"list_name_default\": \"試聽清單\",\n  \"list_name_love\": \"我的最愛\",\n  \"list_name_temp\": \"臨時清單\",\n  \"list_remove\": \"移除\",\n  \"list_remove_music_multi_tip\": \"你真的想要移除所選的 {num} 首歌曲嗎？\",\n  \"list_remove_tip\": \"你真的想要移除「{name}」嗎？\",\n  \"list_remove_tip_button\": \"是的，沒錯\",\n  \"list_rename\": \"重新命名\",\n  \"list_rename_title\": \"重新命名清單\",\n  \"list_select_all\": \"全選\",\n  \"list_select_cancel\": \"取消\",\n  \"list_select_local_file\": \"添加本機歌曲\",\n  \"list_select_local_file_desc\": \"選取本機歌曲資料夾\",\n  \"list_select_local_file_empty_tip\": \"沒有在目前資料夾中發現歌曲\",\n  \"list_select_local_file_result_failed_tip\": \"共發現 {total} 首歌曲，成功添加 {success} 首，失敗 {failed} 首，可到錯誤日誌查看添加失敗的歌曲\",\n  \"list_select_local_file_result_tip\": \"共發現 {total} 首歌曲，已全部添加！\",\n  \"list_select_local_file_temp_add_tip\": \"共找到 {total} 個符合要求的檔案，已快速添加到目前清單，現在將進入檔案標籤讀取流程，請勿退出應用！\",\n  \"list_select_range\": \"區間\",\n  \"list_select_single\": \"單選\",\n  \"list_select_unall\": \"反選\",\n  \"list_sort\": \"排序歌曲\",\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_sync\": \"更新\",\n  \"list_sync_confirm_tip\": \"這將會把「{name}」內的歌曲取代成線上清單的歌曲，你確認要更新嗎？\",\n  \"list_update_error\": \"「{name}」更新失敗\",\n  \"list_update_success\": \"「{name}」更新成功\",\n  \"list_updating\": \"更新中\",\n  \"lists__duplicate\": \"重複歌曲\",\n  \"lists_dislike_music_add_tip\": \"已添加\",\n  \"lists_dislike_music_singer_tip\": \"你真的不喜歡「{singer}」的「{name}」嗎？\",\n  \"lists_dislike_music_tip\": \"你真的不喜歡「{name}」嗎？\",\n  \"load_failed\": \"啊~載入失敗了😥\",\n  \"loading\": \"載入中...\",\n  \"location\": \"來自{location}\",\n  \"lyric__load_error\": \"歌詞獲取失敗\",\n  \"metadata_edit_modal_confirm\": \"儲存\",\n  \"metadata_edit_modal_failed\": \"儲存失敗，詳情可查看錯誤日誌\",\n  \"metadata_edit_modal_file_name\": \"檔案名\",\n  \"metadata_edit_modal_file_path\": \"檔案路徑\",\n  \"metadata_edit_modal_form_album_name\": \"專輯\",\n  \"metadata_edit_modal_form_lyric\": \"LRC 歌詞\",\n  \"metadata_edit_modal_form_match_lyric\": \"線上匹配\",\n  \"metadata_edit_modal_form_match_lyric_failed\": \"線上歌詞匹配失敗\",\n  \"metadata_edit_modal_form_match_lyric_success\": \"歌詞匹配成功🎉\",\n  \"metadata_edit_modal_form_match_pic\": \"線上匹配\",\n  \"metadata_edit_modal_form_match_pic_failed\": \"線上匹配封面失敗\",\n  \"metadata_edit_modal_form_match_pic_success\": \"封面匹配成功🎉\",\n  \"metadata_edit_modal_form_name\": \"歌曲名\",\n  \"metadata_edit_modal_form_parse_name\": \"從檔案名解析標題和演出者\",\n  \"metadata_edit_modal_form_parse_name_singer\": \"標題 - 演出者\",\n  \"metadata_edit_modal_form_parse_singer_name\": \"演出者 - 標題\",\n  \"metadata_edit_modal_form_pic\": \"歌曲專輯封面\",\n  \"metadata_edit_modal_form_remove_lyric\": \"清空\",\n  \"metadata_edit_modal_form_remove_pic\": \"移除圖片\",\n  \"metadata_edit_modal_form_select_pic\": \"選取圖片\",\n  \"metadata_edit_modal_form_select_pic_title\": \"選取歌曲專輯封面圖片\",\n  \"metadata_edit_modal_form_singer\": \"演出者\",\n  \"metadata_edit_modal_processing\": \"寫入中...\",\n  \"metadata_edit_modal_success\": \"儲存成功\",\n  \"metadata_edit_modal_tip\": \"歌曲名不能為空\",\n  \"metadata_edit_modal_title\": \"編輯歌曲標籤\",\n  \"move_to\": \"移動到...\",\n  \"music_source_detail\": \"歌曲詳情頁\",\n  \"music_toggle__confirm\": \"確認\",\n  \"music_toggle__duplicate_tip\": \"列表中已存在相同的歌曲，是否將其移除並繼續？\",\n  \"name\": \"標題：{name}\",\n  \"nav_exit\": \"退出應用\",\n  \"nav_love\": \"我的清單\",\n  \"nav_search\": \"搜尋\",\n  \"nav_setting\": \"設定\",\n  \"nav_songlist\": \"歌單\",\n  \"nav_top\": \"排行榜\",\n  \"never_show\": \"不再提醒\",\n  \"no_item\": \"清單竟然是空的...\",\n  \"notifications_check_tip\": \"你沒有允許 LX Music 顯示通知，或 LX Music 通知設定裡的「MusicService」通知被停用，這將無法使用通知欄進行暫停、切歌等操作，是否去開啟？\",\n  \"notifications_check_title\": \"通知權限提醒\",\n  \"ok\": \"我知道了\",\n  \"open_storage_error_tip\": \"輸入的路徑不合法\",\n  \"open_storage_not_found_title\": \"未找到外部記憶卡，請手動在下方輸入路徑以指定外部儲存器\",\n  \"open_storage_select_managed_folder_failed_tip\": \"選取儲存路徑失敗：{msg}\",\n  \"open_storage_select_path\": \"開啟儲存路徑\",\n  \"open_storage_select_path_tip\": \"提示：對於外部儲存，在授予儲存權限後仍然無法訪問時，可以點擊下面的按鈕選取允許訪問的路徑。\",\n  \"open_storage_tip\": \"輸入儲存路徑\",\n  \"open_storage_title\": \"請手動在下方輸入路徑以指定外部儲存器\",\n  \"parent_dir_name\": \"上一級資料夾\",\n  \"pause\": \"暫停\",\n  \"play\": \"播放\",\n  \"play_all\": \"播放全部\",\n  \"play_detail_setting_lrc_align\": \"歌詞對齊方式\",\n  \"play_detail_setting_lrc_align_center\": \"置中\",\n  \"play_detail_setting_lrc_align_left\": \"置左\",\n  \"play_detail_setting_lrc_align_right\": \"置右\",\n  \"play_detail_setting_lrc_font_size\": \"歌詞字體大小\",\n  \"play_detail_setting_playback_rate\": \"播放速率\",\n  \"play_detail_setting_playback_rate_reset\": \"重設\",\n  \"play_detail_setting_show_lyric_progress_setting\": \"允許透過拖曳歌詞調整播放進度\",\n  \"play_detail_setting_title\": \"播放器設定\",\n  \"play_detail_setting_volume\": \"音量大小\",\n  \"play_detail_todo_tip\": \"你想幹嘛？不可以的，這個功能還沒有實現哦😛，不過你可以試著長按來定位目前播放的歌曲（僅對播放「我的清單」裡的歌曲有效哦）\",\n  \"play_later\": \"稍後播放\",\n  \"play_list_loop\": \"重複播放清單\",\n  \"play_list_order\": \"順序播放\",\n  \"play_list_random\": \"隨機播放\",\n  \"play_next\": \"下一曲\",\n  \"play_prev\": \"上一曲\",\n  \"play_single\": \"停用歌曲切換\",\n  \"play_single_loop\": \"重複播放\",\n  \"player__buffering\": \"緩衝中...\",\n  \"player__end\": \"播放完畢\",\n  \"player__error\": \"音訊載入出錯，5 秒後切換下一首\",\n  \"player__getting_url\": \"歌曲連結獲取中...\",\n  \"player__getting_url_delay_retry\": \"伺服器繁忙，{time} 秒後重試...\",\n  \"player__loading\": \"音樂載入中...\",\n  \"player__refresh_url\": \"URL 過期，正在重新整理 URL...\",\n  \"player_cache_migrating\": \"歌曲快取遷移中，請稍等 ⌛️\",\n  \"quality_high_quality\": \"HQ\",\n  \"quality_lossless\": \"SQ\",\n  \"quality_lossless_24bit\": \"24bit\",\n  \"search__welcome\": \"搜我所想~~😉\",\n  \"search_history_search\": \"歷史搜尋\",\n  \"search_hot_search\": \"熱門搜尋\",\n  \"search_type_music\": \"歌曲\",\n  \"search_type_songlist\": \"歌單\",\n  \"setting__other_dislike_list\": \"「不喜歡的歌曲」規則\",\n  \"setting__other_dislike_list_label\": \"規則數量：{num}\",\n  \"setting__other_dislike_list_saved_tip\": \"已儲存\",\n  \"setting__other_lyric_raw_clear_btn\": \"清理歌詞快取\",\n  \"setting__other_lyric_raw_label\": \"歌詞數量：\",\n  \"setting__other_meta_cache\": \"其他快取管理\",\n  \"setting__other_music_url_clear_btn\": \"清理歌曲 URL 快取\",\n  \"setting__other_music_url_label\": \"歌曲 URL 數量：\",\n  \"setting__other_other_source_clear_btn\": \"清理變更來源歌曲快取\",\n  \"setting__other_other_source_label\": \"變更來源歌曲資訊數量：\",\n  \"setting__other_resource_cache\": \"資源快取管理（包括歌曲、圖片快取）\",\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_part\": \"清單資料（與桌面版清單備份檔案通用）\",\n  \"setting_backup_part_export_list\": \"匯出清單\",\n  \"setting_backup_part_export_list_desc\": \"選取歌單備份檔案儲存位置\",\n  \"setting_backup_part_export_list_tip_failed\": \"歌單匯出失敗\",\n  \"setting_backup_part_export_list_tip_success\": \"匯出成功\",\n  \"setting_backup_part_export_list_tip_zip\": \"📦檔案打包中...\\n若檔案太大可能需要一些時間⏳\",\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_desc\": \"選取清單備份檔案\",\n  \"setting_backup_part_import_list_tip_error\": \"清單匯入失敗😕\",\n  \"setting_backup_part_import_list_tip_running\": \"🚀正在努力匯入中...\\n若清單太大可能需要一些時間⏳\",\n  \"setting_backup_part_import_list_tip_success\": \"匯入成功🎉\",\n  \"setting_backup_part_import_list_tip_unzip\": \"📦檔案解析中...\\n若檔案太大可能需要一些時間⏳\",\n  \"setting_backup_part_import_setting\": \"匯入設定\",\n  \"setting_backup_part_import_setting_desc\": \"選取設定檔\",\n  \"setting_basic\": \"基本設定\",\n  \"setting_basic_allow_progress_bar_seek\": \"允許透過底欄進度條調整播放進度\",\n  \"setting_basic_always_keep_statusbar_height\": \"總是保留狀態欄高度\",\n  \"setting_basic_always_keep_statusbar_height_tip\": \"預設情況下，軟體會動態判斷是否需要為系統狀態欄保留間距，但如果在你的裝置上出現軟體可互動內容與狀態欄內容顯示重疊的情況，可以啟用該選項以始終為系統狀態欄保留空間。\",\n  \"setting_basic_animation\": \"彈出層隨機動畫\",\n  \"setting_basic_auto_hide_play_bar\": \"彈出鍵盤時自動隱藏播放欄\",\n  \"setting_basic_drawer_layout_position\": \"導航、收藏清單彈出方向\",\n  \"setting_basic_drawer_layout_position_left\": \"左側\",\n  \"setting_basic_drawer_layout_position_right\": \"右側\",\n  \"setting_basic_font_size\": \"字體大小設定（重啟後生效）\",\n  \"setting_basic_font_size_100\": \"標準\",\n  \"setting_basic_font_size_110\": \"大\",\n  \"setting_basic_font_size_120\": \"更大\",\n  \"setting_basic_font_size_130\": \"非常大\",\n  \"setting_basic_font_size_80\": \"更小\",\n  \"setting_basic_font_size_90\": \"小\",\n  \"setting_basic_font_size_preview\": \"LX Music 字體大小預覽\",\n  \"setting_basic_home_page_scroll\": \"啟用豎屏首頁橫向滾動\",\n  \"setting_basic_lang\": \"語言\",\n  \"setting_basic_share_type\": \"分享方式\",\n  \"setting_basic_share_type_clipboard\": \"複製到剪貼簿\",\n  \"setting_basic_share_type_system\": \"使用系統分享\",\n  \"setting_basic_show_animation\": \"顯示動畫效果\",\n  \"setting_basic_show_back_btn\": \"顯示返回桌布按鈕\",\n  \"setting_basic_show_exit_btn\": \"顯示退出應用按鈕\",\n  \"setting_basic_source\": \"自訂來源 API\",\n  \"setting_basic_source_direct\": \"試聽 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_title\": \"選取自訂來源 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_startup_auto_play\": \"啟動後自動播放音樂\",\n  \"setting_basic_startup_push_play_detail_screen\": \"啟動後自動開啟播放詳情頁\",\n  \"setting_basic_theme\": \"主題顏色\",\n  \"setting_basic_theme_auto_theme\": \"跟隨系統亮、暗模式切換主題\",\n  \"setting_basic_theme_dynamic_bg\": \"使用動態背景\",\n  \"setting_basic_theme_font_shadow\": \"啟用字體陰影\",\n  \"setting_basic_theme_hide_bg_dark\": \"隱藏黑色主題背景\",\n  \"setting_basic_theme_more_btn_show\": \"更多主題\",\n  \"setting_basic_use_system_file_selector\": \"使用系統檔案選取器\",\n  \"setting_basic_use_system_file_selector_tip\": \"啟用該選項後，匯入備份檔案、自訂來源 API 等操作將不需要申請儲存權限，但可能在某些系統上不可用。\\n\\n若啟用該選項後無法匯入檔案，則可關閉該選項，回退到軟體內建檔案選取器。\",\n  \"setting_dislike_list_input_tip\": \"標題@演出者\\n標題\\n@演出者\",\n  \"setting_dislike_list_tips\": \"1. 每條一行，若歌曲或者演出者名字中存在「@」符號，需要將其取代成「#」\\n2. 指定某演出者的某首歌：標題@演出者\\n3. 指定某首歌：演出者\\n4. 指定某演出者：@演出者\",\n  \"setting_list\": \"清單設定\",\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_show interval\": \"顯示歌曲時長\",\n  \"setting_list_show_album_name\": \"顯示歌曲專輯\",\n  \"setting_lyric_desktop\": \"桌面歌詞\",\n  \"setting_lyric_desktop_enable\": \"顯示歌詞視窗\",\n  \"setting_lyric_desktop_lock\": \"鎖定歌詞\",\n  \"setting_lyric_desktop_maxlineNum\": \"最大行數\",\n  \"setting_lyric_desktop_permission_tip\": \"顯示歌詞視窗功能需要在系統權限設定中授予 LX Music「重疊顯示於其他應用程式上方」權限才能使用，是否去相關頁面授予該權限？\",\n  \"setting_lyric_desktop_single_line\": \"使用單行歌詞\",\n  \"setting_lyric_desktop_text_opacity\": \"歌詞字體透明度\",\n  \"setting_lyric_desktop_text_size\": \"歌詞字體大小\",\n  \"setting_lyric_desktop_text_x\": \"歌詞水平對齊方式\",\n  \"setting_lyric_desktop_text_x_center\": \"置中\",\n  \"setting_lyric_desktop_text_x_left\": \"置左\",\n  \"setting_lyric_desktop_text_x_right\": \"置右\",\n  \"setting_lyric_desktop_text_y\": \"歌詞垂直對齊方式\",\n  \"setting_lyric_desktop_text_y_bottom\": \"居底\",\n  \"setting_lyric_desktop_text_y_center\": \"置中\",\n  \"setting_lyric_desktop_text_y_top\": \"居頂\",\n  \"setting_lyric_desktop_theme\": \"歌詞主題色\",\n  \"setting_lyric_desktop_toggle_anima\": \"顯示歌詞切換動畫\",\n  \"setting_lyric_desktop_view_width\": \"視窗百分比寬度\",\n  \"setting_other\": \"其他\",\n  \"setting_other_cache\": \"快取管理（包括歌曲、歌詞、錯誤日誌等快取，若沒有歌曲播放相關的問題則不建議清理）\",\n  \"setting_other_cache_clear_btn\": \"清理快取\",\n  \"setting_other_cache_clear_success_tip\": \"快取清理完成\",\n  \"setting_other_cache_getting\": \"統計快取中...\",\n  \"setting_other_cache_size\": \"目前已用快取大小：\",\n  \"setting_other_dislike_list_show_btn\": \"編輯規則\",\n  \"setting_other_log\": \"錯誤日誌（執行發生異常時的日誌）\",\n  \"setting_other_log_btn_clean\": \"清空\",\n  \"setting_other_log_btn_hide\": \"關閉\",\n  \"setting_other_log_btn_show\": \"查看日誌\",\n  \"setting_other_log_sync_log\": \"記錄同步日誌\",\n  \"setting_other_log_tip_clean_success\": \"日誌清理完成\",\n  \"setting_other_log_tip_null\": \"日誌是空的哦~\",\n  \"setting_other_log_user_api_log\": \"記錄自訂來源 API 日誌\",\n  \"setting_play_audio_offload\": \"啟用音訊移除\",\n  \"setting_play_audio_offload_tip\": \"啟用音訊移除可以節省耗電量，但在某些裝置上可能會出現所有歌曲都提示「音訊載入出錯」或「無法完整播放整首歌」的問題，這是由於目前系統的 bug 導致的。\\n\\n對於遇到這個問題的人可以關閉該選項後完全重啟應用再試。\",\n  \"setting_play_auto_clean_played_list\": \"自動清空已播放清單\",\n  \"setting_play_auto_clean_played_list_tip\": \"隨機播放模式下，透過點擊「與播放清單相同的清單」內的歌曲切歌時，若啟用「自動清空已播放清單」，則已播放的歌曲將重新參與隨機播放。\",\n  \"setting_play_cache_size\": \"最大快取大小（MB）\",\n  \"setting_play_cache_size_no_cache\": \"停用快取\",\n  \"setting_play_cache_size_save_tip\": \"快取設定完畢，重啟應用後生效\",\n  \"setting_play_handle_audio_focus\": \"其他應用播放聲音時，自動暫停播放\",\n  \"setting_play_handle_audio_focus_tip\": \"重啟應用後生效\",\n  \"setting_play_lyric_transition\": \"顯示歌詞翻譯\",\n  \"setting_play_play_quality\": \"優先播放的音質（如果可用）\",\n  \"setting_play_s2t\": \"將播放的中文歌詞轉換為繁體\",\n  \"setting_play_save_play_time\": \"記住播放進度\",\n  \"setting_play_show_bluetooth_lyric\": \"顯示藍牙歌詞\",\n  \"setting_play_show_notification_image\": \"在通知欄顯示歌曲專輯封面圖片\",\n  \"setting_play_show_roma\": \"顯示歌詞羅馬音（如果可用）\",\n  \"setting_play_show_translation\": \"顯示歌詞翻譯（如果可用）\",\n  \"setting_player\": \"播放設定\",\n  \"setting_player_save_play_time\": \"記住播放進度\",\n  \"setting_search\": \"搜尋設定\",\n  \"setting_search_show_history_search\": \"顯示歷史搜尋記錄\",\n  \"setting_search_show_hot_search\": \"顯示熱門搜尋\",\n  \"setting_sync\": \"資料同步\",\n  \"setting_sync_address\": \"目前裝置位址：{address}\",\n  \"setting_sync_code_blocked_ip\": \"目前裝置的 IP 已被服務端封禁！\",\n  \"setting_sync_code_fail\": \"連線碼無效\",\n  \"setting_sync_code_input_tip\": \"請輸入連線碼\",\n  \"setting_sync_code_label\": \"首次連線需要輸入連線碼\",\n  \"setting_sync_enable\": \"啟用同步\",\n  \"setting_sync_history\": \"歷史位址\",\n  \"setting_sync_history_empty\": \"這裡什麼也沒有😮\",\n  \"setting_sync_history_title\": \"連線歷史\",\n  \"setting_sync_host_label\": \"同步服務位址\",\n  \"setting_sync_host_value_error_tip\": \"位址需要以「http://」或「https://」開頭！\",\n  \"setting_sync_host_value_tip\": \"http://<IP 位址>:<埠號>\",\n  \"setting_sync_port_label\": \"同步服務埠號\",\n  \"setting_sync_port_tip\": \"請輸入同步服務埠號\",\n  \"setting_sync_status\": \"狀態：{status}\",\n  \"setting_sync_status_enabled\": \"已連線\",\n  \"setting_theme\": \"主題設定\",\n  \"setting_version\": \"軟體更新\",\n  \"setting_version_show_ver_modal\": \"開啟更新視窗 🚀\",\n  \"share_card_title_music\": \"將「{name}」分享到...\",\n  \"share_title_music\": \"歌曲分享\",\n  \"singer\": \"演出者：{name}\",\n  \"songlist_hot\": \"最熱\",\n  \"songlist_hot_collect\": \"熱藏\",\n  \"songlist_new\": \"最新\",\n  \"songlist_open\": \"開啟\",\n  \"songlist_open_input_placeholder\": \"輸入歌單連結或歌單 ID\",\n  \"songlist_open_input_tip\": \"1. 不支援跨平台開啟歌單，請確認要開啟的歌單與目前歌單的來源平台是否對應\\n2. 若遇到無法開啟的歌單連結，歡迎回報\\n3. 酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟，但支援用普通版連結與酷狗碼開啟\",\n  \"songlist_recommend\": \"推薦\",\n  \"songlist_rise\": \"飆升\",\n  \"songlist_tag_default\": \"預設\",\n  \"songlist_tag_hot\": \"熱門\",\n  \"songlist_tags\": \"歌單類別\",\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_real_all\": \"聚合搜尋\",\n  \"source_real_bd\": \"百度音樂\",\n  \"source_real_kg\": \"酷狗音樂\",\n  \"source_real_kw\": \"酷我音樂\",\n  \"source_real_mg\": \"咪咕音樂\",\n  \"source_real_tx\": \"企鵝音樂\",\n  \"source_real_wy\": \"網易音樂\",\n  \"stop\": \"停止\",\n  \"stopped\": \"已停止\",\n  \"storage_file_no_match\": \"選取的檔案不符合要求！\",\n  \"storage_file_no_select_file_failed_tip\": \"使用系統檔案選取器選取檔案失敗，是否回退到軟體內建檔案選取器？\",\n  \"storage_permission_tip_disagree\": \"你個騙紙，剛剛問你，你都說同意的，最後又拒絕，哼 🥺\",\n  \"storage_permission_tip_disagree_ask_again\": \"該功能無法使用，因為你已經永久拒絕洛雪訪問手機儲存😫。\\n若想繼續，你需要去👉系統權限管理👈將洛雪的儲存權限設定為「允許」\",\n  \"storage_permission_tip_request\": \"使用該功能需要允許洛雪訪問手機儲存，是否同意並繼續？\",\n  \"sync__dislike_mode_merge_tip_desc\": \"合併兩邊清單內容並去重\",\n  \"sync__dislike_mode_other_tip_desc\": \"「取消同步」將不使用「不喜歡的歌曲」清單同步功能\",\n  \"sync__dislike_mode_overwrite_tip_desc\": \"被覆蓋者的清單將被取代成覆蓋者的清單\",\n  \"sync__dislike_mode_title\": \"選取與「{name}」的「不喜歡的歌曲」清單同步方式\",\n  \"sync__list_mode_merge_tip_desc\": \"將兩邊的清單合併到一起，相同的歌曲將被去掉（去掉的是被合併者的歌曲），不同的歌曲將被添加。\",\n  \"sync__list_mode_other_tip_desc\": \"「取消同步」將不使用清單同步功能。\",\n  \"sync__list_mode_overwrite_tip_desc\": \"被覆蓋者與覆蓋者清單 ID 相同的清單將被刪除後取代成覆蓋者的清單（清單 ID 不同的清單將被合併到一起）。若勾選「完全覆蓋」，則被覆蓋者的所有清單將被移除，然後取代成覆蓋者的清單。\",\n  \"sync__list_mode_title\": \"選取與「{name}」的清單同步方式\",\n  \"sync__mode_merge_btn_local_remote\": \"「本機清單」合併「遠端清單」\",\n  \"sync__mode_merge_btn_remote_local\": \"「遠端清單」合併「本機清單」\",\n  \"sync__mode_merge_tip\": \"合併：\",\n  \"sync__mode_other_label\": \"其他\",\n  \"sync__mode_other_tip\": \"其他：\",\n  \"sync__mode_overwrite\": \"完全覆蓋\",\n  \"sync__mode_overwrite_btn_cancel\": \"取消同步\",\n  \"sync__mode_overwrite_btn_local_remote\": \"「本機清單」覆蓋「遠端清單」\",\n  \"sync__mode_overwrite_btn_remote_local\": \"「遠端清單」覆蓋「本機清單」\",\n  \"sync__mode_overwrite_label\": \"覆蓋\",\n  \"sync__mode_overwrite_tip\": \"覆蓋：\",\n  \"sync_status_disabled\": \"未連線\",\n  \"theme_black\": \"黑燈瞎火\",\n  \"theme_blue\": \"藍田生玉\",\n  \"theme_blue2\": \"清熱版藍\",\n  \"theme_blue_plus\": \"蛋雅深藍\",\n  \"theme_brown\": \"泥牛入海\",\n  \"theme_china_ink\": \"近墨者黑\",\n  \"theme_green\": \"綠意盎然\",\n  \"theme_grey\": \"灰常美麗\",\n  \"theme_happy_new_year\": \"新年快樂\",\n  \"theme_mid_autumn\": \"月裡嫦娥\",\n  \"theme_ming\": \"青出於黑\",\n  \"theme_naruto\": \"木葉之村\",\n  \"theme_orange\": \"橙黃橘綠\",\n  \"theme_pink\": \"粉裝玉琢\",\n  \"theme_purple\": \"重斤球紫\",\n  \"theme_red\": \"熱情似火\",\n  \"timeout_exit_btn_cancel\": \"取消定時\",\n  \"timeout_exit_btn_update\": \"更新定時\",\n  \"timeout_exit_btn_wait_cancel\": \"取消退出\",\n  \"timeout_exit_btn_wait_tip\": \"計時已結束，正在等待退出...\",\n  \"timeout_exit_input_tip\": \"輸入倒數計時分鐘數\",\n  \"timeout_exit_label_isPlayed\": \"等待歌曲播放完畢再停止播放\",\n  \"timeout_exit_min\": \"分鐘\",\n  \"timeout_exit_tip_cancel\": \"已取消定時停止播放\",\n  \"timeout_exit_tip_max\": \"最多只能設定 {num} 分鐘哦\",\n  \"timeout_exit_tip_off\": \"設定定時停止播放\",\n  \"timeout_exit_tip_on\": \"{time} 後停止播放\",\n  \"toggle_source\": \"變更來源\",\n  \"toggle_source_failed\": \"變更來源失敗，請嘗試手動在搜尋頁指定其他音樂串流平台搜尋該歌曲播放\",\n  \"toggle_source_try\": \"嘗試變更到其他來源...\",\n  \"understand\": \"已了解 👌\",\n  \"user_api__init_failed_alert\": \"自訂來源 API「{name}」初始化失敗：\",\n  \"user_api_add_failed_tip\": \"無效的自訂來源 API 檔案\",\n  \"user_api_allow_show_update_alert\": \"允許顯示更新彈出視窗\",\n  \"user_api_btn_import\": \"匯入\",\n  \"user_api_btn_import_local\": \"從本機匯入\",\n  \"user_api_btn_import_online\": \"從線上匯入\",\n  \"user_api_btn_import_online_input_confirm\": \"匯入\",\n  \"user_api_btn_import_online_input_loading\": \"匯入中...\",\n  \"user_api_btn_import_online_input_tip\": \"請輸入 HTTP 連結\",\n  \"user_api_empty\": \"這裡竟然是空的 😲\",\n  \"user_api_import_desc\": \"選取自訂來源 API 檔案\",\n  \"user_api_import_failed_tip\": \"自訂來源 API 匯入失敗：\\n{message}\",\n  \"user_api_import_success_tip\": \"匯入成功 🎉\",\n  \"user_api_max_tip\": \"最多只能同時存在 20 個 API 哦🤪\\n想要繼續匯入的話，請先移除一些舊的 API 騰出位置吧\",\n  \"user_api_note\": \"提示：雖然我們已經盡可能地隔離了 API 腳本的執行環境，但匯入包含惡意行為的 API 腳本仍可能會影響你的系統，請謹慎匯入。\",\n  \"user_api_readme\": \"API 編寫說明：\",\n  \"user_api_remove_tip\": \"你真的要移除「{name}」嗎？\",\n  \"user_api_title\": \"自訂來源 API 管理（實驗性）\",\n  \"user_api_update_alert\": \"自訂來源 API「{name}」發現新版本\",\n  \"user_api_update_alert_open_url\": \"開啟更新位址\",\n  \"version_btn_close\": \"關閉\",\n  \"version_btn_downloading\": \"正在努力下載中... {current}/{total} ({progress}%)\",\n  \"version_btn_failed\": \"重試\",\n  \"version_btn_ignore\": \"忽略\",\n  \"version_btn_ignore_cancel\": \"取消忽略\",\n  \"version_btn_min\": \"後台下載\",\n  \"version_btn_new\": \"更新\",\n  \"version_btn_unknown\": \"項目首頁\",\n  \"version_btn_update\": \"安裝\",\n  \"version_label_change_log\": \"更新說明：\",\n  \"version_label_current_ver\": \"目前版本：\",\n  \"version_label_history\": \"歷史版本：\",\n  \"version_label_latest_ver\": \"最新版本：\",\n  \"version_tip_checking\": \"檢查更新中...⏳\",\n  \"version_tip_downloaded\": \"安裝包已經下載完畢。\",\n  \"version_tip_failed\": \"安裝包下載失敗，你可以重試或者去項目位址手動下載新版更新。\\n💡提示：一般多點幾次重試即可正常更新！\",\n  \"version_tip_latest\": \"軟體已是最新，盡情地體驗吧~🥂\",\n  \"version_tip_min\": \"已切換到後台下載，你可以去「設定 → 軟體更新」重新開啟本視窗哦\",\n  \"version_tip_unknown\": \"獲取最新版本資訊失敗，建議手動去項目位址檢查是否有新版本\",\n  \"version_title_checking\": \"⏳ 檢查更新中 ⏳\",\n  \"version_title_failed\": \"❌ 下載失敗 ❌\",\n  \"version_title_latest\": \"🎉 目前版本已是最新 🎊\",\n  \"version_title_new\": \"🌟 發現新版本 🌟\",\n  \"version_title_unknown\": \"❓ 獲取最新版本資訊失敗 ❓\",\n  \"version_title_update\": \"🚀 軟體更新 🚀\"\n}\n"
  },
  {
    "path": "src/navigation/components/ModalContent.tsx",
    "content": "import { View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\n// import { useWindowSize } from '@/utils/hooks'\nconst HEADER_HEIGHT = 20\n\ninterface Props {\n  children: React.ReactNode\n}\n\n\nexport default ({ children }: Props) => {\n  const theme = useTheme()\n\n  return (\n    <View style={{ ...styles.centeredView, backgroundColor: 'rgba(50,50,50,.3)' }}>\n      <View style={{ ...styles.modalView, backgroundColor: theme['c-content-background'] }}>\n        <View style={{ ...styles.header, backgroundColor: theme['c-primary-light-100-alpha-100'] }}></View>\n        {children}\n      </View>\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  centeredView: {\n    flex: 1,\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n  modalView: {\n    maxWidth: '90%',\n    minWidth: '60%',\n    maxHeight: '78%',\n    // backgroundColor: 'white',\n    borderRadius: 4,\n    // shadowColor: '#000',\n    // shadowOffset: {\n    //   width: 0,\n    //   height: 2,\n    // },\n    // shadowOpacity: 0.25,\n    // shadowRadius: 4,\n    elevation: 3,\n  },\n  header: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexDirection: 'row',\n    borderTopLeftRadius: 4,\n    borderTopRightRadius: 4,\n    height: HEADER_HEIGHT,\n  },\n})\n"
  },
  {
    "path": "src/navigation/components/PactModal.tsx",
    "content": "import { useMemo, useState, useEffect } from 'react'\nimport { View, ScrollView, Alert } from 'react-native'\nimport { Navigation } from 'react-native-navigation'\n\nimport Button from '@/components/common/Button'\nimport { createStyle, openUrl } from '@/utils/tools'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport ModalContent from './ModalContent'\nimport { exitApp } from '@/utils/nativeModules/utils'\nimport { updateSetting } from '@/core/common'\nimport { checkUpdate } from '@/core/version'\nimport { initDeeplink } from '@/core/init/deeplink'\nimport settingState from '@/store/setting/state'\n\nconst Content = () => {\n  const theme = useTheme()\n\n  const openHomePage = () => {\n    void openUrl('https://github.com/lyswhut/lx-music-mobile#readme')\n  }\n  const openLicensePage = () => {\n    void openUrl('http://www.apache.org/licenses/LICENSE-2.0')\n  }\n\n  const textLinkStyle = {\n    ...styles.text,\n    textDecorationLine: 'underline',\n    color: theme['c-primary-font'],\n    // fontSize: 15,\n  } as const\n\n  return (\n    <View style={styles.main}>\n      <Text style={styles.title} size={18} >许可协议</Text>\n      <ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>\n        {!settingState.setting['common.isAgreePact'] && <Text selectable style={styles.bold} >在使用本软件前，你（使用者）需签署本协议才可继续使用！{'\\n'}</Text>}\n        <Text selectable style={styles.text} >本项目基于 <Text onPress={openLicensePage} style={textLinkStyle}>Apache License 2.0</Text> 许可证发行，以下协议是对于 Apache License 2.0 的补充，如有冲突，以以下协议为准。{'\\n'}</Text>\n        <Text selectable style={styles.text} >词语约定：本协议中的“本项目”指 LX Music（洛雪音乐）移动版项目；“使用者”指签署本协议的使用者；“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称；“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >一、数据来源{'\\n'}</Text>\n        <Text selectable style={styles.text} >1.1 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据（与未登录状态在官方平台 APP 获取的数据相同），经过对数据简单地筛选与合并后进行展示，因此本项目不对数据的准确性负责。{'\\n'}</Text>\n        <Text selectable style={styles.text} >1.2 本项目本身没有获取某个音频数据的能力，本项目使用的在线音频数据来源来自软件设置内“自定义源”设置所选择的“源”返回的在线链接。例如播放某首歌，本项目所做的只是将希望播放的歌曲名、艺术家等信息传递给“源”，若“源”返回了一个链接，则本项目将认为这就是该歌曲的音频数据而进行使用，至于这是不是正确的音频数据本项目无法校验其准确性，所以使用本项目的过程中可能会出现希望播放的音频与实际播放的音频不对应或者无法播放的问题。{'\\n'}</Text>\n        <Text selectable style={styles.text} >1.3 本项目的非官方平台数据（例如“我的列表”内列表）来自使用者本地系统或者使用者连接的同步服务，本项目不对这些数据的合法性、准确性负责。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >二、版权数据{'\\n'}</Text>\n        <Text selectable style={styles.text} >2.1 使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在 <Text style={styles.bold}>24 小时内</Text> 清除使用本项目的过程中所产生的版权数据。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >三、音乐平台别名{'\\n'}</Text>\n        <Text selectable style={styles.text} >3.1 本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼，不包含恶意。如果官方音乐平台觉得不妥，可联系本项目更改或移除。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >四、资源使用{'\\n'}</Text>\n        <Text selectable style={styles.text} >4.1 本项目内使用的部分包括但不限于字体、图片等资源来源于互联网。如果出现侵权可联系本项目移除。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >五、免责声明{'\\n'}</Text>\n        <Text selectable style={styles.text} >5.1 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >六、使用限制{'\\n'}</Text>\n        <Text selectable style={styles.text} >6.1 本项目完全免费，且开源发布于 <Text onPress={openHomePage} style={textLinkStyle}>GitHub</Text> 面向全世界人用作对技术的学习交流，本项目不对项目内的技术可能存在违反当地法律法规的行为作保证。{'\\n'}</Text>\n        <Text selectable style={styles.text} >6.2 <Text style={styles.bold}>禁止在违反当地法律法规的情况下使用本项目</Text>，对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >七、版权保护{'\\n'}</Text>\n        <Text selectable style={styles.text} >7.1 音乐平台不易，请尊重版权，支持正版。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >八、非商业性质{'\\n'}</Text>\n        <Text selectable style={styles.text} >8.1 本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。{'\\n'}</Text>\n        <Text selectable style={styles.bold} >九、接受协议{'\\n'}</Text>\n        <Text selectable style={styles.text} >9.1 若你使用了本项目，将代表你接受本协议。{'\\n'}</Text>\n        <Text selectable style={styles.text} >* 若协议更新，恕不另行通知，可到开源地址查看。</Text>\n      </ScrollView>\n    </View>\n  )\n}\n\nconst Footer = ({ componentId }: { componentId: string }) => {\n  const theme = useTheme()\n  const isAgreePact = useSettingValue('common.isAgreePact')\n  // const checkUpdate = useDispatch('common', 'checkUpdate')\n  const [time, setTime] = useState(20)\n\n  const handleRejct = () => {\n    exitApp()\n    // Navigation.dismissOverlay(componentId)\n  }\n\n  const handleConfirm = () => {\n    let _isAgreePact = isAgreePact\n    if (!isAgreePact) updateSetting({ 'common.isAgreePact': true })\n    void Navigation.dismissOverlay(componentId)\n    if (!_isAgreePact) {\n      setTimeout(() => {\n        Alert.alert(\n          '',\n          Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),\n          [{\n            text: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),\n            onPress: () => {\n              void checkUpdate()\n              void initDeeplink()\n            },\n          }],\n        )\n      }, 2e3)\n    }\n  }\n\n\n  const confirmBtn = useMemo(() => {\n    if (isAgreePact) return { disabled: false, text: '关闭' }\n    return time ? { disabled: true, text: `接受（${time}）` } : { disabled: false, text: '接受' }\n  }, [isAgreePact, time])\n\n  useEffect(() => {\n    if (isAgreePact) return\n    const timeoutTools = {\n      timeout: null as NodeJS.Timeout | null,\n      start() {\n        this.timeout = setTimeout(() => {\n          setTime(time => {\n            time--\n            if (time > 0) this.start()\n            return time\n          })\n        }, 1000)\n      },\n      clear() {\n        if (!this.timeout) return\n        clearTimeout(this.timeout)\n      },\n    }\n    timeoutTools.start()\n    return () => {\n      timeoutTools.clear()\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <>\n      {\n        isAgreePact\n          ? null\n          : (\n              <Text selectable style={styles.tip} size={13}>若你（使用者）接受以上协议，请点击下面的“接受”按钮签署本协议；若不接受，请点击“不接受”后退出软件并清除本软件的所有数据。</Text>\n            )\n      }\n      <View style={styles.btns}>\n        {\n          isAgreePact\n            ? null\n            : (\n                <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleRejct}>\n                  <Text color={theme['c-button-font']}>不接受</Text>\n                </Button>\n              )\n        }\n        <Button disabled={confirmBtn.disabled} style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleConfirm}>\n          <Text color={theme['c-button-font']}>{confirmBtn.text}</Text>\n        </Button>\n      </View>\n    </>\n  )\n}\n\nconst PactModal = ({ componentId }: { componentId: string }) => {\n  return (\n    <ModalContent>\n      <Content />\n      <Footer componentId={componentId} />\n    </ModalContent>\n  )\n}\n\nconst styles = createStyle({\n  main: {\n    // flexGrow: 0,\n    flexShrink: 1,\n    marginTop: 15,\n    marginBottom: 10,\n  },\n  content: {\n    flexGrow: 0,\n    marginLeft: 5,\n    marginRight: 5,\n    paddingLeft: 10,\n    paddingRight: 10,\n  },\n  title: {\n    textAlign: 'center',\n    marginBottom: 15,\n  },\n  part: {\n    marginBottom: 10,\n  },\n  text: {\n    fontSize: 14,\n    textAlignVertical: 'bottom',\n    marginBottom: 5,\n  },\n  bold: {\n    fontSize: 14,\n    textAlignVertical: 'bottom',\n    fontWeight: 'bold',\n  },\n  tip: {\n    textAlignVertical: 'bottom',\n    fontWeight: 'bold',\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingBottom: 15,\n  },\n  btns: {\n    flexDirection: 'row',\n    justifyContent: 'center',\n    paddingBottom: 15,\n    paddingLeft: 15,\n    // paddingRight: 15,\n  },\n  btn: {\n    flex: 1,\n    paddingTop: 10,\n    paddingBottom: 10,\n    paddingLeft: 10,\n    paddingRight: 10,\n    alignItems: 'center',\n    borderRadius: 4,\n    marginRight: 15,\n  },\n})\n\nexport default PactModal\n\n"
  },
  {
    "path": "src/navigation/components/SyncModeModal.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { View, ScrollView } from 'react-native'\n\nimport Button from '@/components/common/Button'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport ModalContent from './ModalContent'\nimport syncState from '@/store/sync/state'\nimport CheckBox from '@/components/common/CheckBox'\nimport { setSyncModeComponentId } from '@/core/sync'\n\n\nconst styles = createStyle({\n  main: {\n    // flexGrow: 0,\n    flexShrink: 1,\n    marginTop: 15,\n    marginLeft: 15,\n    // marginRight: 15,\n    marginBottom: 15,\n  },\n  content: {\n    flexGrow: 0,\n  },\n  title: {\n    textAlign: 'center',\n    marginBottom: 10,\n    marginRight: 15,\n  },\n  btnGroup: {\n    marginTop: 10,\n  },\n  btns: {\n    flexDirection: 'row',\n    // justifyContent: 'center',\n    justifyContent: 'flex-start',\n    marginTop: 5,\n    marginBottom: 5,\n    // paddingBottom: 15,\n    // paddingLeft: 15,\n    // paddingRight: 15,\n  },\n  btn: {\n    // flex: 1,\n    paddingTop: 9,\n    paddingBottom: 9,\n    paddingLeft: 8,\n    paddingRight: 8,\n    alignItems: 'center',\n    borderRadius: 4,\n    marginRight: 15,\n    minWidth: 100,\n  },\n  tips: {\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingBottom: 10,\n  },\n  tipTitle: {\n    fontWeight: 'bold',\n  },\n  tip: {\n    // paddingLeft: 15,\n    // paddingRight: 15,\n    paddingBottom: 5,\n  },\n})\n\n\nconst ListModeModal = () => {\n  const theme = useTheme()\n  const t = useI18n()\n  const [isOverwrite, setOverwrite] = useState(false)\n\n  const handleSelectMode = (mode: LX.Sync.List.SyncMode) => {\n    if (mode.startsWith('overwrite') && isOverwrite) mode += '_full'\n    global.app_event.selectSyncMode({ type: 'list', mode })\n  }\n\n  return (\n    <>\n      <View style={styles.main}>\n        <Text style={styles.title} size={16}>{t('sync__list_mode_title', { name: syncState.serverName })}</Text>\n        <ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>\n          <View style={{ ...styles.btnGroup, marginTop: 0 }}>\n            <Text size={14}>{t('sync__mode_merge_tip')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('merge_local_remote') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_merge_btn_local_remote')}</Text>\n              </Button>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('merge_remote_local') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_merge_btn_remote_local')}</Text>\n              </Button>\n            </View>\n          </View>\n          <View style={styles.btnGroup}>\n            <Text size={14}>{t('sync__mode_overwrite_label')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('overwrite_local_remote') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_local_remote')}</Text>\n              </Button>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('overwrite_remote_local') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_remote_local')}</Text>\n              </Button>\n            </View>\n            <View>\n              <CheckBox check={isOverwrite} onChange={setOverwrite} label={t('sync__mode_overwrite')} />\n            </View>\n          </View>\n          <View style={styles.btnGroup}>\n            <Text size={14}>{t('sync__mode_other_label')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('cancel') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_cancel')}</Text>\n              </Button>\n            </View>\n          </View>\n        </ScrollView>\n      </View>\n      <View style={styles.tips}>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_merge_tip')}</Text>\n          {t('sync__list_mode_merge_tip_desc')}\n        </Text>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_overwrite_tip')}</Text>\n          {t('sync__list_mode_overwrite_tip_desc')}\n        </Text>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_other_tip')}</Text>\n          {t('sync__list_mode_other_tip_desc')}\n        </Text>\n      </View>\n    </>\n  )\n}\n\n\nconst DislikeModeModal = () => {\n  const theme = useTheme()\n  const t = useI18n()\n  const handleSelectMode = (mode: LX.Sync.Dislike.SyncMode) => {\n    global.app_event.selectSyncMode({ type: 'dislike', mode })\n  }\n\n  return (\n    <>\n      <View style={styles.main}>\n        <Text style={styles.title} size={16}>{t('sync__dislike_mode_title', { name: syncState.serverName })}</Text>\n        <ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>\n          <View style={{ ...styles.btnGroup, marginTop: 0 }}>\n            <Text size={14}>{t('sync__mode_merge_tip')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('merge_local_remote') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_merge_btn_local_remote')}</Text>\n              </Button>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('merge_remote_local') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_merge_btn_remote_local')}</Text>\n              </Button>\n            </View>\n          </View>\n          <View style={styles.btnGroup}>\n            <Text size={14}>{t('sync__mode_overwrite_label')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('overwrite_local_remote') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_local_remote')}</Text>\n              </Button>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('overwrite_remote_local') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_remote_local')}</Text>\n              </Button>\n            </View>\n          </View>\n          <View style={styles.btnGroup}>\n            <Text size={14}>{t('sync__mode_other_label')}</Text>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={() => { handleSelectMode('cancel') }}>\n                <Text size={13} color={theme['c-button-font']}>{t('sync__mode_overwrite_btn_cancel')}</Text>\n              </Button>\n            </View>\n          </View>\n        </ScrollView>\n      </View>\n      <View style={styles.tips}>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_merge_tip')}</Text>\n          {t('sync__dislike_mode_merge_tip_desc')}\n        </Text>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_overwrite_tip')}</Text>\n          {t('sync__dislike_mode_overwrite_tip_desc')}\n        </Text>\n        <Text style={styles.tip} size={12} color={theme['c-600']}>\n          <Text style={styles.tipTitle} size={12}>{t('sync__mode_other_tip')}</Text>\n          {t('sync__dislike_mode_other_tip_desc')}\n        </Text>\n      </View>\n    </>\n  )\n}\n\nexport default ({ componentId }: { componentId: string }) => {\n  useEffect(() => {\n    setSyncModeComponentId(componentId)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <ModalContent>\n      {\n        syncState.type == 'list' ? <ListModeModal />\n          : syncState.type == 'dislike' ? <DislikeModeModal />\n            : null\n      }\n    </ModalContent>\n  )\n}\n\n"
  },
  {
    "path": "src/navigation/components/Toast.js",
    "content": "import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'\nimport { useGetter } from '@/store'\nimport { Navigation } from 'react-native-navigation'\n\n\nconst Toast = ({ componentId }) => {\n  const theme = useGetter('common', 'theme')\n\n  return (\n    <View style={styles.root}>\n      <View style={{ ...styles.toast, backgroundColor: theme.secondary10 }}>\n        <Text style={styles.text}>This a very important message!</Text>\n        <TouchableOpacity\n          style={styles.button}\n          onPress={() => Navigation.dismissOverlay(componentId)}\n        >\n          <Text style={styles.buttonText}>OK</Text>\n        </TouchableOpacity>\n      </View>\n    </View>\n  )\n}\n\nconst styles = StyleSheet.create({\n  root: {\n    flex: 1,\n    flexDirection: 'column-reverse',\n  },\n  toast: {\n    elevation: 2,\n    flexDirection: 'row',\n    height: 40,\n    margin: 16,\n    borderRadius: 20,\n    alignItems: 'center',\n    justifyContent: 'space-between',\n  },\n  text: {\n    color: 'white',\n    fontSize: 16,\n    marginLeft: 16,\n  },\n  button: {\n    marginRight: 16,\n  },\n  buttonText: {\n    color: 'white',\n    fontSize: 16,\n    fontWeight: 'bold',\n  },\n})\n\nToast.options = {\n  layout: {\n    componentBackgroundColor: 'transparent',\n  },\n  overlay: {\n    interceptTouchOutside: false,\n  },\n}\n\nexport default Toast\n\n"
  },
  {
    "path": "src/navigation/components/VersionModal.tsx",
    "content": "import { useMemo, useState, useEffect, memo } from 'react'\nimport { View, ScrollView } from 'react-native'\n\nimport { compareVer, sizeFormate } from '@/utils'\n\nimport Button from '@/components/common/Button'\nimport { updateApp } from '@/utils/version'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { type VersionInfo } from '@/store/version/state'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { useVersionDownloadProgressUpdated, useVersionInfo, useVersionInfoIgnoreVersionUpdated } from '@/store/version/hook'\nimport ModalContent from './ModalContent'\nimport { checkUpdate, downloadUpdate, hideModal, setIgnoreVersion } from '@/core/version'\n\nconst VersionItem = ({ version, desc }: VersionInfo) => {\n  return (\n    <View style={styles.versionItem}>\n      <Text style={styles.label}>v{version}</Text>\n      <Text selectable style={styles.desc}>{desc}</Text>\n    </View>\n  )\n}\n\nconst Content = memo(({ title, newVersionInfo }: {\n  title: string\n  newVersionInfo: VersionInfo | null\n}) => {\n  const t = useI18n()\n\n  const history = useMemo(() => {\n    if (!newVersionInfo?.history) return []\n    let arr = []\n    for (const ver of newVersionInfo?.history) {\n      if (compareVer(currentVer, ver.version) < 0) arr.push(ver)\n    }\n    return arr\n  }, [newVersionInfo])\n\n  return (\n    <View style={styles.main}>\n      <Text style={styles.title}>{title}</Text>\n      <ScrollView style={styles.content} keyboardShouldPersistTaps={'always'}>\n        <Text style={styles.label}>{t('version_label_latest_ver')}{newVersionInfo?.version}</Text>\n        <Text style={styles.label}>{t('version_label_current_ver')}{currentVer}</Text>\n        {\n          newVersionInfo?.desc\n            ? (\n                <View>\n                  <Text style={styles.label}>{t('version_label_change_log')}</Text>\n                  <View style={{ paddingLeft: 10, marginTop: 5 }}>\n                    <Text selectable style={styles.desc}>{newVersionInfo.desc}</Text>\n                  </View>\n                </View>\n              )\n            : null\n        }\n        {\n          history.length\n            ? (\n                <View style={styles.history}>\n                  <Text style={styles.label}>{t('version_label_history')}</Text>\n                  <View style={{ paddingLeft: 10, marginTop: 5 }}>\n                    {history.map((item, index) => <VersionItem key={index} version={item.version} desc={item.desc} />)}\n                  </View>\n                </View>\n              )\n            : null\n        }\n      </ScrollView>\n    </View>\n  )\n})\n\nconst currentVer = process.versions.app\nconst VersionModal = ({ componentId }: { componentId: string }) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const versionInfo = useVersionInfo()\n  const progress = useVersionDownloadProgressUpdated()\n  const ignoreVersion = useVersionInfoIgnoreVersionUpdated()\n  const [ignoreBtn, setIgnoreBtn] = useState({ text: t('version_btn_ignore'), show: true, disabled: false })\n  const [closeBtnText, setCloseBtnText] = useState(t('version_btn_close'))\n  const [confirmBtn, setConfirmBtn] = useState({ text: '', show: true, disabled: false })\n  const [title, setTitle] = useState('')\n  const [tip, setTip] = useState('')\n\n\n  useEffect(() => {\n    let ignoreBtnConfig = { ...ignoreBtn }\n    if (versionInfo.isLatest) {\n      setTitle(t('version_title_latest'))\n      setTip('')\n      ignoreBtnConfig.show = false\n      setConfirmBtn({ text: t('version_btn_new'), show: false, disabled: true })\n      setCloseBtnText(t('version_btn_close'))\n    } else if (versionInfo.isUnknown) {\n      setTitle(t('version_title_unknown'))\n      setTip(t('version_tip_unknown'))\n      ignoreBtnConfig.show = false\n      setConfirmBtn({ text: t('version_btn_failed'), show: true, disabled: false })\n      setCloseBtnText(t('version_btn_close'))\n    } else {\n      switch (versionInfo.status) {\n        case 'downloading':\n          setTitle(t('version_title_new'))\n          setTip(t('version_btn_downloading', {\n            total: sizeFormate(progress.total),\n            current: sizeFormate(progress.current),\n            progress: progress.total ? (progress.current / progress.total * 100).toFixed(2) : '0',\n          }))\n          if (ignoreBtnConfig.show) ignoreBtnConfig.show = false\n          if (!confirmBtn.disabled) setConfirmBtn({ text: t('version_btn_update'), show: true, disabled: true })\n          setCloseBtnText(t('version_btn_min'))\n          break\n        case 'downloaded':\n          setTitle(t('version_title_update'))\n          setTip('')\n          if (ignoreBtnConfig.show) ignoreBtnConfig.show = false\n          setConfirmBtn({ text: t('version_btn_update'), show: true, disabled: false })\n          setCloseBtnText(t('version_btn_close'))\n          break\n        case 'checking':\n          setTitle(t('version_title_checking'))\n          setTip('')\n          ignoreBtnConfig.show = false\n          setConfirmBtn({ text: t('version_btn_new'), show: false, disabled: true })\n          setCloseBtnText(t('version_btn_close'))\n          break\n        case 'error':\n          setTitle(t('version_title_failed'))\n          setTip(t('version_tip_failed'))\n          ignoreBtnConfig.show = true\n          ignoreBtnConfig.disabled = false\n          setConfirmBtn({ text: t('version_btn_failed'), show: true, disabled: false })\n          setCloseBtnText(t('version_btn_close'))\n          break\n        // case 'idle':\n        //   break\n        default:\n          setTitle(t('version_title_new'))\n          setTip('')\n          ignoreBtnConfig.show = true\n          ignoreBtnConfig.disabled = false\n          setConfirmBtn({ text: t('version_btn_new'), show: true, disabled: false })\n          // setTip(t('version_btn_new'))\n          setCloseBtnText(t('version_btn_close'))\n          break\n      }\n    }\n    ignoreBtnConfig.text = t(ignoreVersion == versionInfo.newVersion?.version ? 'version_btn_ignore_cancel' : 'version_btn_ignore')\n    setIgnoreBtn(ignoreBtnConfig)\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [t, versionInfo, ignoreVersion, progress])\n\n  const handleCancel = () => {\n    hideModal(componentId)\n  }\n  const handleIgnore = () => {\n    setIgnoreVersion(ignoreVersion != versionInfo.newVersion!.version ? versionInfo.newVersion!.version : null)\n    // handleCancel()\n  }\n\n  const handleConfirm = () => {\n    if (versionInfo.isLatest || versionInfo.isUnknown) {\n      void checkUpdate()\n    } else if (versionInfo.status == 'downloaded') {\n      void updateApp()\n    } else if (versionInfo.status == 'idle' || versionInfo.status == 'error') {\n      downloadUpdate()\n    }\n  }\n\n  return (\n    <ModalContent>\n      <Content title={title} newVersionInfo={versionInfo.newVersion} />\n      { tip.length ? <Text style={styles.tip} color={theme['c-primary-font']}>{tip}</Text> : null }\n      <View style={styles.btns}>\n        {\n          ignoreBtn.show\n            ? (\n                <Button disabled={ignoreBtn.disabled} style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleIgnore}>\n                  <Text color={theme['c-button-font']}>{ignoreBtn.text}</Text>\n                </Button>\n              )\n            : null\n        }\n        <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>\n          <Text color={theme['c-button-font']}>{closeBtnText}</Text>\n        </Button>\n        {\n          confirmBtn.show\n            ? (\n                <Button disabled={confirmBtn.disabled} style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleConfirm}>\n                  <Text color={theme['c-button-font']}>{confirmBtn.text}</Text>\n                </Button>\n              )\n            : null\n        }\n      </View>\n    </ModalContent>\n  )\n}\n\nconst styles = createStyle({\n  main: {\n    // flexGrow: 0,\n    flexShrink: 1,\n    marginTop: 15,\n    marginLeft: 15,\n    marginRight: 15,\n    marginBottom: 20,\n  },\n  content: {\n    flexGrow: 0,\n  },\n  title: {\n    fontSize: 18,\n    textAlign: 'center',\n    marginBottom: 15,\n  },\n  history: {\n    marginTop: 15,\n  },\n  versionItem: {\n    marginBottom: 10,\n  },\n  label: {\n    fontSize: 14,\n    marginBottom: 2,\n  },\n  desc: {\n    fontSize: 13,\n    lineHeight: 18,\n  },\n  tip: {\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingBottom: 10,\n  },\n  btns: {\n    flexDirection: 'row',\n    justifyContent: 'center',\n    paddingBottom: 15,\n    paddingLeft: 15,\n    // paddingRight: 15,\n  },\n  btn: {\n    flex: 1,\n    paddingTop: 10,\n    paddingBottom: 10,\n    paddingLeft: 10,\n    paddingRight: 10,\n    alignItems: 'center',\n    borderRadius: 4,\n    marginRight: 15,\n  },\n})\n\nexport default VersionModal\n\n"
  },
  {
    "path": "src/navigation/event.ts",
    "content": "import { type EmitterSubscription } from 'react-native'\nimport { Navigation } from 'react-native-navigation'\n\n\nexport const onModalDismissed = (id: string, handler: () => void) => {\n  let modalDismissedListener: EmitterSubscription | null = Navigation.events().registerModalDismissedListener(({ componentId, modalsDismissed }) => {\n    if (componentId != id || !modalDismissedListener) return\n    handler()\n    modalDismissedListener.remove()\n    modalDismissedListener = null\n  })\n  return () => {\n    if (!modalDismissedListener) return\n    modalDismissedListener.remove()\n    modalDismissedListener = null\n  }\n}\n"
  },
  {
    "path": "src/navigation/hooks.ts",
    "content": "import { useEffect } from 'react'\nimport { type EmitterSubscription } from 'react-native'\nimport { Navigation } from 'react-native-navigation'\n\nexport const useNavigationCommandComplete = (callback = () => {}) => {\n  useEffect(() => {\n    // Register the listener to all events related to our component\n    let commandCompletedListener: EmitterSubscription | null = Navigation.events().registerCommandCompletedListener(({ commandId }) => {\n      callback()\n      if (!commandCompletedListener) return\n      commandCompletedListener.remove()\n      commandCompletedListener = null\n    })\n    return () => {\n      // Make sure to unregister the listener during cleanup\n      if (!commandCompletedListener) return\n      commandCompletedListener.remove()\n      commandCompletedListener = null\n    }\n  }, [callback])\n}\nexport const useNavigationComponentDidAppear = (componentId: string, callback = () => {}) => {\n  useEffect(() => {\n    const listener = {\n      componentDidAppear: () => {\n        callback()\n      },\n    }\n    // Register the listener to all events related to our component\n    const unsubscribe = Navigation.events().registerComponentListener(listener, componentId)\n    return () => {\n      // Make sure to unregister the listener during cleanup\n      unsubscribe.remove()\n    }\n  }, [callback, componentId])\n}\n\nexport const onNavigationComponentDidDisappearEvent = (componentId: string, callback = () => {}) => {\n  const listener = {\n    componentDidDisappear: () => {\n      callback()\n    },\n  }\n  const unsubscribe = Navigation.events().registerComponentListener(listener, componentId)\n  return unsubscribe\n}\n\nexport const useNavigationComponentDidDisappear = (componentId: string, callback = () => {}) => {\n  useEffect(() => {\n    const unsubscribe = onNavigationComponentDidDisappearEvent(componentId, callback)\n    return () => {\n      // Make sure to unregister the listener during cleanup\n      unsubscribe.remove()\n    }\n  }, [callback, componentId])\n}\n"
  },
  {
    "path": "src/navigation/index.ts",
    "content": "import { Navigation } from 'react-native-navigation'\nimport * as screenNames from './screenNames'\nimport * as navigations from './navigation'\n\nimport registerScreens from './registerScreens'\nimport { removeComponentId } from '@/core/common'\nimport { onAppLaunched } from './regLaunchedEvent'\n\nlet unRegisterEvent: ReturnType<ReturnType<typeof Navigation.events>['registerScreenPoppedListener']>\n\nconst init = (callback: () => void | Promise<void>) => {\n  // Register all screens on launch\n  registerScreens()\n\n  if (unRegisterEvent) unRegisterEvent.remove()\n\n  Navigation.setDefaultOptions({\n    // animations: {\n    //   setRoot: {\n    //     waitForRender: true,\n    //   },\n    // },\n  })\n  unRegisterEvent = Navigation.events().registerScreenPoppedListener(({ componentId }) => {\n    removeComponentId(componentId)\n  })\n  onAppLaunched(() => {\n    console.log('Register app launched listener')\n    void callback()\n  })\n}\n\nexport * from './utils'\nexport * from './event'\nexport * from './hooks'\n\nexport {\n  init,\n  screenNames,\n  navigations,\n}\n"
  },
  {
    "path": "src/navigation/navigation.ts",
    "content": "import { Navigation } from 'react-native-navigation'\n// import { InteractionManager } from 'react-native'\n\nimport {\n  HOME_SCREEN,\n  PLAY_DETAIL_SCREEN,\n  SONGLIST_DETAIL_SCREEN,\n  COMMENT_SCREEN,\n  // SETTING_SCREEN,\n} from './screenNames'\n\nimport themeState from '@/store/theme/state'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport { getStatusBarStyle } from './utils'\nimport { windowSizeTools } from '@/utils/windowSizeTools'\nimport { type ListInfoItem } from '@/store/songlist/state'\n\n// const store = getStore()\n// const getTheme = () => getter('common', 'theme')(store.getState())\n\nexport async function pushHomeScreen() {\n  /*\n    Navigation.setDefaultOptions({\n      topBar: {\n        background: {\n          color: '#039893',\n        },\n        title: {\n          color: 'white',\n        },\n        backButton: {\n          title: '', // Remove previous screen name from back button\n          color: 'white',\n        },\n        buttonColor: 'white',\n      },\n      statusBar: {\n        style: 'light',\n      },\n      layout: {\n        orientation: ['portrait'],\n      },\n      bottomTabs: {\n        titleDisplayMode: 'alwaysShow',\n      },\n      bottomTab: {\n        textColor: 'gray',\n        selectedTextColor: 'black',\n        iconColor: 'gray',\n        selectedIconColor: 'black',\n      },\n    })\n  */\n\n  const theme = themeState.theme\n  return Navigation.setRoot({\n    root: {\n      stack: {\n        children: [{\n          component: {\n            name: HOME_SCREEN,\n            options: {\n              topBar: {\n                visible: false,\n                height: 0,\n                drawBehind: false,\n              },\n              statusBar: {\n                drawBehind: true,\n                visible: true,\n                style: getStatusBarStyle(theme.isDark),\n                backgroundColor: 'transparent',\n              },\n              navigationBar: {\n                // visible: false,\n                backgroundColor: theme['c-content-background'],\n              },\n              layout: {\n                componentBackgroundColor: theme['c-content-background'],\n              },\n            },\n          },\n        }],\n      },\n    },\n  })\n}\nexport function pushPlayDetailScreen(componentId: string, skipAnimation = false) {\n  /*\n    Navigation.setDefaultOptions({\n      topBar: {\n        background: {\n          color: '#039893',\n        },\n        title: {\n          color: 'white',\n        },\n        backButton: {\n          title: '', // Remove previous screen name from back button\n          color: 'white',\n        },\n        buttonColor: 'white',\n      },\n      statusBar: {\n        style: 'light',\n      },\n      layout: {\n        orientation: ['portrait'],\n      },\n      bottomTabs: {\n        titleDisplayMode: 'alwaysShow',\n      },\n      bottomTab: {\n        textColor: 'gray',\n        selectedTextColor: 'black',\n        iconColor: 'gray',\n        selectedIconColor: 'black',\n      },\n    })\n  */\n  requestAnimationFrame(() => {\n    const theme = themeState.theme\n\n    void Navigation.push(componentId, {\n      component: {\n        name: PLAY_DETAIL_SCREEN,\n        options: {\n          topBar: {\n            visible: false,\n            height: 0,\n            drawBehind: false,\n          },\n          statusBar: {\n            drawBehind: true,\n            visible: true,\n            style: getStatusBarStyle(theme.isDark),\n            backgroundColor: 'transparent',\n          },\n          navigationBar: {\n            // visible: false,\n            backgroundColor: theme['c-content-background'],\n          },\n          layout: {\n            componentBackgroundColor: theme['c-content-background'],\n          },\n          animations: {\n            push: skipAnimation ? {} : {\n              sharedElementTransitions: [\n                {\n                  fromId: NAV_SHEAR_NATIVE_IDS.playDetail_pic,\n                  toId: NAV_SHEAR_NATIVE_IDS.playDetail_pic,\n                  interpolation: { type: 'spring' },\n                },\n              ],\n              elementTransitions: [\n                {\n                  id: NAV_SHEAR_NATIVE_IDS.playDetail_header,\n                  alpha: {\n                    from: 0, // We don't declare 'to' value as that is the element's current alpha value, here we're essentially animating from 0 to 1\n                    duration: 300,\n                  },\n                  translationY: {\n                    from: -32, // Animate translationY from 16dp to 0dp\n                    duration: 300,\n                  },\n                },\n                {\n                  id: NAV_SHEAR_NATIVE_IDS.playDetail_player,\n                  alpha: {\n                    from: 0, // We don't declare 'to' value as that is the element's current alpha value, here we're essentially animating from 0 to 1\n                    duration: 300,\n                  },\n                  translationY: {\n                    from: 32, // Animate translationY from 16dp to 0dp\n                    duration: 300,\n                  },\n                },\n              ],\n              // content: {\n              //   translationX: {\n              //     from: windowSizeTools.getSize().width,\n              //     to: 0,\n              //     duration: 300,\n              //   },\n              // },\n            },\n            pop: {\n              content: {\n                translationX: {\n                  from: 0,\n                  to: windowSizeTools.getSize().width,\n                  duration: 300,\n                },\n              },\n            },\n          },\n        },\n      },\n    })\n  })\n}\nexport function pushSonglistDetailScreen(componentId: string, info: ListInfoItem) {\n  const theme = themeState.theme\n\n  requestAnimationFrame(() => {\n    void Navigation.push(componentId, {\n      component: {\n        name: SONGLIST_DETAIL_SCREEN,\n        passProps: {\n          info,\n        },\n        options: {\n          topBar: {\n            visible: false,\n            height: 0,\n            drawBehind: false,\n          },\n          statusBar: {\n            drawBehind: true,\n            visible: true,\n            style: getStatusBarStyle(theme.isDark),\n            backgroundColor: 'transparent',\n          },\n          navigationBar: {\n            // visible: false,\n            backgroundColor: theme['c-content-background'],\n          },\n          layout: {\n            componentBackgroundColor: theme['c-content-background'],\n          },\n          animations: {\n            push: {\n              sharedElementTransitions: [\n                {\n                  fromId: `${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_from_${info.id}`,\n                  toId: `${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_to_${info.id}`,\n                  interpolation: { type: 'spring' },\n                },\n              ],\n              elementTransitions: [\n                {\n                  id: NAV_SHEAR_NATIVE_IDS.songlistDetail_title,\n                  alpha: {\n                    from: 0, // We don't declare 'to' value as that is the element's current alpha value, here we're essentially animating from 0 to 1\n                    duration: 300,\n                  },\n                  translationX: {\n                    from: 16, // Animate translationX from 16dp to 0dp\n                    duration: 300,\n                  },\n                },\n              ],\n              // content: {\n              //   scaleX: {\n              //     from: 1.2,\n              //     to: 1,\n              //     duration: 200,\n              //   },\n              //   scaleY: {\n              //     from: 1.2,\n              //     to: 1,\n              //     duration: 200,\n              //   },\n              //   alpha: {\n              //     from: 0,\n              //     to: 1,\n              //     duration: 200,\n              //   },\n              // },\n            },\n            pop: {\n              sharedElementTransitions: [\n                {\n                  fromId: `${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_to_${info.id}`,\n                  toId: `${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_from_${info.id}`,\n                  interpolation: { type: 'spring' },\n                },\n              ],\n              elementTransitions: [\n                {\n                  id: NAV_SHEAR_NATIVE_IDS.songlistDetail_title,\n                  alpha: {\n                    to: 0, // We don't declare 'to' value as that is the element's current alpha value, here we're essentially animating from 0 to 1\n                    duration: 300,\n                  },\n                  translationX: {\n                    to: 16, // Animate translationX from 16dp to 0dp\n                    duration: 300,\n                  },\n                },\n              ],\n              // content: {\n              //   alpha: {\n              //     from: 1,\n              //     to: 0,\n              //     duration: 200,\n              //   },\n              // },\n            },\n          },\n        },\n      },\n    })\n  })\n}\nexport function pushCommentScreen(componentId: string) {\n  /*\n    Navigation.setDefaultOptions({\n      topBar: {\n        background: {\n          color: '#039893',\n        },\n        title: {\n          color: 'white',\n        },\n        backButton: {\n          title: '', // Remove previous screen name from back button\n          color: 'white',\n        },\n        buttonColor: 'white',\n      },\n      statusBar: {\n        style: 'light',\n      },\n      layout: {\n        orientation: ['portrait'],\n      },\n      bottomTabs: {\n        titleDisplayMode: 'alwaysShow',\n      },\n      bottomTab: {\n        textColor: 'gray',\n        selectedTextColor: 'black',\n        iconColor: 'gray',\n        selectedIconColor: 'black',\n      },\n    })\n  */\n  requestAnimationFrame(() => {\n    const theme = themeState.theme\n\n    void Navigation.push(componentId, {\n      component: {\n        name: COMMENT_SCREEN,\n        options: {\n          topBar: {\n            visible: false,\n            height: 0,\n            drawBehind: false,\n          },\n          statusBar: {\n            drawBehind: true,\n            visible: true,\n            style: getStatusBarStyle(theme.isDark),\n            backgroundColor: 'transparent',\n          },\n          navigationBar: {\n            // visible: false,\n            backgroundColor: theme['c-content-background'],\n          },\n          layout: {\n            componentBackgroundColor: theme['c-content-background'],\n          },\n          animations: {\n            push: {\n              content: {\n                translationX: {\n                  from: windowSizeTools.getSize().width,\n                  to: 0,\n                  duration: 300,\n                },\n              },\n            },\n            pop: {\n              content: {\n                translationX: {\n                  from: 0,\n                  to: windowSizeTools.getSize().width,\n                  duration: 300,\n                },\n              },\n            },\n          },\n        },\n      },\n    })\n  })\n}\n\n// export function pushSettingScreen(componentId: string) {\n//   /*\n//     Navigation.setDefaultOptions({\n//       topBar: {\n//         background: {\n//           color: '#039893',\n//         },\n//         title: {\n//           color: 'white',\n//         },\n//         backButton: {\n//           title: '', // Remove previous screen name from back button\n//           color: 'white',\n//         },\n//         buttonColor: 'white',\n//       },\n//       statusBar: {\n//         style: 'light',\n//       },\n//       layout: {\n//         orientation: ['portrait'],\n//       },\n//       bottomTabs: {\n//         titleDisplayMode: 'alwaysShow',\n//       },\n//       bottomTab: {\n//         textColor: 'gray',\n//         selectedTextColor: 'black',\n//         iconColor: 'gray',\n//         selectedIconColor: 'black',\n//       },\n//     })\n//   */\n//     const theme = themeState.theme\n\n//     void Navigation.push(componentId, {\n//       component: {\n//         name: SETTING_SCREEN,\n//         options: {\n//           topBar: {\n//             visible: false,\n//             height: 0,\n//             drawBehind: false,\n//           },\n//           statusBar: {\n//             drawBehind: true,\n//             visible: true,\n//             style: getStatusBarStyle(theme.isDark),\n//             backgroundColor: 'transparent',\n//           },\n//           navigationBar: {\n//             // visible: false,\n//             backgroundColor: theme['c-content-background'],\n//           },\n//           layout: {\n//             componentBackgroundColor: theme['c-content-background'],\n//           },\n//           animations: {\n//             push: {\n//               content: {\n//                 translationX: {\n//                   from: windowSizeTools.getSize().width,\n//                   to: 0,\n//                   duration: 300,\n//                 },\n//               },\n//             },\n//             pop: {\n//               content: {\n//                 translationX: {\n//                   from: 0,\n//                   to: windowSizeTools.getSize().width,\n//                   duration: 300,\n//                 },\n//               },\n//             },\n//           },\n//         },\n//       },\n//   })\n// }\n\n/*\nexport function pushSingleScreenApp() {\n  Navigation.setRoot({\n    root: {\n      stack: {\n        children: [{\n          component: {\n            name: SINGLE_APP_SCREEN,\n            options: {\n              topBar: {\n                title: {\n                  text: 'SINGLE SCREEN APP',\n                },\n                leftButtons: [\n                  {\n                    id: 'nav_user_btn',\n                    icon: require('assets/icons/ic_nav_user.png'),\n                    color: 'white',\n                  },\n                ],\n                rightButtons: [\n                  {\n                    id: 'nav_logout_btn',\n                    icon: require('assets/icons/ic_nav_logout.png'),\n                    color: 'white',\n                  },\n                ],\n              },\n            },\n          },\n        }],\n      },\n    },\n  })\n}\n\nexport function pushTabBasedApp() {\n  Navigation.setRoot({\n    root: {\n      bottomTabs: {\n        children: [{\n          stack: {\n            children: [{\n              component: {\n                name: TAB1_SCREEN,\n                options: {\n                  topBar: {\n                    title: {\n                      text: 'TAB 1',\n                    },\n                    leftButtons: [\n                      {\n                        id: 'nav_user_btn',\n                        icon: require('assets/icons/ic_nav_user.png'),\n                        color: 'white',\n                      },\n                    ],\n                    rightButtons: [\n                      {\n                        id: 'nav_logout_btn',\n                        icon: require('assets/icons/ic_nav_logout.png'),\n                        color: 'white',\n                      },\n                    ],\n                  },\n                },\n              },\n            }],\n            options: {\n              bottomTab: {\n                icon: require('assets/icons/ic_tab_home.png'),\n                testID: 'FIRST_TAB_BAR_BUTTON',\n                text: 'Tab1',\n              },\n            },\n          },\n        },\n        {\n          stack: {\n            children: [{\n              component: {\n                name: TAB2_SCREEN,\n                options: {\n                  topBar: {\n                    title: {\n                      text: 'TAB 2',\n                    },\n                    leftButtons: [\n                      {\n                        id: 'nav_user_btn',\n                        icon: require('assets/icons/ic_nav_user.png'),\n                        color: 'white',\n                      },\n                    ],\n                    rightButtons: [\n                      {\n                        id: 'nav_logout_btn',\n                        icon: require('assets/icons/ic_nav_logout.png'),\n                        color: 'white',\n                      },\n                    ],\n                  },\n                },\n              },\n            }],\n            options: {\n              bottomTab: {\n                icon: require('assets/icons/ic_tab_menu.png'),\n                testID: 'SECOND_TAB_BAR_BUTTON',\n                text: 'Tab2',\n              },\n            },\n          },\n        }],\n      },\n    },\n  })\n}\n */\n"
  },
  {
    "path": "src/navigation/regLaunchedEvent.ts",
    "content": "import { Navigation } from 'react-native-navigation'\n\nlet launched = false\nconst handlers: Array<() => void> = []\n\n\nexport const listenLaunchEvent = () => {\n  Navigation.events().registerAppLaunchedListener(() => {\n    // console.log('Register app launched listener', launched)\n    launched = true\n    setImmediate(() => {\n      for (const handler of handlers) handler()\n    })\n  })\n}\n\nexport const onAppLaunched = (handler: () => void) => {\n  handlers.push(handler)\n  if (launched) {\n    setImmediate(() => {\n      handler()\n    })\n  }\n}\n"
  },
  {
    "path": "src/navigation/registerScreens.tsx",
    "content": "// @flow\n\nimport { Navigation } from 'react-native-navigation'\n\nimport {\n  Home,\n  PlayDetail,\n  SonglistDetail,\n  Comment,\n  // Setting,\n} from '@/screens'\nimport { Provider } from '@/store/Provider'\n\nimport {\n  HOME_SCREEN,\n  PLAY_DETAIL_SCREEN,\n  SONGLIST_DETAIL_SCREEN,\n  COMMENT_SCREEN,\n  VERSION_MODAL,\n  PACT_MODAL,\n  SYNC_MODE_MODAL,\n  // SETTING_SCREEN,\n} from './screenNames'\nimport VersionModal from './components/VersionModal'\nimport PactModal from './components/PactModal'\nimport SyncModeModal from './components/SyncModeModal'\n\nfunction WrappedComponent(Component: any) {\n  return function inject(props: Record<string, any>) {\n    const EnhancedComponent = () => (\n      <Provider>\n        <Component\n          {...props}\n        />\n      </Provider>\n    )\n\n    return <EnhancedComponent />\n  }\n}\n\nexport default () => {\n  Navigation.registerComponent(HOME_SCREEN, () => WrappedComponent(Home))\n  Navigation.registerComponent(PLAY_DETAIL_SCREEN, () => WrappedComponent(PlayDetail))\n  Navigation.registerComponent(SONGLIST_DETAIL_SCREEN, () => WrappedComponent(SonglistDetail))\n  Navigation.registerComponent(COMMENT_SCREEN, () => WrappedComponent(Comment))\n  Navigation.registerComponent(VERSION_MODAL, () => WrappedComponent(VersionModal))\n  Navigation.registerComponent(PACT_MODAL, () => WrappedComponent(PactModal))\n  Navigation.registerComponent(SYNC_MODE_MODAL, () => WrappedComponent(SyncModeModal))\n  // Navigation.registerComponent(SETTING_SCREEN, () => WrappedComponent(Setting))\n\n  console.info('All screens have been registered...')\n}\n"
  },
  {
    "path": "src/navigation/screenNames.ts",
    "content": "export const HOME_SCREEN = 'lxm.HomeScreen'\nexport const PLAY_DETAIL_SCREEN = 'lxm.PlayDetailScreen'\nexport const SONGLIST_DETAIL_SCREEN = 'lxm.SonglistDetailScreen'\nexport const COMMENT_SCREEN = 'lxm.CommentScreen'\nexport const VERSION_MODAL = 'lxm.VersionModal'\nexport const PACT_MODAL = 'lxm.PactModal'\nexport const SYNC_MODE_MODAL = 'lxm.SyncModeModal'\n// export const SETTING_SCREEN = 'lxm.SettingScreen'\n// export const TOAST_SCREEN = 'lxm.ToastScreen'\n\n"
  },
  {
    "path": "src/navigation/utils.ts",
    "content": "import { Navigation } from 'react-native-navigation'\nimport {\n  VERSION_MODAL,\n  PACT_MODAL,\n  SYNC_MODE_MODAL,\n} from './screenNames'\nimport themeState from '@/store/theme/state'\n\n\nexport const getStatusBarStyle = (isDark: boolean) => isDark ? 'light' : 'dark'\n\nexport const dismissOverlay = async(compId: string) => Navigation.dismissOverlay(compId)\n\nexport const pop = async(compId: string) => Navigation.pop(compId)\nexport const popToRoot = async(compId: string) => Navigation.popToRoot(compId)\nexport const popTo = async(compId: string) => Navigation.popTo(compId)\n\nexport const showPactModal = () => {\n  const theme = themeState.theme\n\n  void Navigation.showOverlay({\n    component: {\n      name: PACT_MODAL,\n      options: {\n        layout: {\n          componentBackgroundColor: 'transparent',\n        },\n        overlay: {\n          interceptTouchOutside: true,\n        },\n        statusBar: {\n          drawBehind: true,\n          visible: true,\n          style: getStatusBarStyle(theme.isDark),\n          backgroundColor: 'transparent',\n        },\n        navigationBar: {\n          // visible: false,\n          backgroundColor: theme['c-content-background'],\n        },\n        // animations: {\n\n        //   showModal: {\n        //     enter: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 0,\n        //         to: 1,\n        //         duration: 300,\n        //       },\n        //     },\n        //     exit: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 1,\n        //         to: 0,\n        //         duration: 300,\n        //       },\n        //     },\n        //   },\n        // },\n      },\n    },\n  })\n}\n\nexport const showVersionModal = () => {\n  const theme = themeState.theme\n\n  void Navigation.showOverlay({\n    component: {\n      name: VERSION_MODAL,\n      options: {\n        layout: {\n          componentBackgroundColor: 'transparent',\n        },\n        overlay: {\n          interceptTouchOutside: true,\n        },\n        statusBar: {\n          drawBehind: true,\n          visible: true,\n          style: getStatusBarStyle(theme.isDark),\n          backgroundColor: 'transparent',\n        },\n        navigationBar: {\n          // visible: false,\n          backgroundColor: theme['c-content-background'],\n        },\n        // animations: {\n\n        //   showModal: {\n        //     enter: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 0,\n        //         to: 1,\n        //         duration: 300,\n        //       },\n        //     },\n        //     exit: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 1,\n        //         to: 0,\n        //         duration: 300,\n        //       },\n        //     },\n        //   },\n        // },\n      },\n    },\n  })\n}\n\nexport const showSyncModeModal = () => {\n  const theme = themeState.theme\n\n  void Navigation.showOverlay({\n    component: {\n      name: SYNC_MODE_MODAL,\n      options: {\n        layout: {\n          componentBackgroundColor: 'transparent',\n        },\n        overlay: {\n          interceptTouchOutside: true,\n        },\n        statusBar: {\n          drawBehind: true,\n          visible: true,\n          style: getStatusBarStyle(theme.isDark),\n          backgroundColor: 'transparent',\n        },\n        navigationBar: {\n          // visible: false,\n          backgroundColor: theme['c-content-background'],\n        },\n        // animations: {\n\n        //   showModal: {\n        //     enter: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 0,\n        //         to: 1,\n        //         duration: 300,\n        //       },\n        //     },\n        //     exit: {\n        //       enabled: true,\n        //       alpha: {\n        //         from: 1,\n        //         to: 0,\n        //         duration: 300,\n        //       },\n        //     },\n        //   },\n        // },\n      },\n    },\n  })\n}\n\n// export const showToast = (text) => {\n//   Navigation.showOverlay({\n//     component: {\n//       name: TOAST_SCREEN,\n//     },\n//   })\n// }\n"
  },
  {
    "path": "src/plugins/lyric.ts",
    "content": "import { useEffect, useState } from 'react'\nimport Lyric, { type Lines } from 'lrc-file-parser'\n// import { getStore, subscribe } from '@/store'\nexport type Line = Lines[number]\ntype PlayHook = (line: number, text: string) => void\ntype SetLyricHook = (lines: Lines) => void\n\nconst lrcTools = {\n  isInited: false,\n  lrc: null as Lyric | null,\n  currentLineData: { line: 0, text: '' },\n  currentLines: [] as Lines,\n  playHooks: [] as PlayHook[],\n  setLyricHooks: [] as SetLyricHook[],\n  isPlay: false,\n  isShowTranslation: false,\n  isShowRoma: false,\n  lyricText: '',\n  translationText: '' as string | null | undefined,\n  romaText: '' as string | null | undefined,\n  init() {\n    if (this.isInited) return\n    this.isInited = true\n    this.lrc = new Lyric({\n      onPlay: this.onPlay.bind(this),\n      onSetLyric: this.onSetLyric.bind(this),\n      offset: 100, // offset time(ms), default is 150 ms\n    })\n  },\n  onPlay(line: number, text: string) {\n    this.currentLineData.line = line\n    // console.log(line)\n    this.currentLineData.text = text\n    for (const hook of this.playHooks) hook(line, text)\n  },\n  onSetLyric(lines: Lines) {\n    this.currentLines = lines\n    this.currentLineData.line = 0\n    this.currentLineData.text = ''\n    for (const hook of this.playHooks) hook(-1, '')\n    for (const hook of this.setLyricHooks) hook(lines)\n  },\n  addPlayHook(hook: PlayHook) {\n    this.playHooks.push(hook)\n    hook(this.currentLineData.line, this.currentLineData.text)\n  },\n  removePlayHook(hook: PlayHook) {\n    this.playHooks.splice(this.playHooks.indexOf(hook), 1)\n  },\n  addSetLyricHook(hook: SetLyricHook) {\n    this.setLyricHooks.push(hook)\n    hook(this.currentLines)\n  },\n  removeSetLyricHook(hook: SetLyricHook) {\n    this.setLyricHooks.splice(this.setLyricHooks.indexOf(hook), 1)\n  },\n  setLyric() {\n    const extendedLyrics = [] as string[]\n    if (this.isShowTranslation && this.translationText) extendedLyrics.push(this.translationText)\n    if (this.isShowRoma && this.romaText) extendedLyrics.push(this.romaText)\n    this.lrc!.setLyric(this.lyricText, extendedLyrics)\n  },\n}\n\n\nexport const init = async() => {\n  lrcTools.init()\n}\n\nexport const setLyric = (lyric: string, translation?: string, romalrc?: string) => {\n  lrcTools.isPlay = false\n  lrcTools.lyricText = lyric\n  lrcTools.translationText = translation\n  lrcTools.romaText = romalrc\n  lrcTools.setLyric()\n}\nexport const setPlaybackRate = (playbackRate: number) => {\n  lrcTools.lrc!.setPlaybackRate(playbackRate)\n}\nexport const toggleTranslation = (isShow: boolean) => {\n  lrcTools.isShowTranslation = isShow\n  if (!lrcTools.lyricText) return\n  lrcTools.setLyric()\n}\nexport const toggleRoma = (isShow: boolean) => {\n  lrcTools.isShowRoma = isShow\n  if (!lrcTools.lyricText) return\n  lrcTools.setLyric()\n}\nexport const play = (time: number) => {\n  // console.log(time)\n  lrcTools.isPlay = true\n  lrcTools.lrc!.play(time)\n}\nexport const pause = () => {\n  // console.log('pause')\n  lrcTools.isPlay = false\n  lrcTools.lrc!.pause()\n}\n\n// on lyric play hook\nexport const useLrcPlay = (autoUpdate = true) => {\n  const [lrcInfo, setLrcInfo] = useState(lrcTools.currentLineData)\n  useEffect(() => {\n    if (!autoUpdate) return\n    const setLrcCallback: SetLyricHook = () => {\n      setLrcInfo({ line: 0, text: '' })\n    }\n    const playCallback: PlayHook = (line, text) => {\n      setLrcInfo({ line, text })\n    }\n    lrcTools.addSetLyricHook(setLrcCallback)\n    lrcTools.addPlayHook(playCallback)\n    setLrcInfo(lrcTools.currentLineData)\n    return () => {\n      lrcTools.removeSetLyricHook(setLrcCallback)\n      lrcTools.removePlayHook(playCallback)\n    }\n  }, [autoUpdate])\n\n  return lrcInfo\n}\n\n// on lyric set hook\nexport const useLrcSet = () => {\n  const [lines, setLines] = useState<Lines>(lrcTools.currentLines)\n  useEffect(() => {\n    const callback = (lines: Lines) => {\n      setLines(lines)\n    }\n    lrcTools.addSetLyricHook(callback)\n    return () => { lrcTools.removeSetLyricHook(callback) }\n  }, [])\n\n  return lines\n}\n\n"
  },
  {
    "path": "src/plugins/player/hook.ts",
    "content": "import { useEffect, useState, useRef } from 'react'\nimport TrackPlayer, { State, Event } from 'react-native-track-player'\n\n/** Get current playback state and subsequent updatates  */\nexport const usePlaybackState = () => {\n  const [state, setState] = useState(State.None)\n\n  useEffect(() => {\n    async function setPlayerState() {\n      const playerState = await TrackPlayer.getState()\n      setState(playerState)\n    }\n\n    void setPlayerState()\n\n    const sub = TrackPlayer.addEventListener(Event.PlaybackState, data => {\n      setState(data.state as State)\n    })\n\n    return () => { sub.remove() }\n  }, [])\n\n  return state\n}\n\n/**\n * Attaches a handler to the given TrackPlayer events and performs cleanup on unmount\n * @param events - TrackPlayer events to subscribe to\n * @param handler - callback invoked when the event fires\n */\n// export const useTrackPlayerEvents = (events, handler) => {\n//   const savedHandler = useRef()\n\n//   useEffect(() => {\n//     savedHandler.current = handler\n//   }, [handler])\n\n//   useEffect(() => {\n//     // eslint-disable-next-line no-undef\n//     if (__DEV__) {\n//       const allowedTypes = Object.values(Event)\n//       const invalidTypes = events.filter(type => !allowedTypes.includes(type))\n//       if (invalidTypes.length) {\n//         console.warn(\n//           'One or more of the events provided to useTrackPlayerEvents is ' +\n//             `not a valid TrackPlayer event: ${invalidTypes.join(\"', '\")}. ` +\n//             'A list of available events can be found at ' +\n//             'https://react-native-kit.github.io/react-native-track-player/documentation/#events',\n//         )\n//       }\n//     }\n\n//     const subs = events.map(event =>\n//       TrackPlayer.addEventListener(event, payload => savedHandler.current({ ...payload, type: event })),\n//     )\n\n//     return () => subs.forEach(sub => sub.remove())\n//   }, [events])\n// }\n\nconst pollTrackPlayerStates = [\n  State.Playing,\n  State.Buffering,\n] as const\n/**\n * Poll for track progress for the given interval (in miliseconds)\n * @param updateInterval - ms interval\n */\nexport function useProgress(updateInterval: number) {\n  const [state, setState] = useState({ position: 0, duration: 0, buffered: 0 })\n  const playerState = usePlaybackState()\n  const stateRef = useRef(state)\n  const isUnmountedRef = useRef(true)\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  const getProgress = async() => {\n    const [position, duration, buffered] = await Promise.all([\n      TrackPlayer.getPosition(),\n      TrackPlayer.getDuration(),\n      TrackPlayer.getBufferedPosition(),\n    ])\n    // After the asynchronous code is executed, if the component has been uninstalled, do not update the status\n    if (isUnmountedRef.current) return\n\n    if (\n      position === stateRef.current.position &&\n      duration === stateRef.current.duration &&\n      buffered === stateRef.current.buffered\n    ) return\n\n    const state = { position, duration, buffered }\n    stateRef.current = state\n    setState(state)\n  }\n\n  useEffect(() => {\n    // @ts-expect-error\n    if (!pollTrackPlayerStates.includes(playerState)) return\n\n    void getProgress()\n\n    // eslint-disable-next-line @typescript-eslint/no-misused-promises\n    const poll = setInterval(getProgress, updateInterval || 1000)\n    return () => { clearInterval(poll) }\n  }, [playerState, updateInterval])\n\n  return state\n}\n\nexport function useBufferProgress() {\n  const [progress, setProgress] = useState(0)\n\n  useEffect(() => {\n    let isUnmounted = false\n    let preBuffered = 0\n    let duration = 0\n    let interval: NodeJS.Timer | null = null\n\n    const clearItv = () => {\n      if (!interval) return\n      clearInterval(interval)\n      interval = null\n    }\n    const updateBuffer = async() => {\n      const buffered = await (duration ? TrackPlayer.getBufferedPosition() : Promise.all([TrackPlayer.getBufferedPosition(), TrackPlayer.getDuration()]).then(([buffered, _duration]) => {\n        duration = _duration\n        return buffered\n      }))\n      // console.log('updateBuffer', buffered, duration, buffered > 0, buffered == duration)\n      // After the asynchronous code is executed, if the component has been uninstalled, do not update the status\n      if (buffered > 0 && buffered == duration) clearItv()\n      if (buffered == preBuffered || isUnmounted) return\n      preBuffered = buffered\n      setProgress(duration ? (buffered / duration) : 0)\n    }\n\n    const sub = TrackPlayer.addEventListener(Event.PlaybackState, data => {\n      switch (data.state) {\n        case State.None:\n          // console.log('state', 'None')\n          setProgress(0)\n          break\n        // case State.Ready:\n        //   console.log('state', 'Ready')\n        //   break\n        // case State.Stopped:\n        //   console.log('state', 'Stopped')\n        //   break\n        // case State.Paused:\n        //   console.log('state', 'Paused')\n        //   break\n        // case State.Playing:\n        //   console.log('state', 'Playing')\n        //   break\n        case State.Buffering:\n          // console.log('state', 'Buffering')\n          clearItv()\n          duration = 0\n          interval = setInterval(updateBuffer, 1000)\n          void updateBuffer()\n          break\n        // case State.Connecting:\n        //   console.log('state', 'Connecting')\n        //   break\n        // default:\n        //   console.log('playback-state', data)\n        //   break\n      }\n    })\n\n    void updateBuffer()\n    void TrackPlayer.getState().then((state) => {\n      if (state == State.Buffering) interval = setInterval(updateBuffer, 1000)\n    })\n    return () => {\n      isUnmounted = true\n      sub.remove()\n      clearItv()\n    }\n  }, [])\n\n  return progress\n}\n"
  },
  {
    "path": "src/plugins/player/index.ts",
    "content": "import TrackPlayer from 'react-native-track-player'\nimport { updateOptions, setVolume, setPlaybackRate, migratePlayerCache } from './utils'\n\n// const listenEvent = () => {\n//   TrackPlayer.addEventListener('playback-error', err => {\n//     console.log('playback-error', err)\n//   })\n//   TrackPlayer.addEventListener('playback-state', info => {\n//     console.log('playback-state', info)\n//   })\n//   TrackPlayer.addEventListener('playback-track-changed', info => {\n//     console.log('playback-track-changed', info)\n//   })\n//   TrackPlayer.addEventListener('playback-queue-ended', info => {\n//     console.log('playback-queue-ended', info)\n//   })\n// }\n\nconst initial = async({ volume, playRate, cacheSize, isHandleAudioFocus, isEnableAudioOffload }: {\n  volume: number\n  playRate: number\n  cacheSize: number\n  isHandleAudioFocus: boolean\n  isEnableAudioOffload: boolean\n}) => {\n  if (global.lx.playerStatus.isIniting || global.lx.playerStatus.isInitialized) return\n  global.lx.playerStatus.isIniting = true\n  console.log('Cache Size', cacheSize * 1024)\n  await migratePlayerCache()\n  await TrackPlayer.setupPlayer({\n    maxCacheSize: cacheSize * 1024,\n    maxBuffer: 1000,\n    waitForBuffer: true,\n    handleAudioFocus: isHandleAudioFocus,\n    audioOffload: isEnableAudioOffload,\n    autoUpdateMetadata: false,\n  })\n  global.lx.playerStatus.isInitialized = true\n  global.lx.playerStatus.isIniting = false\n  await updateOptions()\n  await setVolume(volume)\n  await setPlaybackRate(playRate)\n  // listenEvent()\n}\n\n\nconst isInitialized = () => global.lx.playerStatus.isInitialized\n\n\nexport {\n  initial,\n  isInitialized,\n  setVolume,\n  setPlaybackRate,\n}\n\nexport {\n  setResource,\n  setPause,\n  setPlay,\n  setCurrentTime,\n  getDuration,\n  setStop,\n  resetPlay,\n  getPosition,\n  updateMetaData,\n  onStateChange,\n  isEmpty,\n  useBufferProgress,\n  initTrackInfo,\n} from './utils'\n"
  },
  {
    "path": "src/plugins/player/playList.ts",
    "content": "import TrackPlayer, { State } from 'react-native-track-player'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { defaultUrl } from '@/config'\n// import { action as playerAction } from '@/store/modules/player'\nimport settingState from '@/store/setting/state'\n\n\nconst list: LX.Player.Track[] = []\n\nconst defaultUserAgent = 'Mozilla/5.0 (Linux; Android 10; Pixel 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Mobile Safari/537.36'\nconst httpRxp = /^(https?:\\/\\/.+|\\/.+)/\n\nexport const state = {\n  isPlaying: false,\n  prevDuration: -1,\n}\n\nconst formatMusicInfo = (musicInfo: LX.Player.PlayMusic) => {\n  return '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  } : {\n    id: musicInfo.id,\n    pic: musicInfo.meta.picUrl,\n    name: musicInfo.name,\n    singer: musicInfo.singer,\n    album: musicInfo.meta.albumName,\n  }\n}\n\nconst buildTracks = (musicInfo: LX.Player.PlayMusic, url?: LX.Player.Track['url'], duration?: LX.Player.Track['duration']): LX.Player.Track[] => {\n  const mInfo = formatMusicInfo(musicInfo)\n  const track = [] as LX.Player.Track[]\n  const isShowNotificationImage = settingState.setting['player.isShowNotificationImage']\n  const album = mInfo.album || undefined\n  const artwork = isShowNotificationImage && mInfo.pic && httpRxp.test(mInfo.pic) ? mInfo.pic : undefined\n  if (url) {\n    track.push({\n      id: `${mInfo.id}__//${Math.random()}__//${url}`,\n      url,\n      title: mInfo.name || 'Unknow',\n      artist: mInfo.singer || 'Unknow',\n      album,\n      artwork,\n      userAgent: defaultUserAgent,\n      musicId: mInfo.id,\n      // original: { ...musicInfo },\n      duration,\n    })\n  }\n  track.push({\n    id: `${mInfo.id}__//${Math.random()}__//default`,\n    url: defaultUrl,\n    title: mInfo.name || 'Unknow',\n    artist: mInfo.singer || 'Unknow',\n    album,\n    artwork,\n    musicId: mInfo.id,\n    // original: { ...musicInfo },\n    duration: 0,\n  })\n  return track\n  // console.log('buildTrack', musicInfo.name, url)\n}\n// const buildTrack = (musicInfo: LX.Player.PlayMusic, url: LX.Player.Track['url'], duration?: LX.Player.Track['duration']): LX.Player.Track => {\n//   const mInfo = formatMusicInfo(musicInfo)\n//   const isShowNotificationImage = settingState.setting['player.isShowNotificationImage']\n//   const album = mInfo.album || undefined\n//   const artwork = isShowNotificationImage && mInfo.pic && httpRxp.test(mInfo.pic) ? mInfo.pic : undefined\n//   return url\n//     ? {\n//         id: `${mInfo.id}__//${Math.random()}__//${url}`,\n//         url,\n//         title: mInfo.name || 'Unknow',\n//         artist: mInfo.singer || 'Unknow',\n//         album,\n//         artwork,\n//         userAgent: defaultUserAgent,\n//         musicId: `${mInfo.id}`,\n//         original: { ...musicInfo },\n//         duration,\n//       }\n//     : {\n//         id: `${mInfo.id}__//${Math.random()}__//default`,\n//         url: defaultUrl,\n//         title: mInfo.name || 'Unknow',\n//         artist: mInfo.singer || 'Unknow',\n//         album,\n//         artwork,\n//         musicId: `${mInfo.id}`,\n//         original: { ...musicInfo },\n//         duration: 0,\n//       }\n// }\n\nexport const isTempTrack = (trackId: string) => /\\/\\/default$/.test(trackId)\n\n\nexport const getCurrentTrackId = async() => {\n  const currentTrackIndex = await TrackPlayer.getCurrentTrack()\n  return list[currentTrackIndex]?.id\n}\nexport const getCurrentTrack = async() => {\n  const currentTrackIndex = await TrackPlayer.getCurrentTrack()\n  return list[currentTrackIndex]\n}\n\nexport const updateMetaData = async(musicInfo: LX.Player.MusicInfo, isPlay: boolean, lyric?: string, force = false) => {\n  if (!force && isPlay == state.isPlaying) {\n    const duration = await TrackPlayer.getDuration()\n    if (state.prevDuration != duration) {\n      state.prevDuration = duration\n      const trackInfo = await getCurrentTrack()\n      if (trackInfo && musicInfo) {\n        delayUpdateMusicInfo(musicInfo, lyric)\n      }\n    }\n  } else {\n    const [duration, trackInfo] = await Promise.all([TrackPlayer.getDuration(), getCurrentTrack()])\n    state.prevDuration = duration\n    if (trackInfo && musicInfo) {\n      delayUpdateMusicInfo(musicInfo, lyric)\n    }\n  }\n}\n\nexport const initTrackInfo = async(musicInfo: LX.Player.PlayMusic, mInfo: LX.Player.MusicInfo) => {\n  const tracks = buildTracks(musicInfo)\n  await TrackPlayer.add(tracks).then(() => list.push(...tracks))\n  const queue = await TrackPlayer.getQueue() as LX.Player.Track[]\n  await TrackPlayer.skip(queue.findIndex(t => t.id == tracks[0].id))\n  delayUpdateMusicInfo(mInfo)\n}\n\n\nconst handlePlayMusic = async(musicInfo: LX.Player.PlayMusic, url: string, time: number) => {\n// console.log(tracks, time)\n  const tracks = buildTracks(musicInfo, url)\n  const track = tracks[0]\n  // await updateMusicInfo(track)\n  const currentTrackIndex = await TrackPlayer.getCurrentTrack()\n  await TrackPlayer.add(tracks).then(() => list.push(...tracks))\n  const queue = await TrackPlayer.getQueue() as LX.Player.Track[]\n  await TrackPlayer.skip(queue.findIndex(t => t.id == track.id))\n\n  if (currentTrackIndex == null) {\n    if (!isTempTrack(track.id as string)) {\n      if (time) await TrackPlayer.seekTo(time)\n      if (global.lx.restorePlayInfo) {\n        await TrackPlayer.pause()\n        // let startupAutoPlay = settingState.setting['player.startupAutoPlay']\n        global.lx.restorePlayInfo = null\n\n      // TODO startupAutoPlay\n      // if (startupAutoPlay) store.dispatch(playerAction.playMusic())\n      } else {\n        await TrackPlayer.play()\n      }\n    }\n  } else {\n    await TrackPlayer.pause()\n    if (!isTempTrack(track.id as string)) {\n      await TrackPlayer.seekTo(time)\n      await TrackPlayer.play()\n    }\n  }\n\n  if (queue.length > 2) {\n    void TrackPlayer.remove(Array(queue.length - 2).fill(null).map((_, i) => i)).then(() => list.splice(0, list.length - 2))\n  }\n}\nlet playPromise = Promise.resolve()\nlet actionId = Math.random()\nexport const playMusic = (musicInfo: LX.Player.PlayMusic, url: string, time: number) => {\n  const id = actionId = Math.random()\n  void playPromise.finally(() => {\n    if (id != actionId) return\n    playPromise = handlePlayMusic(musicInfo, url, time)\n  })\n}\n\n// let musicId = null\n// let duration = 0\nlet prevArtwork: string | undefined\nconst updateMetaInfo = async(mInfo: LX.Player.MusicInfo, lyric?: string) => {\n  console.log('updateMetaInfo', lyric)\n  const isShowNotificationImage = settingState.setting['player.isShowNotificationImage']\n  // const mInfo = formatMusicInfo(musicInfo)\n  // console.log('+++++updateMusicPic+++++', track.artwork, track.duration)\n\n  // if (track.musicId == musicId) {\n  //   if (global.playInfo.musicInfo.img != null) artwork = global.playInfo.musicInfo.img\n  //   if (track.duration != null) duration = global.playInfo.duration\n  // } else {\n  //   musicId = track.musicId\n  //   artwork = global.playInfo.musicInfo.img\n  //   duration = global.playInfo.duration || 0\n  // }\n  // console.log('+++++updateMetaInfo+++++', mInfo.name)\n  state.isPlaying = await TrackPlayer.getState() == State.Playing\n  let artwork = isShowNotificationImage ? mInfo.pic ?? prevArtwork : undefined\n  if (mInfo.pic) prevArtwork = mInfo.pic\n  let name: string\n  let singer: string\n  if (!state.isPlaying || lyric == null) {\n    name = mInfo.name ?? 'Unknow'\n    singer = mInfo.singer ?? 'Unknow'\n  } else {\n    name = lyric\n    singer = `${mInfo.name}${mInfo.singer ? ` - ${mInfo.singer}` : ''}`\n  }\n  await TrackPlayer.updateNowPlayingMetadata({\n    title: name,\n    artist: singer,\n    album: mInfo.album ?? undefined,\n    artwork,\n    duration: state.prevDuration || 0,\n  }, state.isPlaying)\n}\n\n\n// 解决快速切歌导致的通知栏歌曲信息与当前播放歌曲对不上的问题\nconst debounceUpdateMetaInfoTools = {\n  updateMetaPromise: Promise.resolve(),\n  musicInfo: null as LX.Player.MusicInfo | null,\n  debounce(fn: (musicInfo: LX.Player.MusicInfo, lyric?: string) => void | Promise<void>) {\n    // let delayTimer = null\n    let isDelayRun = false\n    let timer: number | null = null\n    let _musicInfo: LX.Player.MusicInfo | null = null\n    let _lyric: string | undefined\n    return (musicInfo: LX.Player.MusicInfo, lyric?: string) => {\n      // console.log('debounceUpdateMetaInfoTools', musicInfo)\n      if (timer) {\n        BackgroundTimer.clearTimeout(timer)\n        timer = null\n      }\n      // if (delayTimer) {\n      //   BackgroundTimer.clearTimeout(delayTimer)\n      //   delayTimer = null\n      // }\n      if (isDelayRun) {\n        _musicInfo = musicInfo\n        _lyric = lyric\n        timer = BackgroundTimer.setTimeout(() => {\n          timer = null\n          let musicInfo = _musicInfo\n          let lyric = _lyric\n          _musicInfo = null\n          _lyric = undefined\n          if (!musicInfo) return\n          // isDelayRun = false\n          void fn(musicInfo, lyric)\n        }, 500)\n      } else {\n        isDelayRun = true\n        void fn(musicInfo, lyric)\n        BackgroundTimer.setTimeout(() => {\n          // delayTimer = null\n          isDelayRun = false\n        }, 500)\n      }\n    }\n  },\n  init() {\n    return this.debounce(async(musicInfo: LX.Player.MusicInfo, lyric?: string) => {\n      this.musicInfo = musicInfo\n      return this.updateMetaPromise.then(() => {\n        // console.log('run')\n        if (this.musicInfo?.id === musicInfo.id) {\n          this.updateMetaPromise = updateMetaInfo(musicInfo, lyric)\n        }\n      })\n    })\n  },\n}\n\nexport const delayUpdateMusicInfo = debounceUpdateMetaInfoTools.init()\n\n// export const delayUpdateMusicInfo = ((fn, delay = 800) => {\n//   let delayTimer = null\n//   let isDelayRun = false\n//   let timer = null\n//   let _track = null\n//   return track => {\n//     _track = track\n//     if (timer) {\n//       BackgroundTimer.clearTimeout(timer)\n//       timer = null\n//     }\n//     if (isDelayRun) {\n//       if (delayTimer) {\n//         BackgroundTimer.clearTimeout(delayTimer)\n//         delayTimer = null\n//       }\n//       timer = BackgroundTimer.setTimeout(() => {\n//         timer = null\n//         let track = _track\n//         _track = null\n//         isDelayRun = false\n//         fn(track)\n//       }, delay)\n//     } else {\n//       isDelayRun = true\n//       fn(track)\n//       delayTimer = BackgroundTimer.setTimeout(() => {\n//         delayTimer = null\n//         isDelayRun = false\n//       }, 500)\n//     }\n//   }\n// })(track => {\n//   console.log('+++++delayUpdateMusicPic+++++', track.artwork)\n//   updateMetaInfo(track)\n// })\n"
  },
  {
    "path": "src/plugins/player/service.ts",
    "content": "/* eslint-disable @typescript-eslint/no-misused-promises */\nimport TrackPlayer, { State as TPState, Event as TPEvent } from 'react-native-track-player'\n// import { store } from '@/store'\n// import { action as playerAction, STATUS } from '@/store/modules/player'\nimport { isTempId, isEmpty } from './utils'\n// import { play as lrcPlay, pause as lrcPause } from '@/core/lyric'\nimport { exitApp } from '@/core/common'\nimport { getCurrentTrackId } from './playList'\nimport { pause, play, playNext, playPrev } from '@/core/player/player'\n\nlet isInitialized = false\n\n// let retryTrack: LX.Player.Track | null = null\n// let retryGetUrlId: string | null = null\n// let retryGetUrlNum = 0\n// let errorTime = 0\n// let prevDuration = 0\n// let isPlaying = false\n\n// 销毁播放器并退出\nconst handleExitApp = async(reason: string) => {\n  global.lx.isPlayedStop = false\n  exitApp(reason)\n}\n\n\nconst registerPlaybackService = async() => {\n  if (isInitialized) return\n\n  console.log('reg services...')\n  TrackPlayer.addEventListener(TPEvent.RemotePlay, () => {\n    // console.log('remote-play')\n    play()\n  })\n\n  TrackPlayer.addEventListener(TPEvent.RemotePause, () => {\n    // console.log('remote-pause')\n    void pause()\n  })\n\n  TrackPlayer.addEventListener(TPEvent.RemoteNext, () => {\n    // console.log('remote-next')\n    void playNext()\n  })\n\n  TrackPlayer.addEventListener(TPEvent.RemotePrevious, () => {\n    // console.log('remote-previous')\n    void playPrev()\n  })\n\n  TrackPlayer.addEventListener(TPEvent.RemoteStop, () => {\n    // console.log('remote-stop')\n    void handleExitApp('Remote Stop')\n  })\n\n  // TrackPlayer.addEventListener(TPEvent.RemoteDuck, async({ permanent, paused, ducking }) => {\n  //   console.log('remote-duck')\n  //   if (paused) {\n  //     store.dispatch(playerAction.setStatus({ status: STATUS.pause, text: '已暂停' }))\n  //     lrcPause()\n  //   } else {\n  //     store.dispatch(playerAction.setStatus({ status: STATUS.playing, text: '播放中...' }))\n  //     TrackPlayer.getPosition().then(position => {\n  //       lrcPlay(position * 1000)\n  //     })\n  //   }\n  // })\n\n  TrackPlayer.addEventListener(TPEvent.PlaybackError, async(err: any) => {\n    console.log('playback-error', err)\n    global.app_event.error()\n    global.app_event.playerError()\n  })\n\n  TrackPlayer.addEventListener(TPEvent.RemoteSeek, async({ position }) => {\n    global.app_event.setProgress(position as number)\n  })\n\n  TrackPlayer.addEventListener(TPEvent.PlaybackState, async info => {\n    if (global.lx.gettingUrlId || isTempId()) return\n    // let currentIsPlaying = false\n\n    switch (info.state) {\n      case TPState.None:\n        // console.log('state', 'State.NONE')\n        break\n      case TPState.Ready:\n      case TPState.Stopped:\n      case TPState.Paused:\n        global.app_event.playerPause()\n        global.app_event.pause()\n        break\n      case TPState.Playing:\n        global.app_event.playerPlaying()\n        global.app_event.play()\n        break\n      case TPState.Buffering:\n        global.app_event.pause()\n        global.app_event.playerWaiting()\n        break\n      case TPState.Connecting:\n        global.app_event.playerLoadstart()\n        break\n      default:\n        // console.log('playback-state', info)\n        break\n    }\n    if (global.lx.isPlayedStop) return handleExitApp('Timeout Exit')\n\n    // console.log('currentIsPlaying', currentIsPlaying, global.lx.playInfo.isPlaying)\n    // void updateMetaData(global.lx.store_playMusicInfo.musicInfo, currentIsPlaying)\n  })\n  TrackPlayer.addEventListener(TPEvent.PlaybackTrackChanged, async info => {\n    // console.log('PlaybackTrackChanged====>', info)\n    global.lx.playerTrackId = await getCurrentTrackId()\n    if (info.track == null) return\n    if (global.lx.isPlayedStop) return handleExitApp('Timeout Exit')\n\n    // console.log('global.lx.playerTrackId====>', global.lx.playerTrackId)\n    if (isEmpty()) {\n      // console.log('====TEMP PAUSE====')\n      await TrackPlayer.pause()\n      global.app_event.playerPause()\n      global.app_event.pause()\n      global.app_event.playerEnded()\n      global.app_event.playerEmptied()\n      // if (retryTrack) {\n      //   if (retryTrack.musicId == retryGetUrlId) {\n      //     if (++retryGetUrlNum > 1) {\n      //       store.dispatch(playerAction.playNext(true))\n      //       retryGetUrlId = null\n      //       retryTrack = null\n      //       return\n      //     }\n      //   } else {\n      //     retryGetUrlId = retryTrack.musicId\n      //     retryGetUrlNum = 0\n      //   }\n      //   store.dispatch(playerAction.refreshMusicUrl(global.lx.playInfo.currentPlayMusicInfo, errorTime))\n      // } else {\n      //   store.dispatch(playerAction.playNext(true))\n      // }\n    }\n  //   // if (!info.nextTrack) return\n  //   // if (info.track) {\n  //   //   const track = info.track.substring(0, info.track.lastIndexOf('__//'))\n  //   //   const nextTrack = info.track.substring(0, info.nextTrack.lastIndexOf('__//'))\n  //   //   console.log(nextTrack, track)\n  //   //   if (nextTrack == track) return\n  //   // }\n  //   // const track = await TrackPlayer.getTrack(info.nextTrack)\n  //   // if (!track) return\n  //   // let newTrack\n  //   // if (track.url == defaultUrl) {\n  //   //   TrackPlayer.pause().then(async() => {\n  //   //     isRefreshUrl = true\n  //   //     retryGetUrlId = track.id\n  //   //     retryGetUrlNum = 0\n  //   //     try {\n  //   //       newTrack = await updateTrackUrl(track)\n  //   //       console.log('++++newTrack++++', newTrack)\n  //   //     } catch (error) {\n  //   //       console.log('error', error)\n  //   //       if (error.message != '跳过播放') TrackPlayer.skipToNext()\n  //   //       isRefreshUrl = false\n  //   //       retryGetUrlId = null\n  //   //       return\n  //   //     }\n  //   //     retryGetUrlId = null\n  //   //     isRefreshUrl = false\n  //   //     console.log(await TrackPlayer.getQueue(), null, 2)\n  //   //     await TrackPlayer.play()\n  //   //   })\n  //   // }\n  //   // store.dispatch(playerAction.playNext())\n  })\n  // TrackPlayer.addEventListener('playback-queue-ended', async info => {\n  //   // console.log('playback-queue-ended', info)\n  //   store.dispatch(playerAction.playNext())\n  //   // if (!info.nextTrack) return\n  //   // const track = await TrackPlayer.getTrack(info.nextTrack)\n  //   // if (!track) return\n  //   // // if (track.url == defaultUrl) {\n  //   // //   TrackPlayer.pause()\n  //   // //   getMusicUrl(track.original).then(url => {\n  //   // //     TrackPlayer.updateMetadataForTrack(info.nextTrack, {\n  //   // //       url,\n  //   // //     })\n  //   // //     TrackPlayer.play()\n  //   // //   })\n  //   // // }\n  //   // if (!track.artwork) {\n  //   //   getMusicPic(track.original).then(url => {\n  //   //     console.log(url)\n  //   //     TrackPlayer.updateMetadataForTrack(info.nextTrack, {\n  //   //       artwork: url,\n  //   //     })\n  //   //   })\n  //   // }\n  // })\n  // TrackPlayer.addEventListener('playback-destroy', async() => {\n  //   console.log('playback-destroy')\n  //   store.dispatch(playerAction.destroy())\n  // })\n  isInitialized = true\n}\n\n\nexport default () => {\n  if (global.lx.playerStatus.isRegisteredService) return\n  console.log('handle registerPlaybackService...')\n  TrackPlayer.registerPlaybackService(() => registerPlaybackService)\n  global.lx.playerStatus.isRegisteredService = true\n}\n"
  },
  {
    "path": "src/plugins/player/utils.ts",
    "content": "import TrackPlayer, { Capability, Event, RepeatMode, State } from 'react-native-track-player'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { playMusic as handlePlayMusic } from './playList'\nimport { existsFile, moveFile, privateStorageDirectoryPath, temporaryDirectoryPath } from '@/utils/fs'\nimport { toast } from '@/utils/tools'\n// import { PlayerMusicInfo } from '@/store/modules/player/playInfo'\n\n\nexport { useBufferProgress } from './hook'\n\nconst emptyIdRxp = /\\/\\/default$/\nconst tempIdRxp = /\\/\\/default$|\\/\\/default\\/\\/restorePlay$/\nexport const isEmpty = (trackId = global.lx.playerTrackId) => {\n  // console.log(trackId)\n  return !trackId || emptyIdRxp.test(trackId)\n}\nexport const isTempId = (trackId = global.lx.playerTrackId) => !trackId || tempIdRxp.test(trackId)\n\n// export const replacePlayTrack = async(newTrack, oldTrack) => {\n//   console.log('replaceTrack')\n//   await TrackPlayer.add(newTrack)\n//   await TrackPlayer.skip(newTrack.id)\n//   await TrackPlayer.remove(oldTrack.id)\n// }\n\n// let timeout\n// let isFirstPlay = true\n// const updateInfo = async track => {\n//   if (isFirstPlay) {\n//     // timeout = setTimeout(() => {\n//     await delayUpdateMusicInfo(track)\n//     isFirstPlay = false\n//     // }, 500)\n//   }\n// }\n\n\n// 解决快速切歌导致的通知栏歌曲信息与当前播放歌曲对不上的问题\n// const debouncePlayMusicTools = {\n//   prevPlayMusicPromise: Promise.resolve(),\n//   trackInfo: {},\n//   isDelayUpdate: false,\n//   isDebounced: false,\n//   delay: 1000,\n//   delayTimer: null,\n//   debounce(fn, delay = 100) {\n//     let timer = null\n//     let _tracks = null\n//     let _time = null\n//     return (tracks, time) => {\n//       if (!this.isDebounced && _tracks != null) this.isDebounced = true\n//       _tracks = tracks\n//       _time = time\n//       if (timer) {\n//         BackgroundTimer.clearTimeout(timer)\n//         timer = null\n//       }\n//       if (this.isDelayUpdate) {\n//         if (this.updateDelayTimer) {\n//           BackgroundTimer.clearTimeout(this.updateDelayTimer)\n//           this.updateDelayTimer = null\n//         }\n//         timer = BackgroundTimer.setTimeout(() => {\n//           timer = null\n//           let tracks = _tracks\n//           let time = _time\n//           _tracks = null\n//           _time = null\n//           this.isDelayUpdate = false\n//           fn(tracks, time)\n//         }, delay)\n//       } else {\n//         this.isDelayUpdate = true\n//         fn(tracks, time)\n//         this.updateDelayTimer = BackgroundTimer.setTimeout(() => {\n//           this.updateDelayTimer = null\n//           this.isDelayUpdate = false\n//         }, this.delay)\n//       }\n//     }\n//   },\n//   delayUpdateMusicInfo() {\n//     if (this.delayTimer) BackgroundTimer.clearTimeout(this.delayTimer)\n//     this.delayTimer = BackgroundTimer.setTimeout(() => {\n//       this.delayTimer = null\n//       if (this.trackInfo.tracks && this.trackInfo.tracks.length) delayUpdateMusicInfo(this.trackInfo.tracks[0])\n//     }, this.delay)\n//   },\n//   init() {\n//     return this.debounce((tracks, time) => {\n//       tracks = [...tracks]\n//       this.trackInfo.tracks = tracks\n//       this.trackInfo.time = time\n//       return this.prevPlayMusicPromise.then(() => {\n//         // console.log('run')\n//         if (this.trackInfo.tracks === tracks) {\n//           this.prevPlayMusicPromise = handlePlayMusic(tracks, time).then(() => {\n//             if (this.isDebounced) {\n//               this.delayUpdateMusicInfo()\n//               this.isDebounced = false\n//             }\n//           })\n//         }\n//       })\n//     }, 200)\n//   },\n// }\n\nconst playMusic = ((fn: (musicInfo: LX.Player.PlayMusic, url: string, time: number) => void, delay = 800) => {\n  let delayTimer: number | null = null\n  let isDelayRun = false\n  let timer: number | null = null\n  let _musicInfo: LX.Player.PlayMusic | null = null\n  let _url = ''\n  let _time = 0\n  return (musicInfo: LX.Player.PlayMusic, url: string, time: number) => {\n    _musicInfo = musicInfo\n    _url = url\n    _time = time\n    if (timer) {\n      BackgroundTimer.clearTimeout(timer)\n      timer = null\n    }\n    if (isDelayRun) {\n      if (delayTimer) {\n        BackgroundTimer.clearTimeout(delayTimer)\n        delayTimer = null\n      }\n      timer = BackgroundTimer.setTimeout(() => {\n        timer = null\n        let musicInfo = _musicInfo\n        let url = _url\n        let time = _time\n        _musicInfo = null\n        _url = ''\n        _time = 0\n        isDelayRun = false\n        fn(musicInfo!, url, time)\n      }, delay)\n    } else {\n      isDelayRun = true\n      fn(musicInfo, url, time)\n      delayTimer = BackgroundTimer.setTimeout(() => {\n        delayTimer = null\n        isDelayRun = false\n      }, 500)\n    }\n  }\n})((musicInfo, url, time) => {\n  handlePlayMusic(musicInfo, url, time)\n})\n\nexport const setResource = (musicInfo: LX.Player.PlayMusic, url: string, duration?: number) => {\n  playMusic(musicInfo, url, duration ?? 0)\n}\n\nexport const setPlay = async() => TrackPlayer.play()\nexport const getPosition = async() => TrackPlayer.getPosition()\nexport const getDuration = async() => TrackPlayer.getDuration()\nexport const setStop = async() => {\n  await TrackPlayer.stop()\n  if (!isEmpty()) await TrackPlayer.skipToNext()\n}\nexport const setLoop = async(loop: boolean) => TrackPlayer.setRepeatMode(loop ? RepeatMode.Off : RepeatMode.Track)\n\nexport const setPause = async() => TrackPlayer.pause()\n// export const skipToNext = () => TrackPlayer.skipToNext()\nexport const setCurrentTime = async(time: number) => TrackPlayer.seekTo(time)\nexport const setVolume = async(num: number) => TrackPlayer.setVolume(num)\nexport const setPlaybackRate = async(num: number) => TrackPlayer.setRate(num)\nexport const updateNowPlayingTitles = async(duration: number, title: string, artist: string, album: string) => {\n  console.log('set playing titles', duration, title, artist, album)\n  return TrackPlayer.updateNowPlayingTitles(duration, title, artist, album)\n}\n\nexport const resetPlay = async() => Promise.all([setPause(), setCurrentTime(0)])\n\nexport const isCached = async(url: string) => TrackPlayer.isCached(url)\nexport const getCacheSize = async() => TrackPlayer.getCacheSize()\nexport const clearCache = async() => TrackPlayer.clearCache()\nexport const migratePlayerCache = async() => {\n  const newCachePath = privateStorageDirectoryPath + '/TrackPlayer'\n  if (await existsFile(newCachePath)) return\n  const oldCachePath = temporaryDirectoryPath + '/TrackPlayer'\n  if (!await existsFile(oldCachePath)) return\n  let timeout: number | null = BackgroundTimer.setTimeout(() => {\n    timeout = null\n    toast(global.i18n.t('player_cache_migrating'), 'long')\n  }, 2_000)\n  await moveFile(oldCachePath, newCachePath).finally(() => {\n    if (timeout) BackgroundTimer.clearTimeout(timeout)\n  })\n}\n\nexport const destroy = async() => {\n  if (global.lx.playerStatus.isIniting || !global.lx.playerStatus.isInitialized) return\n  await TrackPlayer.destroy()\n  global.lx.playerStatus.isInitialized = false\n}\n\ntype PlayStatus = 'None' | 'Ready' | 'Playing' | 'Paused' | 'Stopped' | 'Buffering' | 'Connecting'\n\nexport const onStateChange = async(listener: (state: PlayStatus) => void) => {\n  const sub = TrackPlayer.addEventListener(Event.PlaybackState, state => {\n    let _state: PlayStatus\n    switch (state) {\n      case State.Ready:\n        _state = 'Ready'\n        break\n      case State.Playing:\n        _state = 'Playing'\n        break\n      case State.Paused:\n        _state = 'Paused'\n        break\n      case State.Stopped:\n        _state = 'Stopped'\n        break\n      case State.Buffering:\n        _state = 'Buffering'\n        break\n      case State.Connecting:\n        _state = 'Connecting'\n        break\n      case State.None:\n      default:\n        _state = 'None'\n        break\n    }\n    listener(_state)\n  })\n\n  return () => {\n    sub.remove()\n  }\n}\n\n/**\n * Subscription player state chuange event\n * @param options state change event\n * @returns remove event function\n */\n// export const playState = callback => TrackPlayer.addEventListener('playback-state', callback)\n\nexport const updateOptions = async(options = {\n  // Whether the player should stop running when the app is closed on Android\n  // stopWithApp: true,\n\n  // An array of media controls capabilities\n  // Can contain CAPABILITY_PLAY, CAPABILITY_PAUSE, CAPABILITY_STOP, CAPABILITY_SEEK_TO,\n  // CAPABILITY_SKIP_TO_NEXT, CAPABILITY_SKIP_TO_PREVIOUS, CAPABILITY_SET_RATING\n  capabilities: [\n    Capability.Play,\n    Capability.Pause,\n    Capability.Stop,\n    Capability.SeekTo,\n    Capability.SkipToNext,\n    Capability.SkipToPrevious,\n  ],\n\n  notificationCapabilities: [\n    Capability.Play,\n    Capability.Pause,\n    Capability.Stop,\n    Capability.SkipToNext,\n    Capability.SkipToPrevious,\n  ],\n\n  // // An array of capabilities that will show up when the notification is in the compact form on Android\n  compactCapabilities: [\n    Capability.Play,\n    Capability.Pause,\n    Capability.Stop,\n    Capability.SkipToNext,\n  ],\n\n  // Icons for the notification on Android (if you don't like the default ones)\n  // playIcon: require('./play-icon.png'),\n  // pauseIcon: require('./pause-icon.png'),\n  // stopIcon: require('./stop-icon.png'),\n  // previousIcon: require('./previous-icon.png'),\n  // nextIcon: require('./next-icon.png'),\n  // icon: notificationIcon, // The notification icon\n}) => {\n  return TrackPlayer.updateOptions(options)\n}\n\n// export const setMaxCache = async size => {\n//   // const currentTrack = await TrackPlayer.getCurrentTrack()\n//   // if (!currentTrack) return\n//   // console.log(currentTrack)\n//   // const currentTime = await TrackPlayer.getPosition()\n//   // const state = await TrackPlayer.getState()\n//   // await stop()\n//   // await TrackPlayer.destroy()\n//   // await TrackPlayer.setupPlayer({ maxCacheSize: size * 1024, maxBuffer: 1000, waitForBuffer: true })\n//   // await updateOptions()\n//   // await TrackPlayer.seekTo(currentTime)\n//   // switch (state) {\n//   //   case TrackPlayer.STATE_PLAYING:\n//   //   case TrackPlayer.STATE_BUFFERING:\n//   //     await TrackPlayer.play()\n//   //     break\n//   //   default:\n//   //     break\n//   // }\n// }\n\n// export {\n//   useProgress,\n// }\n\nexport { updateMetaData, initTrackInfo } from './playList'\n"
  },
  {
    "path": "src/plugins/storage.ts",
    "content": "import AsyncStorage from '@react-native-async-storage/async-storage'\nimport { log } from '@/utils/log'\n\nconst partKeyPrefix = '@___PART___'\nconst partKeyArrPrefix = '@___PART_A___'\nconst partKeyPrefixRxp = /^@___PART___/\nconst partKeyArrPrefixRxp = /^@___PART_A___/\nconst keySplit = ','\nconst limit = 500000\n\nconst buildData = (key: string, value: any, datas: Array<[string, string]>) => {\n  let valueStr = JSON.stringify(value)\n  if (valueStr.length <= limit) {\n    datas.push([key, valueStr])\n    return\n  }\n\n  const partKeys = []\n  for (let i = 0, len = Math.floor(valueStr.length / limit); i <= len; i++) {\n    let partKey = `${partKeyArrPrefix}${key}${i}`\n    partKeys.push(partKey)\n    datas.push([partKey, valueStr.substring(i * limit, (i + 1) * limit)])\n  }\n  datas.push([key, partKeyArrPrefix + JSON.stringify(partKeys)])\n}\n\n// 1.4.0 之前的数据分片存储方式，存在key的内容与分隔符冲突的问题\n// 1.4.0 开始改用数组存储，不再使用分隔符的方式\nconst handleGetDataOld = async<T>(partKeys: string): Promise<T> => {\n  const keys = partKeys.replace(partKeyPrefixRxp, '').split(keySplit)\n\n  return AsyncStorage.multiGet(keys).then(datas => {\n    return JSON.parse(datas.map(data => data[1]).join(''))\n  })\n}\n\nconst handleGetData = async<T>(partKeys: string): Promise<T> => {\n  if (partKeys.startsWith(partKeyPrefix)) return handleGetDataOld<T>(partKeys)\n\n  const keys = JSON.parse(partKeys.replace(partKeyArrPrefixRxp, '')) as string[]\n  return AsyncStorage.multiGet(keys).then(datas => {\n    return JSON.parse(datas.map(data => data[1]).join(''))\n  })\n}\n\nexport const saveData = async(key: string, value: any) => {\n  const datas: Array<[string, string]> = []\n  buildData(key, value, datas)\n\n  try {\n    await removeData(key)\n    await AsyncStorage.multiSet(datas)\n  } catch (e: any) {\n    // saving error\n    log.error('storage error[saveData]:', key, e.message)\n    throw e\n  }\n}\n\nexport const getData = async<T = unknown>(key: string): Promise<T | null> => {\n  let value: string | null\n  try {\n    value = await AsyncStorage.getItem(key)\n  } catch (e: any) {\n    // error reading value\n    log.error('storage error[getData]:', key, e.message)\n    throw e\n  }\n  if (value && (partKeyPrefixRxp.test(value) || partKeyArrPrefixRxp.test(value))) {\n    return handleGetData<T>(value)\n  } else if (value == null) return value\n  return JSON.parse(value)\n}\n\nexport const removeData = async(key: string) => {\n  let value: string | null\n  try {\n    value = await AsyncStorage.getItem(key)\n  } catch (e: any) {\n    // error reading value\n    log.error('storage error[removeData]:', key, e.message)\n    throw e\n  }\n  if (value) {\n    if (partKeyPrefixRxp.test(value)) {\n      let partKeys = value.replace(partKeyPrefixRxp, '').split(keySplit)\n      partKeys.push(key)\n      try {\n        await AsyncStorage.multiRemove(partKeys)\n      } catch (e: any) {\n        // remove error\n        log.error('storage error[removeData]:', key, e.message)\n        throw e\n      }\n      return\n    } else if (partKeyArrPrefixRxp.test(value)) {\n      let partKeys = JSON.parse(value.replace(partKeyArrPrefixRxp, '')) as string[]\n      partKeys.push(key)\n      try {\n        await AsyncStorage.multiRemove(partKeys)\n      } catch (e: any) {\n        // remove error\n        log.error('storage error[removeData]:', key, e.message)\n        throw e\n      }\n      return\n    }\n  }\n\n  try {\n    await AsyncStorage.removeItem(key)\n  } catch (e: any) {\n    // remove error\n    log.error('storage error[removeData]:', key, e.message)\n    throw e\n  }\n}\n\nexport const getAllKeys = async() => {\n  let keys\n  try {\n    keys = await AsyncStorage.getAllKeys()\n  } catch (e: any) {\n    // read key error\n    log.error('storage error[getAllKeys]:', e.message)\n    throw e\n  }\n\n  return keys\n}\n\n\nexport const getDataMultiple = async<T extends readonly string[]>(keys: T) => {\n  type RawData = { [K in keyof T]: [T[K], string | null] }\n  let datas: RawData\n  try {\n    datas = await AsyncStorage.multiGet(keys) as RawData\n  } catch (e: any) {\n    // read error\n    log.error('storage error[getDataMultiple]:', e.message)\n    throw e\n  }\n  const promises: Array<Promise<ReadonlyArray<[unknown | null]>>> = []\n  for (const [, value] of datas) {\n    if (value && (partKeyPrefixRxp.test(value) || partKeyArrPrefixRxp.test(value))) {\n      promises.push(handleGetData(value))\n    } else {\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      promises.push(Promise.resolve(value ? JSON.parse(value) : value))\n    }\n  }\n  return Promise.all(promises).then(values => {\n    return datas.map(([key], index) => ([key, values[index]])) as { [K in keyof T]: [T[K], unknown] }\n  })\n}\n\nexport const saveDataMultiple = async(datas: Array<[string, any]>) => {\n  const allData: Array<[string, string]> = []\n  for (const [key, value] of datas) {\n    buildData(key, value, allData)\n  }\n  try {\n    await removeDataMultiple(datas.map(k => k[0]))\n    await AsyncStorage.multiSet(allData)\n  } catch (e: any) {\n    // save error\n    log.error('storage error[saveDataMultiple]:', e.message)\n    throw e\n  }\n}\n\n\nexport const removeDataMultiple = async(keys: string[]) => {\n  if (!keys.length) return\n  const datas = await AsyncStorage.multiGet(keys)\n  let allKeys = []\n  for (const [key, value] of datas) {\n    allKeys.push(key)\n    if (value) {\n      if (partKeyPrefixRxp.test(value)) {\n        allKeys.push(...value.replace(partKeyPrefixRxp, '').split(keySplit))\n      } else if (partKeyArrPrefixRxp.test(value)) {\n        allKeys.push(...JSON.parse(value.replace(partKeyArrPrefixRxp, '')) as string[])\n      }\n    }\n  }\n  try {\n    await AsyncStorage.multiRemove(allKeys)\n  } catch (e: any) {\n    // remove error\n    log.error('storage error[removeDataMultiple]:', e.message)\n    throw e\n  }\n}\n\nexport const clearAll = async() => {\n  try {\n    await AsyncStorage.clear()\n  } catch (e: any) {\n    // clear error\n    log.error('storage error[clearAll]:', e.message)\n    throw e\n  }\n}\n\nexport { useAsyncStorage } from '@react-native-async-storage/async-storage'\n"
  },
  {
    "path": "src/plugins/sync/client/auth.ts",
    "content": "import { request, generateRsaKey } from './utils'\nimport { getSyncAuthKey, setSyncAuthKey } from '../data'\nimport log from '../log'\nimport { aesDecrypt, aesEncrypt, rsaDecrypt } from '../utils'\nimport { getDeviceName } from '@/utils/nativeModules/utils'\nimport { toMD5 } from '@/utils/tools'\nimport { SYNC_CODE } from '../constants'\n\nconst hello = async(urlInfo: LX.Sync.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)\n  .then(({ text }) => {\n    if (text == SYNC_CODE.helloMsg) return true\n    if (text.startsWith('Hello~::^-^::')) {\n      const verRxp = /v(\\d+)/\n      let result = verRxp.exec(text)?.[1]\n      if (result != null) {\n        const servVer = parseInt(result)\n        const localVer = parseInt(verRxp.exec(SYNC_CODE.helloMsg)![1])\n        if (servVer > localVer) throw new Error(SYNC_CODE.highServiceVersion)\n        else if (servVer < localVer) throw new Error(SYNC_CODE.lowServiceVersion)\n      }\n    }\n    return false\n  })\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.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.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${await getDeviceName()}\\nlx_music_mobile`, 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.KeyInfo\n    void setSyncAuthKey(serverId, info)\n    return info\n  })\n}\n\nconst keyAuth = async(urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {\n  const msg = aesEncrypt(SYNC_CODE.authMsg + await getDeviceName(), keyInfo.key)\n  return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(async({ 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.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.UrlInfo, authCode?: string) => {\n  console.log('connect: ', urlInfo.href, authCode)\n  console.log(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)\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/plugins/sync/client/client.ts",
    "content": "import { encryptMsg, decryptMsg } from './utils'\nimport { callObj } from './sync'\n// import { action as commonAction } from '@/store/modules/common'\n// import { getStore } from '@/store'\n// import registerSyncListHandler from './syncList'\nimport log from '../log'\nimport { aesEncrypt } from '../utils'\nimport { setSyncStatus } from '@/core/sync'\nimport { dateFormat } from '@/utils/common'\nimport { createMsg2call } from 'message2call'\nimport { toast } from '@/utils/tools'\nimport { SYNC_CLOSE_CODE, SYNC_CODE } from '../constants'\n\nlet status: LX.Sync.Status = {\n  status: false,\n  message: '',\n}\n\nexport const sendSyncStatus = (newStatus: Omit<LX.Sync.Status, 'address'>) => {\n  status.status = newStatus.status\n  status.message = newStatus.message\n  setSyncStatus(status)\n}\n\nexport const sendSyncMessage = (message: string) => {\n  status.message = message\n  setSyncStatus(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?.close()\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, 30000)\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.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.addEventListener('open', () => {\n      if (this.connectTimeout) {\n        clearTimeout(this.connectTimeout)\n        this.connectTimeout = null\n      }\n      this.handleOpen()\n    })\n    socket.addEventListener('message', ({ data }) => {\n      if (data == 'ping') this.heartbeat()\n    })\n    socket.addEventListener('close', (event) => {\n      // console.log(event.code)\n      switch (event.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.Socket | null\n// let listSyncPromise: Promise<void>\nexport const connect = (urlInfo: LX.Sync.UrlInfo, keyInfo: LX.Sync.KeyInfo) => {\n  client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`) as LX.Sync.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        toast('Sync connected')\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.r_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.Status => status\n"
  },
  {
    "path": "src/plugins/sync/client/index.ts",
    "content": "import handleAuth from './auth'\nimport { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client'\n// import { getSyncHost } from '@/utils/data'\nimport log from '../log'\nimport { parseUrl } from './utils'\nimport { SYNC_CODE } from '../constants'\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  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 as string)\n})\n\nexport {\n  connectServer,\n  disconnectServer,\n}\n\nexport {\n  getStatus,\n} from './client'\n"
  },
  {
    "path": "src/plugins/sync/client/modules/dislike/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\nimport { handleRemoteDislikeAction, getLocalDislikeData, setLocalDislikeData } from '../../../dislikeEvent'\nimport log from '../../../log'\n// import { SYNC_CLOSE_CODE } from '@/config/constant'\nimport { removeSyncModeEvent, selectSyncMode } from '@/core/sync'\nimport { toMD5 } from '@/utils/tools'\nimport { registerEvent, unregisterEvent } from './localEvent'\n\nconst logInfo = (eventName: string, success = false) => {\n  log.info(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')}${success ? ' success' : ''}`)\n}\n// const logError = (eventName: string, err: Error) => {\n//   log.error(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')} error: ${err.message}`)\n// }\nconst handler: LX.Sync.ClientSyncHandlerDislikeActions<LX.Sync.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    logInfo('dislike:sync:dislike_sync_get_sync_mode')\n    const unsubscribe = socket.onClose(() => {\n      removeSyncModeEvent()\n    })\n    return selectSyncMode(socket.data.keyInfo.serverName, 'dislike').finally(unsubscribe)\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"
  },
  {
    "path": "src/plugins/sync/client/modules/dislike/index.ts",
    "content": "\nexport { default as handler } from './handler'\n\nexport * from './localEvent'\n"
  },
  {
    "path": "src/plugins/sync/client/modules/dislike/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@/plugins/sync/constants'\nimport { registerDislikeActionEvent } from '../../../dislikeEvent'\n\nlet unregisterLocalListAction: (() => void) | null\n\nexport const registerEvent = (socket: LX.Sync.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/plugins/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/plugins/sync/client/modules/list/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\nimport { handleRemoteListAction, getLocalListData, setLocalListData } from '../../../listEvent'\nimport log from '../../../log'\n// import { SYNC_CLOSE_CODE } from '@/config/constant'\nimport { removeSyncModeEvent, selectSyncMode } from '@/core/sync'\nimport { toMD5 } from '@/utils/tools'\nimport { registerEvent, unregisterEvent } from './localEvent'\n\nconst logInfo = (eventName: string, success = false) => {\n  log.info(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')}${success ? ' success' : ''}`)\n}\n// const logError = (eventName: string, err: Error) => {\n//   log.error(`[${eventName}]${eventName.replace('list:sync:list_sync_', '').replace(/_/g, ' ')} error: ${err.message}`)\n// }\nconst handler: LX.Sync.ClientSyncHandlerListActions<LX.Sync.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  async list_sync_get_sync_mode(socket) {\n    logInfo('list:sync:list_sync_get_sync_mode')\n    const unsubscribe = socket.onClose(() => {\n      removeSyncModeEvent()\n    })\n    return selectSyncMode(socket.data.keyInfo.serverName, 'list').finally(unsubscribe)\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\n\n// export default async(socket: LX.Sync.Socket) => new Promise<void>((resolve, reject) => {\n//   let listenEvents: Array<() => void> = []\n//   const unregisterEvents = () => {\n//     while (listenEvents.length) listenEvents.shift()?.()\n//   }\n\n//   socket.onClose(() => {\n//     unregisterEvents()\n//     removeSyncModeEvent()\n//     reject(new Error('closed'))\n//   })\n//   listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {\n//     logInfo('list:sync:list_sync_get_list_data')\n//     socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {\n//       if (err) {\n//         logError('list:sync:list_sync_get_list_data', err)\n//         socket.close(SYNC_CLOSE_CODE.failed)\n//         return\n//       }\n//       logInfo('list:sync:list_sync_get_list_data', true)\n//     })\n//   }))\n//   listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {\n//     logInfo('list:sync:list_sync_get_sync_mode')\n//     let mode: LX.Sync.List.SyncMode\n//     try {\n//       mode = await selectSyncMode(socket.data.keyInfo.serverName)\n//     } catch (err: unknown) {\n//       logError('list:sync:list_sync_get_sync_mode', err as Error)\n//       socket.close(SYNC_CLOSE_CODE.normal)\n//       return\n//     }\n//     socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {\n//       if (err) {\n//         logError('list:sync:list_sync_get_sync_mode', err)\n//         socket.close(SYNC_CLOSE_CODE.failed)\n//         return\n//       }\n//       logInfo('list:sync:list_sync_get_sync_mode', true)\n//     })\n//   }))\n//   listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {\n//     logInfo('list:sync:list_sync_set_data')\n//     await setLocalListData(data)\n//     logInfo('list:sync:list_sync_set_data', true)\n//   }))\n//   listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {\n//     unregisterEvents()\n//     resolve()\n//     logInfo('list:sync:finished', true)\n//     toast('Sync successfully')\n//   }))\n// })\n"
  },
  {
    "path": "src/plugins/sync/client/modules/list/index.ts",
    "content": "\nexport { default as handler } from './handler'\n\nexport * from './localEvent'\n"
  },
  {
    "path": "src/plugins/sync/client/modules/list/localEvent.ts",
    "content": "import { SYNC_CLOSE_CODE } from '@/plugins/sync/constants'\nimport { registerListActionEvent } from '../../../listEvent'\n\nlet unregisterLocalListAction: (() => void) | null\n\nexport const registerEvent = (socket: LX.Sync.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/plugins/sync/client/sync/handler.ts",
    "content": "// 这个文件导出的方法将暴露给服务端调用，第一个参数固定为当前 socket 对象\n// import { getUserSpace } from '@/user'\n// import { modules } from '../modules'\n\nimport { featureVersion } from '../modules'\n\n\nconst handler: Omit<LX.Sync.ClientSyncHandlerActions<LX.Sync.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/plugins/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/plugins/sync/client/utils.ts",
    "content": "// import { generateKeyPair } from 'crypto'\nimport { gzipString, unGzipString } from '@/utils/fs'\nimport BackgroundTimer from 'react-native-background-timer'\n\nexport const request = async(url: string, { timeout = 10000, ...options }: RequestInit & { timeout?: number } = {}) => {\n  const controller = new AbortController()\n  let id: number | null = BackgroundTimer.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    BackgroundTimer.clearTimeout(id)\n  })\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 { generateRsaKey } from '@/utils/nativeModules/crypto'\n// export 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\n\nexport const encryptMsg = async(keyInfo: LX.Sync.KeyInfo, msg: string): Promise<string> => {\n  return msg.length > 1024\n    ? 'cg_' + await gzipString(msg)\n    : msg\n  // if (!keyInfo) return ''\n  // return aesEncrypt(msg, keyInfo.key, keyInfo.iv)\n}\n\nexport const decryptMsg = async(keyInfo: LX.Sync.KeyInfo, enMsg: string): Promise<string> => {\n  return enMsg.substring(0, 3) == 'cg_'\n    ? unGzipString(enMsg.replace('cg_', ''))\n    : 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 = (href: string): LX.Sync.UrlInfo => {\n  // const url = new URL(host)\n  // console.log(host)\n  // let hostPath = url.host + url.pathname\n  // let href = url.href\n  if (href.endsWith('/')) href = href.replace(/\\/$/, '')\n  // if (href.endsWith('/')) href = href.replace(/\\/$/, '')\n  const httpProtocol = /^https:/.test(href) ? 'https:' : 'http:'\n\n  console.log({\n    wsProtocol: httpProtocol == 'https:' ? 'wss:' : 'ws:',\n    httpProtocol,\n    hostPath: href.replace(httpProtocol + '//', ''),\n    href,\n  })\n\n  return {\n    wsProtocol: httpProtocol == 'https:' ? 'wss:' : 'ws:',\n    httpProtocol,\n    hostPath: href.replace(httpProtocol + '//', ''),\n    href,\n  }\n}\n\n\nexport const sendStatus = (status: LX.Sync.Status) => {\n  // syncLog.log(JSON.stringify(status))\n}\n"
  },
  {
    "path": "src/plugins/sync/constants.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  lowServiceVersion: 'Sync server version too low, please upgrade server to latest',\n  highServiceVersion: 'Sync server version too high, please upgrade app to latest',\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  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} as const\n\nexport const FeaturesList = [\n  'list',\n  'dislike',\n] as const\n"
  },
  {
    "path": "src/plugins/sync/data.ts",
    "content": "export {\n  getSyncAuthKey,\n  setSyncAuthKey,\n  getSyncHost,\n  setSyncHost,\n  getSyncHostHistory,\n  addSyncHostHistory,\n  removeSyncHostHistory,\n} from '@/utils/data'\n"
  },
  {
    "path": "src/plugins/sync/dislikeEvent.ts",
    "content": "import { state } from '@/store/dislikeList'\n\nexport const getLocalDislikeData = async(): Promise<LX.Dislike.DislikeRules> => {\n  return state.dislikeInfo.rules\n}\n\nexport const setLocalDislikeData = async(listData: LX.Dislike.DislikeRules) => {\n  await global.dislike_event.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.dislike_event.on('dislike_music_add', dislike_music_add)\n  global.dislike_event.on('dislike_data_overwrite', dislike_data_overwrite)\n  global.dislike_event.on('dislike_music_clear', dislike_music_clear)\n  return () => {\n    global.dislike_event.off('dislike_music_add', dislike_music_add)\n    global.dislike_event.off('dislike_data_overwrite', dislike_data_overwrite)\n    global.dislike_event.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.dislike_event.dislike_music_add(event.data, true)\n      break\n    case 'dislike_data_overwrite':\n      await global.dislike_event.dislike_data_overwrite(event.data, true)\n      break\n    case 'dislike_music_clear':\n      await global.dislike_event.dislike_music_clear(true)\n      break\n    default:\n      throw new Error('unknown dislike sync action')\n  }\n}\n"
  },
  {
    "path": "src/plugins/sync/index.ts",
    "content": "// import Event from './event/event'\n\nexport {\n  connectServer,\n  disconnectServer,\n  getStatus,\n} from './client'\n"
  },
  {
    "path": "src/plugins/sync/listEvent.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\nimport { getListMusics } from '@/core/list'\nimport { userLists } from '@/utils/listManage'\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  return Promise.all([\n    getListMusics(LIST_IDS.DEFAULT),\n    getListMusics(LIST_IDS.LOVE),\n    // eslint-disable-next-line @typescript-eslint/promise-function-async\n    ...userLists.map(l => getListMusics(l.id)),\n  ]).then(([defaultList, loveList, ...userList]) => {\n    return {\n      defaultList,\n      loveList,\n      userList: userLists.map((l, i) => buildUserListInfoFull({ ...l, list: userList[i] })),\n    }\n  })\n}\n\nexport const setLocalListData = async(listData: LX.Sync.List.ListData) => {\n  await global.list_event.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.list_event.on('list_data_overwrite', list_data_overwrite)\n  global.list_event.on('list_create', list_create)\n  global.list_event.on('list_remove', list_remove)\n  global.list_event.on('list_update', list_update)\n  global.list_event.on('list_update_position', list_update_position)\n  global.list_event.on('list_music_overwrite', list_music_overwrite)\n  global.list_event.on('list_music_add', list_music_add)\n  global.list_event.on('list_music_move', list_music_move)\n  global.list_event.on('list_music_remove', list_music_remove)\n  global.list_event.on('list_music_update', list_music_update)\n  global.list_event.on('list_music_clear', list_music_clear)\n  global.list_event.on('list_music_update_position', list_music_update_position)\n  return () => {\n    global.list_event.off('list_data_overwrite', list_data_overwrite)\n    global.list_event.off('list_create', list_create)\n    global.list_event.off('list_remove', list_remove)\n    global.list_event.off('list_update', list_update)\n    global.list_event.off('list_update_position', list_update_position)\n    global.list_event.off('list_music_overwrite', list_music_overwrite)\n    global.list_event.off('list_music_add', list_music_add)\n    global.list_event.off('list_music_move', list_music_move)\n    global.list_event.off('list_music_remove', list_music_remove)\n    global.list_event.off('list_music_update', list_music_update)\n    global.list_event.off('list_music_clear', list_music_clear)\n    global.list_event.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.list_event.list_data_overwrite(data, true)\n      break\n    case 'list_create':\n      await global.list_event.list_create(data.position, data.listInfos, true)\n      break\n    case 'list_remove':\n      await global.list_event.list_remove(data, true)\n      break\n    case 'list_update':\n      await global.list_event.list_update(data, true)\n      break\n    case 'list_update_position':\n      await global.list_event.list_update_position(data.position, data.ids, true)\n      break\n    case 'list_music_add':\n      await global.list_event.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)\n      break\n    case 'list_music_move':\n      await global.list_event.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)\n      break\n    case 'list_music_remove':\n      await global.list_event.list_music_remove(data.listId, data.ids, true)\n      break\n    case 'list_music_update':\n      await global.list_event.list_music_update(data, true)\n      break\n    case 'list_music_update_position':\n      await global.list_event.list_music_update_position(data.listId, data.position, data.ids, true)\n      break\n    case 'list_music_overwrite':\n      await global.list_event.list_music_overwrite(data.listId, data.musicInfos, true)\n      break\n    case 'list_music_clear':\n      await global.list_event.list_music_clear(data, true)\n      break\n    default:\n      throw new Error('unknown list sync action')\n  }\n}\n"
  },
  {
    "path": "src/plugins/sync/log.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unsafe-argument */\nimport { log as writeLog } from '@/utils/log'\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  },\n  warn(...params: any[]) {\n    if (global.lx.isEnableSyncLog) writeLog.warn(...params)\n  },\n  error(...params: any[]) {\n    if (global.lx.isEnableSyncLog) writeLog.error(...params)\n  },\n}\n"
  },
  {
    "path": "src/plugins/sync/utils.ts",
    "content": "// import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'crypto'\nimport { aesEncryptSync, aesDecryptSync, rsaEncryptSync, rsaDecryptSync, AES_MODE, RSA_PADDING } from '@/utils/nativeModules/crypto'\nimport { btoa } from 'react-native-quick-base64'\n\n\nexport const aesEncrypt = (text: string, b64Key: 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  return aesEncryptSync(btoa(text), b64Key, '', AES_MODE.ECB_128_NoPadding)\n}\n\nexport const aesDecrypt = (text: string, b64Key: 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  return aesDecryptSync(text, b64Key, '', AES_MODE.ECB_128_NoPadding)\n}\n\nexport const rsaEncrypt = (buffer: Buffer, key: string): string => {\n  // return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')\n  return rsaEncryptSync(buffer.toString('base64'), key, RSA_PADDING.OAEPWithSHA1AndMGF1Padding)\n}\nexport const rsaDecrypt = (buffer: Buffer, key: string): string => {\n  // return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)\n  return rsaDecryptSync(buffer.toString('base64'), key, RSA_PADDING.OAEPWithSHA1AndMGF1Padding)\n}\n"
  },
  {
    "path": "src/resources/fonts/selection.json",
    "content": "{\"IcoMoonType\":\"selection\",\"icons\":[{\"icon\":{\"paths\":[\"M712.832 97.835l-60.331 60.331 97.835 97.835h-25.003c-258.688 0-469.333 210.645-469.333 469.333v42.667h85.333v-42.667c0-212.608 171.392-384 384-384h25.003l-97.835 97.835 60.331 60.331 200.832-200.832-200.832-200.832zM85.333 341.333v469.333c0 70.187 57.813 128 128 128h597.333c70.187 0 128-57.813 128-128v-85.333h-85.333v85.333c0 24.107-18.56 42.667-42.667 42.667h-597.333c-24.107 0-42.667-18.56-42.667-42.667v-469.333h-85.333z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"share\"],\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":108,\"id\":57,\"name\":\"share\",\"prevSize\":32,\"code\":59699},\"setIdx\":0,\"setId\":2,\"iconIdx\":0},{\"icon\":{\"paths\":[\"M512 185.28c-180.135 0-326.72 146.585-326.72 326.72s146.585 326.72 326.72 326.72c180.135 0 326.72-146.585 326.72-326.72s-146.585-326.72-326.72-326.72z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"full_stop\"],\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":104,\"id\":52,\"name\":\"full_stop\",\"prevSize\":32,\"code\":59695},\"setIdx\":0,\"setId\":2,\"iconIdx\":1},{\"icon\":{\"paths\":[\"M378.5 85.333c-25.45 0-49.884 10.102-67.875 28.125l-111.833 111.833c-18.023 17.991-28.125 42.425-28.125 67.875v101.5c0.002 17.672 14.328 31.998 32 32l32 0v42.667h-32c-17.672 0.002-31.998 14.328-32 32l-0 0v320c0 64.422 52.911 117.333 117.333 117.333h448c64.422 0 117.333-52.911 117.333-117.333v-618.667c0-64.422-52.911-117.333-117.333-117.333h-357.5zM378.5 149.333h357.5c29.829 0 53.333 23.505 53.333 53.333v618.667c0 29.829-23.505 53.333-53.333 53.333h-448c-29.829 0-53.333-23.505-53.333-53.333v-288h32c17.672-0.002 31.998-14.328 32-32l0-0v-106.667c-0.002-17.672-14.328-31.998-32-32l-32-0v-69.5c0-8.512 3.376-16.595 9.375-22.583l111.875-111.875c5.988-5.999 14.071-9.375 22.583-9.375zM394.167 191.542c-17.459 0.281-31.503 14.5-31.503 31.999 0 0.161 0.001 0.323 0.004 0.483l-0-0.024v106.667c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024v-106.667c0.002-0.136 0.003-0.298 0.003-0.459 0-17.675-14.328-32.003-32.003-32.003-0.176 0-0.351 0.001-0.526 0.004l0.026-0zM500.833 191.542c-17.459 0.281-31.503 14.5-31.503 31.999 0 0.161 0.001 0.323 0.004 0.483l-0-0.024v106.667c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024v-106.667c0.002-0.136 0.003-0.298 0.003-0.459 0-17.675-14.328-32.003-32.003-32.003-0.176 0-0.351 0.001-0.526 0.004l0.026-0zM607.5 191.542c-17.459 0.281-31.503 14.5-31.503 31.999 0 0.161 0.001 0.323 0.004 0.483l-0-0.024v106.667c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024v-106.667c0.002-0.136 0.003-0.298 0.003-0.459 0-17.675-14.328-32.003-32.003-32.003-0.176 0-0.351 0.001-0.526 0.004l0.026-0zM714.167 191.542c-17.459 0.281-31.503 14.5-31.503 31.999 0 0.161 0.001 0.323 0.004 0.483l-0-0.024v106.667c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024v-106.667c0.002-0.136 0.003-0.298 0.003-0.459 0-17.675-14.328-32.003-32.003-32.003-0.176 0-0.351 0.001-0.526 0.004l0.026-0z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"sd-card\"],\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":98,\"id\":50,\"name\":\"sd-card\",\"prevSize\":32,\"code\":59696},\"setIdx\":0,\"setId\":2,\"iconIdx\":2},{\"icon\":{\"paths\":[\"M512 85.333c-235.263 0-426.667 191.404-426.667 426.667s191.404 426.667 426.667 426.667c235.263 0 426.667-191.404 426.667-426.667s-191.404-426.667-426.667-426.667zM512 149.333c200.674 0 362.667 161.992 362.667 362.667s-161.992 362.667-362.667 362.667c-200.674 0-362.667-161.992-362.667-362.667s161.992-362.667 362.667-362.667zM512 277.333c-70.312 0-128 57.688-128 128v10.667c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024v-10.667c0-35.715 28.285-64 64-64s64 28.285 64 64c0 49.939-12.984 56.197-35.75 74.083-11.383 8.943-26.292 19.254-39.042 36.625s-21.208 41.585-21.208 70.625c-0.002 0.135-0.003 0.293-0.003 0.453 0 17.675 14.328 32.003 32.003 32.003s32.003-14.328 32.003-32.003c0-0.159-0.001-0.318-0.003-0.477l0 0.024c0-18.202 3.541-25.596 8.792-32.75s14.341-14.254 26.958-24.167c25.234-19.826 60.25-57.023 60.25-124.417 0-70.312-57.688-128-128-128zM512 682.667c-23.564 0-42.667 19.103-42.667 42.667s19.103 42.667 42.667 42.667v0c23.564 0 42.667-19.103 42.667-42.667s-19.103-42.667-42.667-42.667v0z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-help\"],\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":97,\"id\":49,\"name\":\"help\",\"prevSize\":32,\"code\":59694},\"setIdx\":0,\"setId\":2,\"iconIdx\":3},{\"icon\":{\"paths\":[\"M810.667 128h-597.333c-47.36 0-85.333 37.973-85.333 85.333v597.333c0 47.128 38.205 85.333 85.333 85.333v0h597.333c47.128 0 85.333-38.205 85.333-85.333v0-597.333c0-47.36-38.4-85.333-85.333-85.333zM810.667 213.333v597.333h-597.333v-597.333h597.333z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"checkbox\"],\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":95,\"id\":3,\"name\":\"checkbox-blank-outline\",\"prevSize\":32,\"code\":59691},\"setIdx\":0,\"setId\":2,\"iconIdx\":4},{\"icon\":{\"paths\":[\"M426.667 725.333l-213.333-213.333 60.16-60.587 153.173 153.173 323.84-323.84 60.16 60.587zM810.667 128h-597.333c-47.36 0-85.333 37.973-85.333 85.333v597.333c0 47.128 38.205 85.333 85.333 85.333v0h597.333c47.128 0 85.333-38.205 85.333-85.333v0-597.333c0-47.36-38.4-85.333-85.333-85.333z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"checkbox\"],\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":96,\"id\":4,\"name\":\"checkbox-marked\",\"prevSize\":32,\"code\":59692},\"setIdx\":0,\"setId\":2,\"iconIdx\":5},{\"icon\":{\"paths\":[\"M725.333 554.667h-426.667v-85.333h426.667zM810.667 128h-597.333c-47.36 0-85.333 37.973-85.333 85.333v597.333c0 47.128 38.205 85.333 85.333 85.333v0h597.333c47.128 0 85.333-38.205 85.333-85.333v0-597.333c0-47.36-38.4-85.333-85.333-85.333z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"checkbox\"],\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":51,\"id\":5,\"name\":\"minus-box\",\"prevSize\":32,\"code\":59693},\"setIdx\":0,\"setId\":2,\"iconIdx\":6},{\"icon\":{\"paths\":[\"M510.718-13.119c-8.781 0.309-16.775 3.446-23.157 8.523l0.080-0.062-373.172 293.995c-47.363 37.326-75.076 94.363-75.076 154.664v501.222c0 35.802 29.837 65.639 65.639 65.639h262.559c35.802 0 65.639-29.837 65.639-65.639v-262.559c0-7.746 5.381-13.128 13.128-13.128h131.28c7.746 0 13.128 5.381 13.128 13.128v262.559c0 35.802 29.837 65.639 65.639 65.639h262.559c35.802 0 65.639-29.837 65.639-65.639v-501.222c0-60.3-27.713-117.338-75.076-154.664l-373.172-293.995c-6.644-5.286-15.157-8.48-24.416-8.48-0.431 0-0.86 0.007-1.287 0.021l0.063-0.001zM512 76.418l348.815 274.816c28.465 22.433 45.024 56.552 45.024 92.768v488.093h-236.303v-249.432c0-50.278-41.616-91.896-91.896-91.896h-131.28c-50.278 0-91.896 41.616-91.896 91.896v249.432h-236.303v-488.093c0-36.216 16.561-70.335 45.024-92.768l348.815-274.816z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-home\"],\"defaultCode\":59690,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":52,\"id\":6,\"name\":\"home\",\"prevSize\":32,\"code\":59690},\"setIdx\":0,\"setId\":2,\"iconIdx\":7},{\"icon\":{\"paths\":[\"M56.89 113.777h910.222c31.419 0 56.889 25.469 56.889 56.889s-25.469 56.889-56.889 56.889v0h-910.222c-31.419 0-56.889-25.469-56.889-56.889s25.469-56.889 56.889-56.889v0zM56.89 455.111h910.222c31.419 0 56.889 25.469 56.889 56.889s-25.469 56.889-56.889 56.889v0h-910.222c-31.419 0-56.889-25.469-56.889-56.889s25.469-56.889 56.889-56.889v0zM56.89 796.446h910.222c31.419 0 56.889 25.469 56.889 56.889s-25.469 56.889-56.889 56.889v0h-910.222c-31.419 0-56.889-25.469-56.889-56.889s25.469-56.889 56.889-56.889v0z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"icon-menu\"],\"defaultCode\":59680,\"grid\":128,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":53,\"id\":7,\"name\":\"menu\",\"prevSize\":32,\"code\":59680},\"setIdx\":0,\"setId\":2,\"iconIdx\":8},{\"icon\":{\"paths\":[\"M69.048 478.058l388.688-388.686c18.746-18.746 49.138-18.746 67.882 0l45.334 45.334c18.714 18.714 18.75 49.044 0.080 67.802l-308.042 309.492 308.042 309.49c18.67 18.758 18.634 49.088-0.080 67.802l-45.334 45.334c-18.746 18.746-49.138 18.746-67.882 0l-388.686-388.686c-18.746-18.744-18.746-49.136-0.002-67.882z\"],\"width\":640,\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"icon-chevron-left\"],\"defaultCode\":59664,\"grid\":128,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":54,\"id\":8,\"name\":\"chevron-left\",\"prevSize\":32,\"code\":59664},\"setIdx\":0,\"setId\":2,\"iconIdx\":9},{\"icon\":{\"paths\":[\"M570.952 545.942l-388.688 388.686c-18.746 18.746-49.138 18.746-67.882 0l-45.334-45.334c-18.714-18.714-18.75-49.044-0.080-67.802l308.042-309.492-308.042-309.49c-18.67-18.758-18.634-49.088 0.080-67.802l45.334-45.334c18.746-18.746 49.138-18.746 67.882 0l388.686 388.686c18.746 18.744 18.746 49.136 0.002 67.882z\"],\"width\":640,\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"tags\":[\"icon-chevron-right\"],\"defaultCode\":59689,\"grid\":128,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":55,\"id\":9,\"prevSize\":32,\"name\":\"chevron-right\",\"code\":59689},\"setIdx\":0,\"setId\":2,\"iconIdx\":10},{\"icon\":{\"paths\":[\"M326.352-0.185c-11.523 0.347-21.847 5.211-29.312 12.875l-284.34 284.34c-7.716 7.718-12.487 18.379-12.487 30.155s4.771 22.436 12.487 30.155l284.33 284.33c7.774 8.087 18.685 13.112 30.768 13.112 23.557 0 42.654-19.096 42.654-42.654 0-12.083-5.025-22.993-13.099-30.754l-0.015-0.013-211.526-211.526h508.353c157.526 0 284.33 126.804 284.33 284.33s-126.804 284.33-284.33 284.33h-213.248c-0.18-0.003-0.391-0.004-0.604-0.004-23.557 0-42.654 19.096-42.654 42.654s19.096 42.654 42.654 42.654c0.212 0 0.424-0.001 0.636-0.004h213.216c203.631 0 369.63-166 369.63-369.63s-166-369.63-369.63-369.63h-508.353l211.526-211.526c7.958-7.757 12.895-18.581 12.895-30.557 0-23.557-19.096-42.654-42.654-42.654-0.432 0-0.862 0.007-1.291 0.019l0.063-0.001z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-back-2\"],\"defaultCode\":59676,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":56,\"id\":10,\"name\":\"back-2\",\"prevSize\":32,\"code\":59676},\"setIdx\":0,\"setId\":2,\"iconIdx\":11},{\"icon\":{\"paths\":[\"M842.375 148.875c-8.79 0.214-16.67 3.94-22.323 9.823l-0.010 0.011-308.042 308.042-308.042-308.042c-5.822-5.991-13.956-9.708-22.958-9.708l-0-0c-17.672 0.004-31.995 14.331-31.995 32.003 0 9 3.715 17.133 9.696 22.948l0.007 0.007 308.042 308.042-308.042 308.042c-6.068 5.833-9.838 14.019-9.838 23.085 0 17.675 14.328 32.003 32.003 32.003 9.066 0 17.252-3.77 23.075-9.828l0.010-0.011 308.042-308.042 308.042 308.042c5.833 6.068 14.019 9.838 23.085 9.838 17.675 0 32.003-14.328 32.003-32.003 0-9.066-3.77-17.252-9.828-23.075l-0.011-0.010-308.042-308.042 308.042-308.042c6.071-5.834 9.842-14.021 9.842-23.089 0-17.675-14.328-32.003-32.003-32.003-0.266 0-0.531 0.003-0.795 0.010l0.039-0.001z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-remove\"],\"defaultCode\":59688,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":57,\"id\":11,\"name\":\"remove\",\"prevSize\":32,\"code\":59688},\"setIdx\":0,\"setId\":2,\"iconIdx\":12},{\"icon\":{\"paths\":[\"M735.375 63.708c-8.646 0.26-16.392 3.91-21.993 9.66l-0.007 0.007-416 416c-5.789 5.791-9.369 13.79-9.369 22.625s3.58 16.834 9.369 22.625l416 416c5.833 6.068 14.019 9.838 23.085 9.838 17.675 0 32.003-14.328 32.003-32.003 0-9.066-3.77-17.252-9.828-23.075l-0.011-0.010-393.375-393.375 393.375-393.375c5.971-5.82 9.675-13.941 9.675-22.927 0-17.675-14.328-32.003-32.003-32.003-0.324 0-0.647 0.005-0.969 0.014l0.047-0.001z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-chevron-left-2\"],\"defaultCode\":59663,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":58,\"id\":12,\"name\":\"chevron-left-2\",\"prevSize\":32,\"code\":59663},\"setIdx\":0,\"setId\":2,\"iconIdx\":13},{\"icon\":{\"paths\":[\"M394.333 63.667c-17.672 0.004-31.995 14.331-31.995 32.003 0 9 3.715 17.133 9.696 22.948l0.007 0.007 393.375 393.375-393.375 393.375c-6.068 5.833-9.838 14.019-9.838 23.085 0 17.675 14.328 32.003 32.003 32.003 9.066 0 17.252-3.77 23.075-9.828l0.010-0.011 416-416c5.789-5.791 9.369-13.79 9.369-22.625s-3.58-16.834-9.369-22.625l-416-416c-5.822-5.991-13.956-9.708-22.958-9.708l-0-0z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-chevron-right-2\"],\"defaultCode\":59677,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":59,\"id\":13,\"name\":\"chevron-right-2\",\"prevSize\":32,\"code\":59677},\"setIdx\":0,\"setId\":2,\"iconIdx\":14},{\"icon\":{\"paths\":[\"M693.333 106.667c-52.64 0-96 43.36-96 96v10.667h-480c-0.135-0.002-0.293-0.003-0.453-0.003-17.675 0-32.003 14.328-32.003 32.003s14.328 32.003 32.003 32.003c0.159 0 0.318-0.001 0.477-0.003l-0.024 0h480v10.667c0 52.64 43.36 96 96 96s96-43.36 96-96v-10.667h117.333c0.135 0.002 0.293 0.003 0.453 0.003 17.675 0 32.003-14.328 32.003-32.003s-14.328-32.003-32.003-32.003c-0.159 0-0.318 0.001-0.477 0.003l0.024-0h-117.333v-10.667c0-52.64-43.36-96-96-96zM693.333 170.667c18.059 0 32 13.941 32 32v37.417c-0.269 1.559-0.423 3.356-0.423 5.187s0.154 3.628 0.45 5.376l-0.026-0.188v37.542c0 18.059-13.941 32-32 32s-32-13.941-32-32v-37.417c0.269-1.559 0.423-3.356 0.423-5.187s-0.154-3.628-0.45-5.376l0.026 0.188v-37.542c0-18.059 13.941-32 32-32zM330.667 373.333c-52.64 0-96 43.36-96 96v10.667h-117.333c-0.135-0.002-0.293-0.003-0.453-0.003-17.675 0-32.003 14.328-32.003 32.003s14.328 32.003 32.003 32.003c0.159 0 0.318-0.001 0.477-0.003l-0.024 0h117.333v10.667c0 52.64 43.36 96 96 96s96-43.36 96-96v-10.667h480c0.135 0.002 0.293 0.003 0.453 0.003 17.675 0 32.003-14.328 32.003-32.003s-14.328-32.003-32.003-32.003c-0.159 0-0.318 0.001-0.477 0.003l0.024-0h-480v-10.667c0-52.64-43.36-96-96-96zM330.667 437.333c18.059 0 32 13.941 32 32v37.417c-0.269 1.559-0.423 3.356-0.423 5.187s0.154 3.628 0.45 5.376l-0.026-0.188v37.542c0 18.059-13.941 32-32 32s-32-13.941-32-32v-37.417c0.269-1.559 0.423-3.356 0.423-5.187s-0.154-3.628-0.45-5.376l0.026 0.188v-37.542c0-18.059 13.941-32 32-32zM650.667 640c-52.64 0-96 43.36-96 96v10.667h-437.333c-0.135-0.002-0.293-0.003-0.453-0.003-17.675 0-32.003 14.328-32.003 32.003s14.328 32.003 32.003 32.003c0.159 0 0.318-0.001 0.477-0.003l-0.024 0h437.333v10.667c0 52.64 43.36 96 96 96s96-43.36 96-96v-10.667h160c0.135 0.002 0.293 0.003 0.453 0.003 17.675 0 32.003-14.328 32.003-32.003s-14.328-32.003-32.003-32.003c-0.159 0-0.318 0.001-0.477 0.003l0.024-0h-160v-10.667c0-52.64-43.36-96-96-96zM650.667 704c18.059 0 32 13.941 32 32v37.417c-0.269 1.559-0.423 3.356-0.423 5.187s0.154 3.628 0.45 5.376l-0.026-0.188v37.542c0 18.059-13.941 32-32 32s-32-13.941-32-32v-37.417c0.269-1.559 0.423-3.356 0.423-5.187s-0.154-3.628-0.45-5.376l0.026 0.188v-37.542c0-18.059 13.941-32 32-32z\"],\"attrs\":[],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-slider\"],\"defaultCode\":59687,\"colorPermutations\":{\"25525525519191911\":[]}},\"attrs\":[],\"properties\":{\"order\":60,\"id\":14,\"name\":\"slider\",\"prevSize\":32,\"code\":59687},\"setIdx\":0,\"setId\":2,\"iconIdx\":15},{\"icon\":{\"paths\":[\"M953.53 703.521c-18.424 5.13-37.468 7.696-56.594 7.624-46.633 0-82.019-16.794-106.154-50.382s-36.204-83.451-36.206-149.587c-0.383-28.361 2.926-56.655 9.836-84.167 6.56-24.605 15.931-45.434 28.121-62.49 11.454-16.414 26.71-29.815 44.465-39.055 18.138-9.144 38.216-13.765 58.522-13.476 19.737-0.188 39.394 2.51 58.352 8.006 18.15 5.494 35.518 13.306 51.67 23.24v-68.358c-16.76-8.293-34.465-14.52-52.726-18.551-19.525-4.167-39.443-6.193-59.405-6.054-31.407 0-59.761 6.249-85.066 18.746-25.375 12.553-47.509 30.799-64.671 53.31-17.811 23.043-31.52 51.098-41.122 84.167s-14.409 70.041-14.412 110.917c0 84.359 17.4 148.087 52.197 191.181s84.652 64.639 149.568 64.637c39.94 0.084 79.414-8.581 115.646-25.386v-65.608c-16.474 9.050-33.93 16.191-52.022 21.288z\",\"M81.72 707.623v-451.111h-63.624v510.485h264.342v-59.374h-0.007z\",\"M565.077 549.641c-5.179-7.413-11.673-13.745-19.109-18.731l-8.622-8.648c13.226-3.886 25.788-9.788 37.223-17.525 11.983-8.146 22.452-18.326 30.931-30.073 8.395-11.742 14.752-24.812 18.806-38.666 4.284-14.652 6.415-29.847 6.328-45.109 0.223-18.642-2.685-37.195-8.607-54.876-5.47-16.104-14.752-30.645-27.065-42.375-13.575-12.459-29.713-21.792-47.278-27.339-19.217-6.506-42.18-9.762-68.895-9.764h-123.032v83.59l-259.699-260.476-43.525 43.405 303.219 304.133v339.812h62.559v-227.309h29.527c7.657-0.091 15.284 0.829 22.718 2.644l51.401 51.555c1.51 3.241 2.957 6.508 4.28 9.834l68.542 163.277h70.662l-22.372-51.635 228.306 228.994 43.525-43.407-348.903-349.953c-0.31-0.45-0.608-0.909-0.921-1.359zM418.312 315.103h59.063c27.885 0 49.502 6.77 64.853 20.309s23.026 33.977 23.023 61.319c0.243 12.767-2.086 25.451-6.854 37.297v0.005c-4.316 10.446-10.922 19.789-19.333 27.339-8.773 7.677-19.075 13.402-30.23 16.794-4.076 1.238-8.209 2.255-12.372 3.090l-78.151-78.384v-87.768z\"],\"attrs\":[{},{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-lyric-off\"],\"colorPermutations\":{\"25525525519191911\":[{},{},{}]}},\"attrs\":[{},{},{}],\"properties\":{\"order\":107,\"id\":56,\"name\":\"lyric-off\",\"prevSize\":32,\"code\":59697},\"setIdx\":0,\"setId\":2,\"iconIdx\":16},{\"icon\":{\"paths\":[\"M280.698 708.793h-201.648v-453.218h-63.922v512.869h265.574v-59.653zM592.39 599.711c-8.373-19.027-17.636-35.333-28.339-50.559l0.615 0.921c-8.197-11.627-19.414-20.62-32.53-25.916l-0.492-0.174c16.178-4.134 30.373-10.673 43.015-19.32l-0.461 0.296c12.16-8.339 22.452-18.356 30.83-29.854l0.249-0.36c8.039-11.114 14.485-24.031 18.657-37.93l0.234-0.915c4.037-13.298 6.36-28.577 6.36-44.401 0-0.324-0.001-0.646-0.002-0.966v0.051c0.006-0.597 0.012-1.306 0.012-2.010 0-19.007-3.172-37.271-9.009-54.294l0.352 1.177c-5.802-16.78-15.127-31.060-27.152-42.532l-0.041-0.038c-13.172-12.122-29.052-21.535-46.601-27.218l-0.895-0.251q-28.96-9.807-69.219-9.812h-123.607v512.845h62.852v-228.369h29.664c0.325-0.003 0.709-0.005 1.089-0.005 8.468 0 16.668 1.149 24.452 3.297l-0.644-0.154c7.851 2.265 14.683 5.83 20.611 10.495l-0.128-0.098c6.832 5.546 12.631 12.020 17.287 19.296l0.193 0.323c5.745 8.903 11.078 19.163 15.449 29.915l0.453 1.258 68.861 164.037h70.992zM557.955 433.93c-4.55 10.838-11.136 20.005-19.353 27.408l-0.068 0.061c-8.479 7.451-18.622 13.234-29.784 16.715l-0.586 0.16c-11.456 3.613-24.626 5.693-38.286 5.693-0.63 0-1.263-0.004-1.894-0.013l0.097 0.001h-50.866v-169.51h59.34q42.023 0 65.156 20.402t23.13 61.606c0.011 0.544 0.019 1.182 0.019 1.824 0 12.844-2.535 25.096-7.135 36.286l0.23-0.635zM1007.199 683.285c-14.963 8.404-32.307 15.691-50.508 20.952l-1.754 0.433c-16.803 4.863-36.101 7.658-56.055 7.658-0.283 0-0.562 0-0.846-0.001h0.042q-70.279 0-106.65-50.616t-36.376-150.287c-0.020-1.313-0.031-2.861-0.031-4.415 0-28.467 3.615-56.085 10.411-82.427l-0.498 2.285q9.883-37.078 28.252-62.783c11.565-16.427 26.556-29.68 43.998-38.913l0.679-0.327c16.587-8.542 36.203-13.552 56.985-13.552 0.636 0 1.274 0.004 1.907 0.013l-0.098-0.001c0.57-0.005 1.248-0.007 1.926-0.007 20.2 0 39.718 2.943 58.141 8.42l-1.442-0.366c19.78 6.074 36.984 14.010 52.848 23.894l-0.94-0.547v-68.677c-15.213-7.722-32.895-14.138-51.415-18.338l-1.556-0.296c-17.353-3.872-37.283-6.088-57.733-6.088-0.686 0-1.369 0.002-2.055 0.006h0.103q-47.327 0-85.465 18.836c-26.035 13.060-47.76 31.136-64.652 53.123l-0.323 0.436q-26.84 34.723-41.316 84.558t-14.481 111.434q0 127.133 52.442 192.073t150.267 64.939c0.168 0 0.366 0 0.567 0 41.958 0 81.716-9.402 117.289-26.216l-1.674 0.711v-65.913z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-lyric-on\"],\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":101,\"id\":53,\"name\":\"lyric-on\",\"prevSize\":32,\"code\":59698},\"setIdx\":0,\"setId\":2,\"iconIdx\":17},{\"icon\":{\"paths\":[\"M212.62 135.003c-79.211 0-144.145 64.933-144.145 144.145v376.997c0 79.211 64.933 144.145 144.145 144.145h33.264v121.969c0 43.599 53.828 70.497 88.705 44.353l221.761-166.322h255.027c79.211 0 144.145-64.933 144.145-144.145v-376.997c0-79.211-64.933-144.145-144.145-144.145h-598.758zM212.62 201.533h598.758c43.245 0 77.617 34.371 77.617 77.617v376.997c0 43.245-34.371 77.617-77.617 77.617h-266.114c-7.531 0.003-14.476 2.51-20.050 6.731l0.084-0.060-212.884 159.651v-133.058c-0.002-18.37-14.894-33.262-33.264-33.264h-66.528c-43.245 0-77.617-34.371-77.617-77.617v-376.997c0-43.245 34.371-77.617 77.617-77.617zM323.503 356.723c-0.141-0.002-0.305-0.003-0.471-0.003-18.373 0-33.267 14.894-33.267 33.267s14.894 33.267 33.267 33.267c0.165 0 0.33-0.001 0.495-0.003h376.971c0.141 0.002 0.305 0.003 0.471 0.003 18.373 0 33.267-14.894 33.267-33.267s-14.894-33.267-33.267-33.267c-0.165 0-0.33 0.001-0.495 0.003h-376.971zM323.503 511.956c-0.141-0.002-0.305-0.003-0.471-0.003-18.373 0-33.267 14.894-33.267 33.267s14.894 33.267 33.267 33.267c0.165 0 0.33-0.001 0.495-0.003h288.265c0.141 0.002 0.305 0.003 0.471 0.003 18.373 0 33.267-14.894 33.267-33.267s-14.894-33.267-33.267-33.267c-0.165 0-0.33 0.001-0.495 0.003h-288.265z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-comment\"],\"defaultCode\":59672,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":61,\"id\":15,\"name\":\"comment\",\"prevSize\":32,\"code\":59672},\"setIdx\":0,\"setId\":2,\"iconIdx\":18},{\"icon\":{\"paths\":[\"M245.333 128c-64.704 0-117.333 52.629-117.333 117.333v533.333c0 64.704 52.629 117.333 117.333 117.333h533.333c64.704 0 117.333-52.629 117.333-117.333v-533.333c0-64.704-52.629-117.333-117.333-117.333h-533.333zM245.333 192h533.333c29.397 0 53.333 23.936 53.333 53.333v533.333c0 29.397-23.936 53.333-53.333 53.333h-533.333c-29.397 0-53.333-23.936-53.333-53.333v-533.333c0-29.397 23.936-53.333 53.333-53.333zM387.583 278.917l149.958 233.083-149.958 233.083h138.167l151.667-233.083-151.667-233.083h-138.167z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-playback-rate\"],\"defaultCode\":59674,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":62,\"id\":16,\"name\":\"playback-rate\",\"prevSize\":32,\"code\":59674},\"setIdx\":0,\"setId\":2,\"iconIdx\":19},{\"icon\":{\"paths\":[\"M559.292 130.167c-2.813-0.004-5.639 0.202-8.458 0.583-11.277 1.526-22.4 6.179-31.583 14.333-0 0.006-0 0.014-0 0.021s0 0.015 0 0.022l-0-0.001-196.75 174.875h-141.167c-52.64 0-96 43.36-96 96v192c0 52.64 43.36 96 96 96h141.167l196.75 174.875c18.364 16.326 44.408 18.728 64.125 9.875s35.292-29.926 35.292-54.5v-644.5c0-24.565-15.531-45.644-35.25-54.5-7.394-3.321-15.686-5.070-24.125-5.083zM554.667 199.25v625.5l-198.75-176.667c-5.622-5.015-13.078-8.080-21.249-8.083l-153.334-0c-18.059 0-32-13.941-32-32v-192c0-18.059 13.941-32 32-32h153.333c8.172-0.003 15.628-3.068 21.282-8.111l-0.032 0.028 198.75-176.667zM927.708 383.542c-8.79 0.214-16.67 3.94-22.323 9.823l-0.010 0.011-73.375 73.375-73.375-73.375c-5.822-5.991-13.956-9.708-22.958-9.708l-0-0c-17.672 0.004-31.995 14.331-31.995 32.003 0 9 3.715 17.133 9.696 22.948l0.007 0.007 73.375 73.375-73.375 73.375c-6.068 5.833-9.838 14.019-9.838 23.085 0 17.675 14.328 32.003 32.003 32.003 9.066 0 17.252-3.77 23.075-9.828l0.010-0.011 73.375-73.375 73.375 73.375c5.833 6.068 14.019 9.838 23.085 9.838 17.675 0 32.003-14.328 32.003-32.003 0-9.066-3.77-17.252-9.828-23.075l-0.011-0.010-73.375-73.375 73.375-73.375c6.071-5.833 9.842-14.021 9.842-23.089 0-17.675-14.328-32.003-32.003-32.003-0.266 0-0.531 0.003-0.795 0.010l0.039-0.001z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-volume-mute\"],\"defaultCode\":59682,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":63,\"id\":17,\"name\":\"volume-mute\",\"prevSize\":32,\"code\":59682},\"setIdx\":0,\"setId\":2,\"iconIdx\":20},{\"icon\":{\"paths\":[\"M687.292 130.167c-2.813-0.004-5.639 0.202-8.458 0.583-11.277 1.526-22.4 6.179-31.583 14.333-0 0.006-0 0.014-0 0.021s0 0.015 0 0.022l-0-0.001-196.75 174.875h-141.167c-52.64 0-96 43.36-96 96v192c0 52.64 43.36 96 96 96h141.167l196.75 174.875c18.364 16.326 44.408 18.728 64.125 9.875s35.292-29.926 35.292-54.5v-644.5c0-24.565-15.531-45.644-35.25-54.5-7.394-3.321-15.686-5.070-24.125-5.083zM682.667 199.25v625.5l-198.75-176.667c-5.622-5.015-13.078-8.080-21.249-8.083l-153.334-0c-18.059 0-32-13.941-32-32v-192c0-18.059 13.941-32 32-32h153.333c8.172-0.003 15.628-3.068 21.282-8.111l-0.032 0.028 198.75-176.667z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-volume-off\"],\"defaultCode\":59683,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":64,\"id\":18,\"name\":\"volume-off\",\"prevSize\":32,\"code\":59683},\"setIdx\":0,\"setId\":2,\"iconIdx\":21},{\"icon\":{\"paths\":[\"M644.625 130.167c-2.813-0.004-5.639 0.202-8.458 0.583-11.277 1.526-22.4 6.179-31.583 14.333-0 0.006-0 0.014-0 0.021s0 0.015 0 0.022l-0-0.001-196.75 174.875h-141.167c-52.64 0-96 43.36-96 96v192c0 52.64 43.36 96 96 96h141.167l196.75 174.875c18.364 16.326 44.408 18.728 64.125 9.875s35.292-29.926 35.292-54.5v-644.5c0-24.565-15.531-45.644-35.25-54.5-7.394-3.321-15.686-5.070-24.125-5.083zM640 199.25v625.5l-198.75-176.667c-5.622-5.015-13.078-8.080-21.249-8.083l-153.334-0c-18.059 0-32-13.941-32-32v-192c0-18.059 13.941-32 32-32h153.333c8.172-0.003 15.628-3.068 21.282-8.111l-0.032 0.028 198.75-176.667zM793 362.583c-17.078 0.752-30.639 14.779-30.639 31.974 0 4.991 1.142 9.715 3.18 13.925l-0.083-0.191c32.353 70.794 32.308 136.984-0.167 207.792-1.89 3.961-2.993 8.609-2.993 13.514 0 17.675 14.328 32.003 32.003 32.003 12.893 0 24.006-7.625 29.075-18.611l0.082-0.198c39.205-85.481 39.279-175.631 0.208-261.125-5.085-11.349-16.282-19.113-29.292-19.113-0.483 0-0.964 0.011-1.442 0.032l0.068-0.002z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-volume-low\"],\"defaultCode\":59684,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":65,\"id\":19,\"name\":\"volume-low\",\"prevSize\":32,\"code\":59684},\"setIdx\":0,\"setId\":2,\"iconIdx\":22},{\"icon\":{\"paths\":[\"M594.083 128.667c-4.952-0.641-10.063-0.606-15.125 0.125-10.124 1.461-20.056 5.758-28.167 13.125-0.159 0.139-0.281 0.248-0.402 0.358l0.027-0.024-180.5 169.083c-5.945 5.571-13.738 8.667-21.875 8.667h-166.708c-52.64 0-96 43.36-96 96v192c0 52.64 43.36 96 96 96h166.708c8.137 0 15.93 3.096 21.875 8.667l180.5 169.083c0.094 0.085 0.216 0.194 0.339 0.302l0.036 0.031c16.222 14.734 39.769 17.16 57.458 9.333s31.75-26.876 31.75-48.792v-661.25c0-21.916-14.061-40.965-31.75-48.792-4.422-1.957-9.215-3.275-14.167-3.917zM576 205.958v612.083l-162.333-152.083c-17.778-16.659-41.25-25.958-65.625-25.958h-166.708c-18.059 0-32-13.941-32-32v-192c0-18.059 13.941-32 32-32h166.708c24.375 0 47.847-9.3 65.625-25.958l162.333-152.083zM837.292 246.875c-0.060-0-0.131-0.001-0.202-0.001-17.675 0-32.003 14.328-32.003 32.003 0 6.667 2.039 12.858 5.527 17.984l-0.071-0.111c40.473 61.983 64.125 135.68 64.125 215.25s-23.652 153.267-64.125 215.25c-3.584 5.097-5.727 11.433-5.727 18.27 0 17.675 14.328 32.003 32.003 32.003 11.502 0 21.587-6.068 27.228-15.177l0.079-0.137c46.951-71.905 74.542-157.917 74.542-250.208s-27.59-178.304-74.542-250.208c-5.722-8.959-15.583-14.834-26.821-14.917l-0.012-0zM746.917 346.833c-0.13-0.002-0.282-0.003-0.436-0.003-17.675 0-32.003 14.328-32.003 32.003 0 5.456 1.365 10.593 3.773 15.088l-0.084-0.172c18.144 35.641 28.5 75.635 28.5 118.25s-10.356 82.609-28.5 118.25c-2.504 4.458-3.979 9.784-3.979 15.455 0 17.675 14.328 32.003 32.003 32.003 12.741 0 23.744-7.446 28.893-18.224l0.083-0.193c22.474-44.146 35.5-94.248 35.5-147.292s-13.026-103.146-35.5-147.292c-5.268-10.538-15.902-17.689-28.225-17.875l-0.025-0z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-volume-medium\"],\"defaultCode\":59685,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":66,\"id\":20,\"name\":\"volume-medium\",\"prevSize\":32,\"code\":59685},\"setIdx\":0,\"setId\":2,\"iconIdx\":23},{\"icon\":{\"paths\":[\"M516.625 130.167c-2.813-0.004-5.639 0.202-8.458 0.583-11.277 1.526-22.4 6.179-31.583 14.333-0 0.006-0 0.014-0 0.021s0 0.015 0 0.022l-0-0.001-196.75 174.875h-141.167c-52.64 0-96 43.36-96 96v192c0 52.64 43.36 96 96 96h141.167l196.75 174.875c18.364 16.326 44.408 18.728 64.125 9.875s35.292-29.926 35.292-54.5v-644.5c0-24.565-15.531-45.644-35.25-54.5-7.394-3.321-15.686-5.070-24.125-5.083zM858 170.292c-17.11 0.713-30.712 14.756-30.712 31.977 0 6.070 1.69 11.746 4.625 16.582l-0.080-0.142c114.831 198.084 115.245 388.522 0.042 586.542-3.085 4.833-4.917 10.725-4.917 17.046 0 17.675 14.328 32.003 32.003 32.003 12.138 0 22.697-6.757 28.122-16.714l0.084-0.168c124.583-214.14 124.143-436.716 0.042-650.792-5.587-9.834-15.991-16.359-27.919-16.359-0.453 0-0.905 0.009-1.353 0.028l0.064-0.002zM512 199.25v625.5l-198.75-176.667c-5.622-5.015-13.078-8.080-21.249-8.083l-153.334-0c-18.059 0-32-13.941-32-32v-192c0-18.059 13.941-32 32-32h153.333c8.172-0.003 15.628-3.068 21.282-8.111l-0.032 0.028 198.75-176.667zM762.792 255.583c-0.039-0-0.085-0-0.131-0-17.675 0-32.003 14.328-32.003 32.003 0 5.571 1.423 10.809 3.926 15.372l-0.084-0.166c73.798 141.328 73.798 277.089 0 418.417-2.436 4.408-3.869 9.663-3.869 15.253 0 17.675 14.328 32.003 32.003 32.003 12.451 0 23.241-7.11 28.532-17.492l0.084-0.181c81.936-156.912 81.936-320.671 0-477.583-5.363-10.498-16.078-17.572-28.451-17.625l-0.007-0zM665 362.583c-17.078 0.752-30.639 14.779-30.639 31.974 0 4.991 1.142 9.715 3.18 13.925l-0.083-0.191c32.353 70.794 32.308 136.984-0.167 207.792-1.89 3.961-2.993 8.609-2.993 13.514 0 17.675 14.328 32.003 32.003 32.003 12.893 0 24.006-7.625 29.075-18.611l0.082-0.198c39.205-85.481 39.279-175.631 0.208-261.125-5.085-11.349-16.282-19.113-29.292-19.113-0.483 0-0.964 0.011-1.442 0.032l0.068-0.002z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-volume-higt\"],\"defaultCode\":59686,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":67,\"id\":21,\"name\":\"volume-higt\",\"prevSize\":32,\"code\":59686},\"setIdx\":0,\"setId\":2,\"iconIdx\":24},{\"icon\":{\"paths\":[\"M995.882 547.882c37.49-37.49 37.49-98.274 0-135.764l-320-320c-37.49-37.49-98.272-37.492-135.766 0l-512 512c-37.49 37.49-37.49 98.274 0 135.764l192 192c17.373 17.372 41.374 28.118 67.884 28.118l712-0c13.254 0 24-10.746 24-24v-80c0-13.254-10.746-24-24-24h-288.234l284.116-284.118zM390.628 422.628l274.746 274.746-134.628 134.626h-229.49l-160-160 249.372-249.372z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-eraser\"],\"defaultCode\":59671,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":68,\"id\":22,\"name\":\"eraser\",\"prevSize\":32,\"code\":59671},\"setIdx\":0,\"setId\":2,\"iconIdx\":25},{\"icon\":{\"paths\":[\"M512 141.505c-124.939 0-235.61 62.225-302.634 157.148-3.539 4.956-5.656 11.137-5.656 17.81 0 17.056 13.823 30.878 30.878 30.878 10.383 0 19.568-5.121 25.167-12.98l0.066-0.095c55.935-79.215 147.706-131.016 252.185-131.016 166.341 0 300.996 130.715 308.062 295.278l-49.532-49.532c-5.619-5.779-13.464-9.368-22.152-9.368v0c-17.051 0.004-30.866 13.823-30.866 30.876 0 8.685 3.587 16.53 9.355 22.141l92.631 92.631c5.585 5.583 13.307 9.041 21.831 9.041s16.24-3.452 21.831-9.041l7.154-7.154c2.263-1.729 4.223-3.683 5.897-5.874l0.056-0.072 79.519-79.519c5.857-5.628 9.492-13.526 9.492-22.273 0-17.055-13.823-30.876-30.876-30.876-8.746 0-16.648 3.637-22.262 9.481l-0.011 0.012-31.239 31.239c-16.193-189.427-175.348-338.737-368.888-338.737zM182.23 429.669c-8.362 0.125-15.906 3.557-21.386 9.047l-7.035 7.035c-2.334 1.766-4.344 3.777-6.053 6.036l-0.057 0.075-79.479 79.479c-5.857 5.628-9.492 13.526-9.492 22.273 0 17.055 13.823 30.876 30.876 30.876 8.746 0 16.648-3.637 22.262-9.481l0.011-0.012 31.239-31.239c16.193 189.427 175.348 338.737 368.888 338.737 124.939 0 235.61-62.225 302.634-157.148 3.539-4.956 5.656-11.137 5.656-17.81 0-17.056-13.823-30.878-30.878-30.878-10.383 0-19.568 5.121-25.167 12.98l-0.066 0.095c-55.935 79.215-147.706 131.016-252.185 131.016-166.341 0-300.996-130.715-308.062-295.278l49.532 49.532c5.628 5.857 13.526 9.492 22.273 9.492 17.055 0 30.876-13.823 30.876-30.876 0-8.746-3.637-16.648-9.481-22.262l-0.012-0.011-92.624-92.624c-5.586-5.588-13.312-9.050-21.835-9.050-0.154 0-0.3 0.001-0.455 0.003h0.022z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"available_updates\"],\"defaultCode\":59665,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":69,\"id\":23,\"name\":\"available_updates\",\"prevSize\":32,\"code\":59665},\"setIdx\":0,\"setId\":2,\"iconIdx\":26},{\"icon\":{\"paths\":[\"M512 92.955c-231.063 0-419.045 187.982-419.045 419.045s187.982 419.045 419.045 419.045c11.039 0 21.862-0.797 32.697-1.634-17.366-18.376-32.24-39.139-43.99-61.792-191.152-6.030-344.891-163.022-344.891-355.611 0-196.407 159.784-356.19 356.19-356.19 192.588 0 349.581 153.752 355.611 344.891 22.674 11.75 43.422 26.596 61.792 43.99 0.841-10.832 1.634-21.651 1.634-32.697 0-231.063-187.982-419.045-419.045-419.045zM521.982 260.128c-17.144 0.276-30.937 14.239-30.937 31.432 0 0.156 0.001 0.314 0.005 0.469v-0.027 219.998h-136.192c-0.137-0.002-0.291-0.003-0.447-0.003-17.354 0-31.432 14.070-31.432 31.432s14.070 31.432 31.432 31.432c0.154 0 0.307-0.001 0.467-0.003h167.597c17.352-0.002 31.432-14.070 31.432-31.432v0-251.432c0.002-0.137 0.003-0.298 0.003-0.454 0-17.354-14.070-31.432-31.432-31.432-0.175 0-0.348 0.001-0.522 0.005h0.027zM742.477 512c-127.289 0-230.477 103.192-230.477 230.477s103.192 230.477 230.477 230.477c127.289 0 230.477-103.192 230.477-230.477s-103.192-230.477-230.477-230.477zM742.477 574.855c0.299 0 0.507 0.164 0.78 0.164 2.368 0.103 4.736 0.412 7.001 1.306l78.402 31.382c24.007 9.615 39.527 32.498 39.527 58.356v10.477c0 6.957-3.467 13.449-9.212 17.352-3.52 2.392-7.617 3.602-11.742 3.602-2.614 0-5.243-0.485-7.781-1.512l-76.037-30.403v171.184c0 3.416-0.432 5.887-1.107 7.893-5.362 25.145-35.518 44.479-72.232 44.479-40.506 0-73.338-23.449-73.338-52.386s32.832-52.386 73.338-52.386c11.299 0 21.87 1.966 31.432 5.241v-193.804c0-2.557 0.591-4.977 1.432-7.287 0.192-0.479 0.358-0.928 0.577-1.396 0.983-2.117 2.253-4.073 3.882-5.768 0.299-0.31 0.632-0.57 0.943-0.854 0.777-0.712 1.496-1.457 2.376-2.049 0.88-0.608 1.858-0.966 2.829-1.432 0.414-0.192 0.768-0.448 1.182-0.616 2.265-0.906 4.631-1.304 7.039-1.396 0.247-0.026 0.445-0.164 0.69-0.164z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"music_time\"],\"defaultCode\":59648,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":70,\"id\":24,\"name\":\"music_time\",\"prevSize\":32,\"code\":59648},\"setIdx\":0,\"setId\":2,\"iconIdx\":27},{\"icon\":{\"paths\":[\"M769.011 64.070c-18.539 0.005-33.566 15.036-33.566 33.572 0 9.438 3.894 17.972 10.168 24.072l76.976 76.976h-612.718c-67.808 0-123.084 55.277-123.084 123.084v361.214l67.136-67.136v-294.078c0-30.884 25.061-55.948 55.948-55.948h612.718l-76.972 76.972c-6.363 6.121-10.323 14.703-10.323 24.22 0 18.542 15.034 33.572 33.572 33.572 9.509 0 18.099-3.952 24.208-10.311l0.012-0.013 134.273-134.273c6.076-6.076 9.828-14.469 9.828-23.733s-3.756-17.662-9.828-23.733l-134.273-134.273c-6.108-6.286-14.644-10.183-24.084-10.183v0zM937.202 341.006l-67.136 67.136v294.078c0 30.884-25.061 55.948-55.948 55.948h-612.718l76.972-76.972c6.263-6.105 10.151-14.626 10.151-24.052 0-18.542-15.034-33.572-33.572-33.572-0.34 0-0.68 0.006-1.018 0.017l0.048-0.001c-9.070 0.272-17.196 4.104-23.071 10.133l-134.281 134.281c-6.076 6.076-9.828 14.469-9.828 23.733s3.756 17.662 9.828 23.733l134.273 134.273c6.121 6.363 14.703 10.323 24.22 10.323 18.542 0 33.572-15.034 33.572-33.572 0-9.509-3.952-18.099-10.311-24.208l-0.013-0.012-76.972-76.972h612.718c67.808 0 123.084-55.277 123.084-123.084v-361.214z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-list-loop\"],\"defaultCode\":59649,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":71,\"id\":25,\"name\":\"list-loop\",\"prevSize\":32,\"code\":59649},\"setIdx\":0,\"setId\":2,\"iconIdx\":28},{\"icon\":{\"paths\":[\"M812.371 117.66c-17.181 0.004-31.101 13.928-31.101 31.11 0 8.751 3.613 16.654 9.426 22.308l71.333 71.333h-127.421c-45.011 0-87.849 19.533-117.378 53.466l-340.347 391.422c-17.747 20.41-43.393 32.077-70.433 32.077h-119.567c-0.129-0.002-0.283-0.003-0.44-0.003-17.181 0-31.11 13.927-31.11 31.11s13.927 31.11 31.11 31.11c0.155 0 0.311-0.001 0.466-0.003h119.541c44.998 0 87.844-19.498 117.378-53.466l340.347-391.422c17.754-20.399 43.406-32.077 70.433-32.077h127.421l-71.325 71.325c-5.896 5.67-9.563 13.63-9.563 22.442 0 17.181 13.927 31.11 31.11 31.11 8.811 0 16.771-3.667 22.431-9.552l0.011-0.012 124.425-124.425c5.628-5.63 9.106-13.404 9.106-21.994s-3.478-16.364-9.106-21.994l-124.425-124.425c-5.658-5.825-13.567-9.439-22.315-9.439v0zM86.876 242.408c-17.213 0-31.106 13.893-31.106 31.106s13.893 31.106 31.106 31.106h119.646c26.958 0 52.685 11.628 70.315 32.161l111.143 127.952 41.274-47.509-105.349-121.305c-29.654-34.009-72.379-53.507-117.378-53.507h-119.646zM553.071 559.266l-41.274 47.509 105.349 121.305c29.654 34.009 72.379 53.507 117.378 53.507h127.507l-71.325 71.325c-5.896 5.67-9.563 13.63-9.563 22.442 0 17.181 13.927 31.11 31.11 31.11 8.811 0 16.771-3.667 22.431-9.552l0.011-0.012 124.425-124.425c5.628-5.63 9.106-13.404 9.106-21.994s-3.478-16.364-9.106-21.994l-124.425-124.425c-5.658-5.825-13.567-9.439-22.315-9.439v0c-17.181 0.004-31.101 13.928-31.101 31.11 0 8.751 3.613 16.654 9.426 22.308l71.333 71.333h-127.507c-26.958 0-52.685-11.628-70.315-32.161l-111.143-127.952z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-list-random\"],\"defaultCode\":59650,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":72,\"id\":26,\"name\":\"list-random\",\"prevSize\":32,\"code\":59650},\"setIdx\":0,\"setId\":2,\"iconIdx\":29},{\"icon\":{\"paths\":[\"M713.492 83.628c-19.45 0.31-35.103 16.154-35.103 35.656 0 0.182 0.001 0.358 0.005 0.536v-0.028 722.111l-105.523-105.523c-6.501-6.76-15.62-10.964-25.719-10.964-19.695 0-35.662 15.967-35.662 35.662 0 10.099 4.204 19.223 10.951 25.713l0.014 0.013 166.393 166.393c6.455 6.453 15.368 10.437 25.212 10.437s18.757-3.988 25.212-10.437l166.393-166.393c6.76-6.501 10.964-15.62 10.964-25.719 0-19.695-15.967-35.662-35.662-35.662-10.099 0-19.223 4.204-25.713 10.951l-0.013 0.014-105.523 105.523v-722.111c0.003-0.149 0.004-0.332 0.004-0.514 0-19.695-15.967-35.662-35.662-35.662-0.193 0-0.392 0.001-0.583 0.005h0.028zM283.215 131.815c-2.79 0.238-5.592 0.761-8.311 1.675l-71.308 23.771c-18.681 6.229-28.793 26.441-22.565 45.128s26.487 28.86 45.128 22.565l24.375-8.168v212.028c0 19.707 15.95 35.658 35.658 35.658s35.658-15.95 35.658-35.658v-261.472c0-11.46-5.518-22.22-14.813-28.922-6.989-5.031-15.439-7.295-23.817-6.592zM274.301 583.308c-63.132 0-109.64 39.984-118.48 101.903-2.778 19.495 10.733 37.562 30.223 40.343 19.467 2.804 37.562-10.705 40.343-30.223 2.665-18.563 13.134-40.719 47.913-40.719 26.216 0 47.544 21.319 47.544 47.544s-21.319 47.544-47.544 47.544c-57.236 0-118.85 40.907-118.85 130.735 0 19.707 15.977 35.658 35.658 35.658h166.393c19.707 0 35.658-15.95 35.658-35.658s-15.95-35.658-35.658-35.658h-123.397c11.247-21.559 32.216-23.771 40.204-23.771 65.533 0 118.85-53.32 118.85-118.85s-53.32-118.85-118.85-118.85z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-list-order\"],\"defaultCode\":59651,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":73,\"id\":27,\"name\":\"list-order\",\"prevSize\":32,\"code\":59651},\"setIdx\":0,\"setId\":2,\"iconIdx\":30},{\"icon\":{\"paths\":[\"M782.78 40.073c-19.532 0.005-35.363 15.841-35.363 35.37 0 9.945 4.104 18.935 10.714 25.362l81.101 81.101h-645.541c-71.441 0-129.678 58.238-129.678 129.678v380.564l70.733-70.733v-309.831c0-32.538 26.405-58.945 58.945-58.945h645.541l-81.095 81.095c-6.705 6.448-10.875 15.492-10.875 25.516 0 19.535 15.838 35.37 35.37 35.37 10.019 0 19.068-4.165 25.504-10.863l0.012-0.013 141.467-141.467c6.4-6.401 10.354-15.243 10.354-25.005s-3.957-18.607-10.354-25.005l-141.467-141.467c-6.435-6.622-15.427-10.729-25.374-10.729v0zM959.981 331.847l-70.733 70.733v309.831c0 32.538-26.405 58.945-58.945 58.945h-645.541l81.095-81.095c6.599-6.432 10.694-15.409 10.694-25.34 0-19.535-15.838-35.37-35.37-35.37-0.358 0-0.716 0.006-1.072 0.017l0.051-0.001c-9.556 0.287-18.117 4.323-24.307 10.676l-141.475 141.475c-6.4 6.401-10.354 15.243-10.354 25.005s3.957 18.607 10.354 25.005l141.467 141.467c6.448 6.705 15.492 10.875 25.516 10.875 19.535 0 35.37-15.838 35.37-35.37 0-10.019-4.165-19.068-10.863-25.504l-0.013-0.012-81.095-81.095h645.541c71.441 0 129.678-58.238 129.678-129.678v-380.564zM527.334 347.002c-2.311 0.196-4.617 0.675-6.863 1.429l-70.733 23.576c-15.442 5.162-23.792 21.859-18.651 37.302 5.162 15.422 21.834 23.746 37.302 18.651l31.913-10.639v230.251c0 16.268 13.203 29.471 29.471 29.471s29.471-13.202 29.471-29.52v-271.145c0-9.477-4.565-18.361-12.247-23.901-5.763-4.158-12.735-6.061-19.665-5.479z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-single-loop\"],\"defaultCode\":59652,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":74,\"id\":28,\"name\":\"single-loop\",\"prevSize\":32,\"code\":59652},\"setIdx\":0,\"setId\":2,\"iconIdx\":31},{\"icon\":{\"paths\":[\"M512 72c-242.617 0-440 197.383-440 440s197.383 440 440 440c242.617 0 440-197.383 440-440s-197.383-440-440-440zM512 138c91.519 0 174.114 33.962 239.037 88.303l-524.727 524.689c-54.329-64.918-88.303-147.483-88.303-238.997 0-206.939 167.060-374 374-374zM797.697 272.963c54.347 64.922 88.303 147.518 88.303 239.037 0 206.939-167.060 374-374 374-91.503 0-174.078-33.963-238.997-88.303l524.689-524.728z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-single\"],\"defaultCode\":59673,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":75,\"id\":29,\"name\":\"single\",\"prevSize\":32,\"code\":59673},\"setIdx\":0,\"setId\":2,\"iconIdx\":32},{\"icon\":{\"paths\":[\"M197.733 16.079c-57.158 2.263-109.626 48.892-109.626 111.719v768.402c0 83.768 93.326 138.729 166.607 98.131l693.403-384.201c75.389-41.772 75.389-154.491 0-196.264l-693.403-384.201c-18.321-10.148-37.927-14.342-56.98-13.588zM198.61 89.763c6.448-0.029 13.207 1.643 19.82 5.308l693.452 384.201c27.392 15.178 27.392 50.275 0 65.453l-693.452 384.201c-26.458 14.659-55.52-2.494-55.52-32.727v-768.402c0-15.116 7.249-26.935 17.825-33.165 5.288-3.117 11.426-4.843 17.874-4.87z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-play-outline\"],\"defaultCode\":59681,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":76,\"id\":30,\"name\":\"play-outline\",\"prevSize\":32,\"code\":59681},\"setIdx\":0,\"setId\":2,\"iconIdx\":33},{\"icon\":{\"paths\":[\"M469.685 572.65v-572.65h84.628v572.65zM511.999 1023.998q-104.374 0-196.76-40.198t-161.498-109.312-109.312-161.498-40.198-196.76q0-112.837 47.956-210.865t135.405-174.192l59.24 59.24q-74.754 60.651-116.364 144.573t-41.609 181.246q0 176.308 123.416 299.724t299.724 123.416 299.724-123.416 123.416-299.724q0-97.322-41.609-182.655t-113.543-143.163l60.651-59.24q84.628 71.934 131.878 172.077t47.25 212.981q0 104.374-40.198 196.76t-108.606 161.498-160.793 109.312-198.171 40.198z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-exit\"],\"defaultCode\":59679,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":77,\"id\":31,\"name\":\"exit2\",\"prevSize\":32,\"code\":59679},\"setIdx\":0,\"setId\":2,\"iconIdx\":34},{\"icon\":{\"paths\":[\"M86.515 1024.001q-34.039 0-59.568-25.529t-25.529-59.568v-297.839h85.097v297.839h850.97v-853.806h-850.97v300.676h-85.097v-300.676q0-34.039 25.529-59.568t59.568-25.529h850.97q34.039 0 59.568 25.529t25.529 59.568v853.806q0 34.039-25.529 59.568t-59.568 25.529zM416.975 787.148l-63.823-63.823 167.358-167.358h-519.092v-85.097h519.092l-167.358-167.358 63.823-63.823 273.729 273.729z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-exit\"],\"defaultCode\":59678,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":78,\"id\":32,\"name\":\"exit\",\"prevSize\":32,\"code\":59678},\"setIdx\":0,\"setId\":2,\"iconIdx\":35},{\"icon\":{\"paths\":[\"M465.902 1.102c-44.961-7.649-81.748 25.464-81.748 71.063v471.297c-26.614-9.764-55.241-15.373-85.225-15.373-136.943 0-247.961 111.022-247.961 247.97 0 136.939 111.018 247.942 247.961 247.942 78.047 0 147.539-36.165 192.953-92.542l-113.518-113.532c-34.555-34.546-26.765-66.068-21.813-78.044 4.951-11.963 21.749-39.776 70.599-39.776h41.336v-130.039c0-48.308 33.415-88.839 78.332-100.001 0.073-101.437 0.073-187.014 0.073-187.014 320.377 0 301.122 191.965 263.634 302.201-14.676 43.169-5.946 50.406 24.901 16.811 390.955-425.798-153.327-564.193-369.524-600.961z\",\"M688.227 511.782h-116.573c-8.941 0-17.314 2.172-24.892 5.768-19.696 9.35-33.41 29.256-33.41 52.516v174.902h-86.201c-32.192 0-39.84 18.45-17.067 41.214l178.635 178.638c11.393 11.396 26.319 17.086 41.224 17.086 14.912 0 29.837-5.678 41.214-17.086l178.656-178.64c22.748-22.764 15.106-41.214-17.076-41.214h-86.205v-174.902c0.002-32.182-26.106-58.282-58.305-58.282z\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-logo\"],\"defaultCode\":59669,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":79,\"id\":33,\"name\":\"logo\",\"prevSize\":32,\"code\":59669},\"setIdx\":0,\"setId\":2,\"iconIdx\":36},{\"icon\":{\"paths\":[\"M181.333 170.667c-52.928 0-96 43.072-96 96v490.667c0 52.928 43.072 96 96 96h309.417c-8.448-20.203-14.314-41.707-17.792-64h-291.625c-17.643 0-32-14.357-32-32v-490.667c0-17.643 14.357-32 32-32h161.125c12.459 0 24.567 4.376 34.167 12.333l104.25 86.917c5.739 4.8 12.97 7.417 20.458 7.417h341.333c17.643 0 32 14.357 32 32v127.542c23.531 12.309 45.035 27.885 64 46.125v-173.667c0-52.928-43.072-96-96-96h-329.75l-95.375-79.5c-21.056-17.515-47.713-27.167-75.083-27.167h-161.125zM746.667 512c-129.6 0-234.667 105.067-234.667 234.667s105.067 234.667 234.667 234.667c129.6 0 234.667-105.067 234.667-234.667s-105.067-234.667-234.667-234.667zM746.667 576c11.776 0 21.333 9.557 21.333 21.333v128h128c11.776 0 21.333 9.557 21.333 21.333s-9.557 21.333-21.333 21.333h-128v128c0 11.776-9.557 21.333-21.333 21.333s-21.333-9.557-21.333-21.333v-128h-128c-11.776 0-21.333-9.557-21.333-21.333s9.557-21.333 21.333-21.333h128v-128c0-11.776 9.557-21.333 21.333-21.333z\"],\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"iocn-add_folder\"],\"defaultCode\":59666,\"colorPermutations\":{\"25525525519191911\":[{\"f\":0}]}},\"attrs\":[{\"fill\":\"rgb(91, 91, 91)\"}],\"properties\":{\"order\":80,\"id\":34,\"name\":\"add_folder\",\"prevSize\":32,\"code\":59666},\"setIdx\":0,\"setId\":2,\"iconIdx\":37},{\"icon\":{\"paths\":[\"M964.54 573.38c17.54-29.7 27.46-61.38 27.46-99.68 0-88.030-74.436-171.16-171.64-171.16h-72.96c9.84-25.62 17.7-56.26 17.7-93.080 0-145.588-75.38-209.46-190.54-209.46-123.214 0-116.186 189.866-143.52 217.2-45.494 45.494-99.23 132.894-137.52 166.8h-197.52c-35.346 0-64 28.654-64 64v480c0 35.346 28.654 64 64 64h128c29.786 0 54.816-20.348 61.956-47.9 89.018 2.002 150.12 79.88 355.604 79.88 14.44 0 30.44 0.020 44.44 0.020 154.234 0 223.972-78.846 225.88-190.66 26.638-36.85 40.598-86.244 34.68-133.98 19.708-36.904 27.328-80.686 17.98-125.98zM841.040 681.040c25.12 42.26 2.52 98.82-27.88 115.14 15.4 97.56-35.216 131.8-106.24 131.8h-75.64c-143.278 0-236.058-75.64-343.28-75.64v-372.34h21.84c56.72 0 135.96-141.78 189.080-194.92 56.72-56.72 37.82-151.26 75.64-189.080 94.54 0 94.54 65.96 94.54 113.46 0 78.34-56.72 113.44-56.72 189.080h207.98c42.22 0 75.46 37.82 75.64 75.64 0.18 37.8-25.64 75.62-44.54 75.62 26.978 29.11 32.742 90.472-10.42 131.24zM208 864c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48 48 21.49 48 48z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-thumbs-up\"],\"defaultCode\":59675,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":81,\"id\":35,\"name\":\"thumbs-up\",\"prevSize\":32,\"code\":59675},\"setIdx\":0,\"setId\":2,\"iconIdx\":38},{\"icon\":{\"paths\":[\"M509.272 282.5s-134.688-165.602-304.010-51.748c-151.442 165.64-64.878 386.113 304.010 579.983 46.009-18.027 94.868-36.652 94.868-36.652 40.964-13.784 60.703 44.723 34.497 56.060-59.488 25.735 12.883-8.927-129.364 60.371-628.547-297.863-457.104-653.226-347.129-722.289 187.149-142.063 349.285 21.563 349.285 21.563s125.392-130.495 301.851-49.592c224.303 126.189 137.987 349.068 97.023 422.589-19.732 32.671-85.295-8.963-68.994-34.497 43.856-81.687 98.151-231.451-53.904-323.411-151.099-64.857-278.136 77.615-278.136 77.615z\",\"M783.097 709.401c-17.463 0.024-53.411-0.332-86.245 0-33.876 0.344-32.95 75.386 0 75.461 32.558 0.074 86.245 0 86.245 0s-0.146 92.282 0 103.49c0.448 34.415 69.558 34.046 68.994-2.156-0.156-10.021 0-101.335 0-101.335s81.947-0.069 92.712 0c33.922 0.099 32.225-73.263-2.156-73.305-10.528 0.2-90.556 0-90.556 0s-0.116-61.104 0-99.179c0.099-32.749-69.055-33.445-68.994 0 0.059 31.78-0.472 90.856 0 97.023z\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-add-music\"],\"defaultCode\":59670,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":82,\"id\":36,\"name\":\"add-music\",\"prevSize\":32,\"code\":59670},\"setIdx\":0,\"setId\":2,\"iconIdx\":39},{\"icon\":{\"paths\":[\"M512 768.001c70.692 0 128 57.308 128 128v0c0 70.692-57.308 128-128 128v0c-70.692 0-128-57.308-128-128v0c0-70.692 57.308-128 128-128v0zM512 384c70.692 0 128 57.308 128 128v0c0 70.692-57.308 128-128 128v0c-70.692 0-128-57.308-128-128v0c0-70.692 57.308-128 128-128v0zM512 0c70.692 0 128 57.308 128 128v0c0 70.692-57.308 128-128 128v0c-70.692 0-128-57.308-128-128v0c0-70.692 57.308-128 128-128v0z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-dots-vertical\"],\"defaultCode\":59667,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":83,\"id\":37,\"name\":\"dots-vertical\",\"prevSize\":32,\"code\":59667},\"setIdx\":0,\"setId\":2,\"iconIdx\":40},{\"icon\":{\"paths\":[\"M633.703 512l365.094-365.094c33.607-33.607 33.607-88.095 0-121.698-33.607-33.607-88.091-33.607-121.698 0l-365.099 365.099-365.099-365.104c-33.607-33.607-88.091-33.607-121.698 0-33.603 33.607-33.603 88.095 0 121.698l365.099 365.094-365.099 365.099c-33.603 33.607-33.603 88.095 0 121.698 33.607 33.607 88.091 33.607 121.698 0l365.099-365.099 365.099 365.099c33.603 33.607 88.091 33.607 121.698 0s33.607-88.091 0-121.698l-365.094-365.094z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-close\"],\"defaultCode\":59662,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":84,\"id\":38,\"name\":\"close\",\"prevSize\":32,\"code\":59662},\"setIdx\":0,\"setId\":2,\"iconIdx\":41},{\"icon\":{\"paths\":[\"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\",\"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\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-pause\"],\"defaultCode\":59661,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":85,\"id\":39,\"name\":\"pause\",\"prevSize\":32,\"code\":59661},\"setIdx\":0,\"setId\":2,\"iconIdx\":42},{\"icon\":{\"paths\":[\"M217.83 22.578c-92.32-52.955-167.165-9.574-167.165 96.819v785.129c0 106.499 74.847 149.824 167.165 96.919l686.242-393.555c92.351-52.974 92.351-138.801 0-191.763l-686.242-393.549z\"],\"attrs\":[{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-play\"],\"defaultCode\":59653,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":86,\"id\":40,\"name\":\"play\",\"prevSize\":32,\"code\":59653},\"setIdx\":0,\"setId\":2,\"iconIdx\":43},{\"icon\":{\"paths\":[\"M96.898 152.171c-53.489 0-96.898 64.037-96.898 143.006v433.653c0 78.944 43.432 143.001 96.898 143.001 53.545 0 96.902-64.056 96.902-143.001v-119.627l352.204 201.971c67.023 38.471 121.477 8.351 123.795-67l-225.15-129.124c-44.415-25.451-69.917-63.036-69.917-103.083 0-40.084 25.502-77.637 69.917-103.134l225.15-129.073c-2.318-75.295-56.772-105.452-123.795-66.986l-352.204 201.92v-119.515c0.023-78.995-43.358-143.006-96.902-143.006z\",\"M502.254 583.092l397.714 228.080c68.475 39.291 124.032 7.103 124.032-71.883v-454.686c0-78.94-55.557-111.137-124.032-71.832l-397.742 228.020c-68.452 39.301-68.452 103.009 0.028 142.301z\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-prevMusic\"],\"defaultCode\":59668,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":87,\"id\":41,\"prevSize\":32,\"code\":59668,\"name\":\"prevMusic\"},\"setIdx\":0,\"setId\":2,\"iconIdx\":44},{\"icon\":{\"paths\":[\"M927.102 871.83c53.489 0 96.898-64.037 96.898-143.006v-433.653c0-78.944-43.432-143.001-96.898-143.001-53.545 0-96.902 64.056-96.902 143.001v119.627l-352.204-201.971c-67.023-38.471-121.477-8.351-123.795 67l225.15 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.15 129.072c2.318 75.295 56.772 105.452 123.795 66.986l352.204-201.92v119.515c-0.023 78.995 43.358 143.006 96.902 143.006z\",\"M521.746 440.909l-397.714-228.080c-68.475-39.291-124.032-7.103-124.032 71.883v454.686c0 78.94 55.557 111.137 124.032 71.832l397.742-228.020c68.452-39.301 68.452-103.009-0.028-142.301z\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-nextMusic\"],\"defaultCode\":59654,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":88,\"id\":42,\"name\":\"nextMusic\",\"prevSize\":32,\"code\":59654},\"setIdx\":0,\"setId\":2,\"iconIdx\":45},{\"icon\":{\"paths\":[\"M578.472 732.037c-121.522 0-220.036-98.514-220.036-220.036s98.514-220.036 220.036-220.036c121.522 0 220.036 98.514 220.036 220.036v0c-0.146 121.464-98.572 219.891-220.023 220.036h-0.014zM578.472 384.418c-70.462 0-127.583 57.121-127.583 127.583s57.12 127.583 127.583 127.583c70.462 0 127.583-57.12 127.583-127.583v0c-0.079-70.43-57.152-127.503-127.574-127.583h-0.008z\",\"M1145.056 469.341l-246.423-426.682c-15.048-25.628-42.44-42.579-73.799-42.659h-492.694c-31.404 0.050-58.837 17.009-73.685 42.258l-0.218 0.401-246.329 426.682c-7.162 12.244-11.391 26.957-11.391 42.659s4.229 30.416 11.61 43.065l-0.219-0.406 246.423 426.682c15.063 25.643 42.485 42.601 73.871 42.659h492.621c31.397-0.054 58.822-17.012 73.662-42.258l0.218-0.401 246.423-426.682c7.14-12.229 11.357-26.921 11.357-42.599 0-15.727-4.242-30.462-11.645-43.125l0.219 0.405zM341.627 922.223l-236.844-410.223 236.844-410.247h473.712l236.844 410.247-236.844 410.223z\"],\"attrs\":[{},{}],\"width\":1157,\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-setting\"],\"defaultCode\":59655,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":89,\"id\":43,\"name\":\"setting\",\"prevSize\":32,\"code\":59655},\"setIdx\":0,\"setId\":2,\"iconIdx\":46},{\"icon\":{\"paths\":[\"M974.751 1023.699h-925.525c-27.228-0.19-49.226-22.307-49.226-49.561 0-0.119 0-0.238 0.001-0.357v0.018-414.489c-0.001-0.1-0.001-0.219-0.001-0.338 0-27.254 21.999-49.371 49.208-49.561h0.018c27.228 0.19 49.226 22.307 49.226 49.561 0 0.119 0 0.238-0.001 0.357v-0.018 364.613h827.075v-365.504c0-27.193 22.044-49.237 49.237-49.237s49.237 22.044 49.237 49.237v0 415.38c0 0.007 0 0.016 0 0.025 0 13.741-5.516 26.193-14.456 35.262l0.006-0.006c-8.86 9.010-21.175 14.599-34.796 14.618h-0.004z\",\"M512 582.502c-25.964-0.191-46.938-21.283-46.938-47.273 0-0.102 0-0.204 0.001-0.306v0.016-487.676c0-25.936 21.025-46.961 46.961-46.961s46.961 21.025 46.961 46.961v0 487.58c0.001 0.115 0.002 0.25 0.002 0.386 0 26.008-21.002 47.111-46.972 47.274h-0.016z\",\"M512 658.628c-0.024 0-0.052 0-0.080 0-13.966 0-26.633-5.558-35.912-14.582l0.012 0.012-193.361-186.497c-8.999-8.738-14.582-20.95-14.582-34.466 0-12.694 4.925-24.238 12.969-32.824l-0.025 0.027c8.515-9.098 20.596-14.768 34.002-14.768 12.587 0 24.006 4.998 32.382 13.118l164.57 158.742 164.582-158.754c8.364-8.108 19.783-13.106 32.37-13.106 13.406 0 25.487 5.67 33.978 14.742l0.024 0.026c8.019 8.56 12.945 20.104 12.945 32.798 0 13.516-5.584 25.728-14.571 34.454l-0.012 0.011-193.385 186.521c-9.268 8.998-21.928 14.546-35.885 14.546-0.008 0-0.016 0-0.024 0h0.001zM540.755 575.109v0z\"],\"attrs\":[{},{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-download-2\"],\"defaultCode\":59656,\"colorPermutations\":{\"25525525519191911\":[{},{},{}]}},\"attrs\":[{},{},{}],\"properties\":{\"order\":90,\"id\":44,\"name\":\"download-2\",\"prevSize\":32,\"code\":59656},\"setIdx\":0,\"setId\":2,\"iconIdx\":47},{\"icon\":{\"paths\":[\"M582.103 1023.979c-0.017 0-0.037 0-0.057 0-9.91 0-19.135-2.929-26.858-7.969l0.189 0.116-0.366-0.236c-37.014-20.47-322.334-182.265-474.13-387.408-50.491-68.21-80.819-154.003-80.819-246.879 0-19.704 1.365-39.089 4.006-58.067l-0.25 2.192c14.344-104.705 67.037-196.32 148.366-258.045 92.559-70.283 201.609-86.225 315.345-45.86 43.791 16.083 81.683 36.31 116.21 60.947l-1.48-1.003c33.048-23.634 70.939-43.86 111.372-58.853l3.359-1.091c113.788-40.337 222.838-24.422 315.37 45.86 81.33 61.829 134.022 153.471 148.366 258.045 2.401 16.818 3.771 36.242 3.771 55.986 0 92.851-30.314 178.622-81.584 247.955l0.802-1.136c-152.712 206.19-440.415 368.797-474.783 387.643-7.598 4.895-16.869 7.819-26.821 7.853h-0.009zM347.59 105.694c-43.425 0-89.994 12.46-135.593 47.117-59.054 44.892-97.349 111.588-107.794 187.813-1.757 12.365-2.76 26.646-2.76 41.16 0 68.639 22.431 132.040 60.363 183.271l-0.591-0.835c123.421 166.664 348.143 304.769 421.071 347.018 73.005-42.169 297.65-180.354 421.071-347.018 37.342-50.393 59.773-113.792 59.773-182.429 0-14.517-1.004-28.801-2.945-42.783l0.184 1.616c-10.471-76.199-48.74-142.895-107.82-187.813-159.125-120.933-330.108 28.27-337.384 34.684-8.691 7.859-20.268 12.668-32.969 12.668s-24.278-4.809-33.012-12.706l0.043 0.038c-5.052-4.555-93.553-81.801-201.634-81.801z\"],\"attrs\":[{}],\"width\":1165,\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-love\"],\"defaultCode\":59657,\"colorPermutations\":{\"25525525519191911\":[{}]}},\"attrs\":[{}],\"properties\":{\"order\":91,\"id\":45,\"name\":\"love\",\"prevSize\":32,\"code\":59657},\"setIdx\":0,\"setId\":2,\"iconIdx\":48},{\"icon\":{\"paths\":[\"M968.343 913.191h-912.975c-30.592 0-55.393 24.8-55.393 55.393s24.8 55.393 55.393 55.393v0h912.975c30.592 0 55.393-24.8 55.393-55.393s-24.8-55.393-55.393-55.393v0z\",\"M931.085 848.958c30.592 0 55.393-24.8 55.393-55.393v0-738.197c0-30.592-24.8-55.393-55.393-55.393s-55.393 24.8-55.393 55.393v0 738.173c0 0.007 0 0.016 0 0.024 0 30.592 24.8 55.393 55.393 55.393v0z\",\"M511.735 848.958c30.592 0 55.393-24.8 55.393-55.393v0-504.921c0-30.592-24.8-55.393-55.393-55.393s-55.393 24.8-55.393 55.393v0 504.896c0 0.007 0 0.016 0 0.024 0 30.592 24.8 55.393 55.393 55.393v0z\",\"M92.386 848.958c30.592 0 55.393-24.8 55.393-55.393v0-331.636c0-30.592-24.8-55.393-55.393-55.393s-55.393 24.8-55.393 55.393v0 331.612c0 0.007 0 0.016 0 0.024 0 30.592 24.8 55.393 55.393 55.393v0z\"],\"attrs\":[{},{},{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-leaderboard\"],\"defaultCode\":59658,\"colorPermutations\":{\"25525525519191911\":[{},{},{},{}]}},\"attrs\":[{},{},{},{}],\"properties\":{\"order\":92,\"id\":46,\"name\":\"leaderboard\",\"prevSize\":32,\"code\":59658},\"setIdx\":0,\"setId\":2,\"iconIdx\":49},{\"icon\":{\"paths\":[\"M48.166 895.662c-26.601 0-48.166-21.564-48.166-48.166v0-799.331c0-26.601 21.564-48.166 48.166-48.166v0h755.983c26.601 0 48.166 21.564 48.166 48.166s-21.564 48.166-48.166 48.166v0h-707.816v751.165c0 26.601-21.564 48.166-48.166 48.166v0z\",\"M975.834 1024h-736.932c-26.601 0-48.166-21.564-48.166-48.166v0-354.499c0-26.601 21.564-48.166 48.166-48.166s48.166 21.564 48.166 48.166v0 306.333h640.602v-633.858h-515.371c-26.601 0-48.166-21.564-48.166-48.166s21.564-48.166 48.166-48.166v0h563.537c26.601 0 48.166 21.564 48.166 48.166v0 730.19c0 26.601-21.564 48.166-48.166 48.166v0z\",\"M623.744 786.76c-23.941 0-43.349-19.408-43.349-43.349v0-346.046c0-23.941 19.408-43.349 43.349-43.349s43.349 19.408 43.349 43.349v0 345.828c0 0.064 0.001 0.141 0.001 0.217 0 23.941-19.408 43.349-43.349 43.349 0 0 0 0-0.001 0v0z\",\"M764.001 581.238c-0.010 0-0.022 0-0.034 0-11.992 0-22.847-4.869-30.695-12.739l-140.307-140.307c-7.864-7.864-12.729-18.729-12.729-30.73 0-24.001 19.457-43.458 43.458-43.458 12 0 22.865 4.864 30.73 12.729l140.306 140.33c7.964 7.864 12.896 18.781 12.896 30.851 0 23.941-19.408 43.349-43.349 43.349-0.097 0-0.195 0-0.292-0.001h0.015z\",\"M537.239 867.414c-0.021 0-0.047 0-0.072 0-71.756 0-129.927-58.17-129.927-129.927s58.17-129.927 129.927-129.927c71.756 0 129.927 58.17 129.927 129.927v0c-0.068 71.704-58.157 129.817-129.844 129.927h-0.010zM537.239 694.498c-23.741 0-42.988 19.246-42.988 42.988s19.246 42.988 42.988 42.988c23.741 0 42.988-19.246 42.988-42.988v0c-0.027-23.73-19.257-42.96-42.985-42.988h-0.003z\"],\"attrs\":[{},{},{},{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-album\"],\"defaultCode\":59659,\"colorPermutations\":{\"25525525519191911\":[{},{},{},{},{}]}},\"attrs\":[{},{},{},{},{}],\"properties\":{\"order\":93,\"id\":47,\"name\":\"album\",\"prevSize\":32,\"code\":59659},\"setIdx\":0,\"setId\":2,\"iconIdx\":50},{\"icon\":{\"paths\":[\"M744.005 130.752c-78.812-80.698-188.704-130.742-310.286-130.742-239.452 0-433.565 194.114-433.565 433.565s194.114 433.565 433.565 433.565c117.797 0 224.623-46.978 302.762-123.221l-0.089 0.086c81.089-78.675 131.413-188.664 131.413-310.409 0-117.972-47.254-224.906-123.865-302.909l0.063 0.064zM669.119 675.117c-60.686 59.116-143.692 95.577-235.214 95.577-186.24 0-337.217-150.977-337.217-337.217 0-186.089 150.733-336.972 336.765-337.217h0.023c0.077 0 0.168 0 0.258 0 186.24 0 337.217 150.977 337.217 337.217 0 94.682-39.020 180.25-101.858 241.499l-0.071 0.069z\",\"M1007.3 928.945l-159.527-163.792c-10.072-10.318-24.118-16.717-39.659-16.717-30.597 0-55.4 24.804-55.4 55.4 0 15.056 6.006 28.709 15.753 38.694l-0.011-0.011 160.564 164.851c10.026 10.27 24.010 16.639 39.481 16.639 2.35 0 4.667-0.147 6.941-0.432l-0.271 0.028c10.466-1.218 19.816-5.232 27.505-11.266l-0.118 0.089c13.153-10.193 21.538-25.994 21.538-43.752 0-0.259-0.002-0.517-0.005-0.775v0.039c-0.431-15.268-6.764-28.978-16.788-38.997v0z\"],\"attrs\":[{},{}],\"isMulticolor\":false,\"isMulticolor2\":false,\"grid\":128,\"tags\":[\"icon-search-2\"],\"defaultCode\":59660,\"colorPermutations\":{\"25525525519191911\":[{},{}]}},\"attrs\":[{},{}],\"properties\":{\"order\":94,\"id\":48,\"name\":\"search-2\",\"prevSize\":32,\"code\":59660},\"setIdx\":0,\"setId\":2,\"iconIdx\":51}],\"height\":1024,\"metadata\":{\"name\":\"icomoon\"},\"preferences\":{\"showGlyphs\":true,\"showQuickUse\":true,\"showQuickUse2\":true,\"showSVGs\":true,\"fontPref\":{\"prefix\":\"icon-\",\"metadata\":{\"fontFamily\":\"icomoon\",\"majorVersion\":1,\"minorVersion\":0},\"metrics\":{\"emSize\":1024,\"baseline\":6.25,\"whitespace\":50},\"embed\":false,\"autoHost\":false,\"noie8\":true,\"ie7\":false,\"showSelector\":false,\"showMetrics\":false,\"showMetadata\":false,\"showVersion\":false},\"imagePref\":{\"prefix\":\"icon-\",\"png\":true,\"useClassSelector\":true,\"color\":0,\"bgColor\":16777215,\"classSelector\":\".icon\",\"name\":\"icomoon\"},\"historySize\":50,\"showCodes\":true,\"gridSize\":16,\"showGrid\":false,\"quickUsageToken\":{\"UntitledProject\":\"N2VjNTI2ODAzZWI0N2M1NzhlMjNhYzY3OTEwMWRiMDEjMSMxNjA0MzkyNDUxIyMj\"},\"showLiga\":false}}"
  },
  {
    "path": "src/screens/Comment/CommentHot.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { filterList, getHotComment } from './utils'\nimport music from '@/utils/musicSdk'\nimport List, { type ListType } from './components/List'\nconst limit = 15\n\nexport default ({ musicInfo, onUpdateTotal }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  onUpdateTotal: (total: number) => void\n}) => {\n  // const [isLoading, setIsLoading] = useState(false)\n  const listRef = useRef<ListType>(null)\n  const listInfo = useRef({ page: 1, total: 0, maxPage: 1, isEnd: false, isLoading: false, isLoadError: false })\n  const handleGetComment = async(musicInfo: LX.Music.MusicInfoOnline, page: number) => {\n    // setIsLoading(true)\n    listInfo.current.isLoading = true\n    return getHotComment(musicInfo, page, limit).then(commentInfo => {\n      listInfo.current.page = page\n      listInfo.current.isLoading = false\n      // setIsLoading(false)\n      if (listInfo.current.isLoadError) {\n        listInfo.current.isLoadError = false\n      }\n      return commentInfo\n    }).catch(async err => {\n      console.log(err)\n      if (err.message != '取消请求') {\n        listInfo.current.isLoading = false\n        // setIsLoading(false)\n        listInfo.current.isLoadError = true\n      }\n      throw err\n    })\n  }\n  const updateStatus = () => {\n    if (listInfo.current.isLoadError) {\n      listRef.current?.setStatus('error')\n    } else if (listInfo.current.isEnd) {\n      listRef.current?.setStatus('end')\n    } else if (!listInfo.current.isLoading) {\n      listRef.current?.setStatus('idle')\n    }\n  }\n  const handleListLoadMore = () => {\n    if (listInfo.current.isLoading || listInfo.current.isEnd) return\n    const list = listRef.current?.getList() ?? []\n    const page = list.length ? listInfo.current.page + 1 : 1\n    listRef.current?.setStatus('loading')\n    void handleGetComment(musicInfo, page).then(({ comments }) => {\n      let isEnd = page >= listInfo.current.maxPage\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      listRef.current?.setList(filterList([...listRef.current.getList(), ...comments]))\n    }).finally(updateStatus)\n  }\n\n  const handleListRefresh = () => {\n    listRef.current?.setStatus('refreshing')\n    void handleGetComment(musicInfo, 1).then(({ comments, maxPage, total }) => {\n      listInfo.current.total = total\n      listInfo.current.maxPage = maxPage\n      onUpdateTotal(total)\n      let isEnd = maxPage === 1\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      listRef.current?.setList(filterList(comments))\n    }).finally(updateStatus)\n  }\n\n  const handleShowComment = (musicInfo: LX.Music.MusicInfoOnline) => {\n    if (!musicInfo.id || !music[musicInfo.source].comment) return\n    listInfo.current.page = 1\n    listInfo.current.total = 0\n    listInfo.current.maxPage = 1\n    listInfo.current.isEnd = false\n    listInfo.current.isLoading = false\n    listInfo.current.isLoadError = false\n    listRef.current?.setList([])\n    listRef.current?.setStatus('loading')\n    void handleGetComment(musicInfo, 1).then(({ comments, maxPage, total }) => {\n      listInfo.current.total = total\n      listInfo.current.maxPage = maxPage\n      onUpdateTotal(total)\n      let isEnd = maxPage === 1\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      setTimeout(() => {\n        listRef.current?.setList(filterList(comments))\n        setTimeout(updateStatus, 300)\n      }, 300)\n    })\n  }\n\n  useEffect(() => {\n    handleShowComment(musicInfo)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [musicInfo.id])\n\n\n  return (\n    <List\n      ref={listRef}\n      onLoadMore={handleListLoadMore}\n      onRefresh={handleListRefresh}\n    />\n  )\n}\n"
  },
  {
    "path": "src/screens/Comment/CommentNew.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { filterList, getNewComment } from './utils'\nimport music from '@/utils/musicSdk'\nimport List, { type ListType } from './components/List'\nconst limit = 15\n\nexport default ({ musicInfo, onUpdateTotal }: {\n  musicInfo: LX.Music.MusicInfoOnline\n  onUpdateTotal: (total: number) => void\n}) => {\n  // const [isLoading, setIsLoading] = useState(false)\n  const listRef = useRef<ListType>(null)\n  const listInfo = useRef({ page: 1, total: 0, maxPage: 1, isEnd: false, isLoading: false, isLoadError: false })\n  const handleGetComment = async(musicInfo: LX.Music.MusicInfoOnline, page: number) => {\n    // setIsLoading(true)\n    listInfo.current.isLoading = true\n    return getNewComment(musicInfo, page, limit).then(commentInfo => {\n      listInfo.current.page = page\n      listInfo.current.isLoading = false\n      // setIsLoading(false)\n      if (listInfo.current.isLoadError) {\n        listInfo.current.isLoadError = false\n      }\n      return commentInfo\n    }).catch(async err => {\n      console.log(err)\n      if (err.message != '取消请求') {\n        listInfo.current.isLoading = false\n        // setIsLoading(false)\n        listInfo.current.isLoadError = true\n      }\n      throw err\n    })\n  }\n  const updateStatus = () => {\n    if (listInfo.current.isLoadError) {\n      listRef.current?.setStatus('error')\n    } else if (listInfo.current.isEnd) {\n      listRef.current?.setStatus('end')\n    } else if (!listInfo.current.isLoading) {\n      listRef.current?.setStatus('idle')\n    }\n  }\n  const handleListLoadMore = () => {\n    console.log('load')\n    if (listInfo.current.isLoading || listInfo.current.isEnd) return\n    const list = listRef.current?.getList() ?? []\n    const page = list.length ? listInfo.current.page + 1 : 1\n    listRef.current?.setStatus('loading')\n    void handleGetComment(musicInfo, page).then(({ comments }) => {\n      let isEnd = page >= listInfo.current.maxPage\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      listRef.current?.setList(filterList([...listRef.current.getList(), ...comments]))\n    }).finally(updateStatus)\n  }\n\n  const handleListRefresh = () => {\n    listRef.current?.setStatus('refreshing')\n    void handleGetComment(musicInfo, 1).then(({ comments, maxPage, total }) => {\n      listInfo.current.total = total\n      listInfo.current.maxPage = maxPage\n      onUpdateTotal(total)\n      let isEnd = maxPage === 1\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      listRef.current?.setList(filterList(comments))\n    }).finally(updateStatus)\n  }\n\n  const handleShowComment = (musicInfo: LX.Music.MusicInfoOnline) => {\n    if (!musicInfo.id || !music[musicInfo.source].comment) return\n    listInfo.current.page = 1\n    listInfo.current.total = 0\n    listInfo.current.maxPage = 1\n    listInfo.current.isEnd = false\n    listInfo.current.isLoading = false\n    listInfo.current.isLoadError = false\n    listRef.current?.setList([])\n    listRef.current?.setStatus('loading')\n    void handleGetComment(musicInfo, 1).then(({ comments, maxPage, total }) => {\n      listInfo.current.total = total\n      listInfo.current.maxPage = maxPage\n      onUpdateTotal(total)\n      let isEnd = maxPage === 1\n      if (listInfo.current.isEnd != isEnd) listInfo.current.isEnd = isEnd\n      setTimeout(() => {\n        listRef.current?.setList(filterList(comments))\n        setTimeout(updateStatus, 300)\n      }, 300)\n    })\n  }\n\n  useEffect(() => {\n    handleShowComment(musicInfo)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [musicInfo.id])\n\n\n  return (\n    <List\n      ref={listRef}\n      onLoadMore={handleListLoadMore}\n      onRefresh={handleListRefresh}\n    />\n  )\n}\n"
  },
  {
    "path": "src/screens/Comment/components/CommentFloor.tsx",
    "content": "import { memo, useState, useMemo, useCallback } from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport { BorderWidths } from '@/theme'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { type Comment } from '../utils'\nimport Text from '@/components/common/Text'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { useLayout } from '@/utils/hooks'\nimport { useI18n } from '@/lang'\nimport Image from '@/components/common/Image'\nimport CommentImage from './CommentImage'\nimport CommentText from './CommentText'\n// eslint-disable-next-line @typescript-eslint/no-var-requires\nconst defaultUser = require('@/resources/images/defaultUser.jpg')\n\nconst GAP = 12\nconst avatarWidth = scaleSizeW(36)\n\nconst CommentFloor = memo(({ comment, isLast }: {\n  comment: Comment\n  isLast?: boolean\n}) => {\n  const theme = useTheme()\n  const [isAvatarError, setIsAvatarError] = useState(false)\n  const { onLayout, width } = useLayout()\n  const t = useI18n()\n\n  const handleAvatarError = useCallback(() => {\n    setIsAvatarError(true)\n  }, [])\n\n  const replyComments = useMemo(() => {\n    if (!comment.reply?.length) return null\n    const endIndex = comment.reply.length - 1\n    return (\n      <View style={{ ...styles.replyFloor, borderTopColor: theme['c-list-header-border-bottom'] }}>\n        {\n          comment.reply.map((c, index) => (\n            <CommentFloor comment={c} isLast={index === endIndex} key={`${comment.id}_${c.id}`} />\n          ))\n        }\n      </View>\n    )\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  const likedCount = useMemo(() => {\n    if (comment.likedCount == null) return null\n    return (\n      <View style={styles.like}>\n        <Icon name=\"thumbs-up\" style={{ color: theme['c-450'] }} size={12} />\n        <Text style={styles.likedCount} size={12} color={ theme['c-450'] }>{comment.likedCount}</Text>\n      </View>\n    )\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <View style={{ ...styles.container, borderBottomColor: theme['c-list-header-border-bottom'], borderBottomWidth: isLast ? 0 : BorderWidths.normal, paddingBottom: isLast ? 0 : GAP }}>\n      <View style={styles.comment}>\n        <View>\n          <Image\n            url={comment.avatar && !isAvatarError ? comment.avatar : defaultUser}\n            onError={handleAvatarError}\n            style={stylesRaw.avatar} />\n        </View>\n        <View style={styles.right}>\n          <View style={styles.info}>\n            <View>\n              <Text selectable numberOfLines={1} size={14}>\n                {comment.userName}\n              </Text>\n              <View style={styles.metaInfo}>\n                <Text numberOfLines={1} size={12} color={theme['c-450']}>{comment.timeStr}</Text>\n                { comment.location ? <Text numberOfLines={1} style={styles.location} size={12} color={theme['c-450']}>{t('location', { location: comment.location })}</Text> : null }\n              </View>\n            </View>\n            {likedCount}\n          </View>\n          <CommentText text={comment.text} />\n          {\n            comment.images?.length\n              ? (\n                  <View style={styles.images} onLayout={onLayout}>\n                    {\n                      comment.images.map((url, index) => <CommentImage key={String(index)} url={url} maxWidth={width} />)\n                    }\n                  </View>\n                )\n              : null\n          }\n        </View>\n      </View>\n      {replyComments}\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    marginTop: GAP,\n    paddingBottom: GAP,\n    borderBottomWidth: BorderWidths.normal,\n    borderStyle: 'dashed',\n  },\n  comment: {\n    flex: 1,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    flexDirection: 'row',\n  },\n  right: {\n    flex: 1,\n    paddingLeft: 10,\n  },\n  info: {\n    flex: 1,\n    flexDirection: 'row',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    justifyContent: 'space-between',\n    alignItems: 'center',\n  },\n  metaInfo: {\n    marginTop: 2,\n    flexDirection: 'row',\n  },\n  location: {\n    marginLeft: 10,\n  },\n  like: {\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  likedCount: {\n    marginLeft: 2,\n  },\n  images: {\n    paddingTop: 5,\n    width: '100%',\n    flexDirection: 'row',\n  },\n  replyFloor: {\n    marginTop: GAP,\n    marginLeft: 20,\n    borderTopWidth: BorderWidths.normal,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    borderStyle: 'dashed',\n  },\n})\n\nconst stylesRaw = StyleSheet.create({\n  avatar: {\n    height: avatarWidth,\n    width: avatarWidth,\n    borderRadius: 4,\n  },\n})\n\nexport default CommentFloor\n"
  },
  {
    "path": "src/screens/Comment/components/CommentImage.tsx",
    "content": "import { useEffect, useState } from 'react'\nimport { TouchableOpacity, View } from 'react-native'\nimport Image, { getSize } from '@/components/common/Image'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { BorderWidths } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\n\nconst MAX_IMAGE_HEIGHT = scaleSizeH(260)\n\n\nexport default ({ url, maxWidth }: { url: string, maxWidth: number }) => {\n  const [show, setShow] = useState(false)\n  const [wh, setWH] = useState({ width: 0, height: 0 })\n  const theme = useTheme()\n\n  useEffect(() => {\n    getSize(url, (realWidth, realHeight) => {\n      let w = 0\n      let h = 0\n\n      if (w && !h) {\n        h = realHeight * (w / realWidth)\n      } else if (!w && h) {\n        w = realWidth * (h / realHeight)\n      } else {\n        if (maxWidth && realWidth > maxWidth) {\n          w = maxWidth\n          h = realHeight * (w / realWidth)\n\n          if (MAX_IMAGE_HEIGHT && h > MAX_IMAGE_HEIGHT) {\n            w = realWidth * (MAX_IMAGE_HEIGHT / realHeight)\n            h = MAX_IMAGE_HEIGHT\n          }\n        } else if (MAX_IMAGE_HEIGHT && realHeight > MAX_IMAGE_HEIGHT) {\n          w = realWidth * (h / realHeight)\n          h = MAX_IMAGE_HEIGHT\n        }\n      }\n      setWH({ width: w || realWidth, height: h || realHeight })\n    })\n  }, [maxWidth, url])\n\n  return (\n    wh.width ? (\n      <View style={{ height: wh.height, width: wh.width }}>\n        {\n          show ? (<Image\n            url={url}\n            style={{ height: wh.height, width: wh.width, borderWidth: BorderWidths.normal, borderColor: theme['c-border-background'] }}\n          />) : (\n            <TouchableOpacity style={{ ...styles.defaultPic, borderColor: theme['c-border-background'], backgroundColor: theme['c-primary-light-200-alpha-900'] }} onPress={() => { setShow(true) }}>\n              <Text size={13} color={theme['c-primary-font-hover']}>{global.i18n.t('comment_show_image')}</Text>\n            </TouchableOpacity>\n          )\n        }\n      </View>\n    ) : null\n  )\n}\n\nconst styles = createStyle({\n  defaultPic: {\n    width: '100%',\n    height: '100%',\n    alignItems: 'center',\n    justifyContent: 'center',\n    borderWidth: BorderWidths.normal,\n    borderStyle: 'dashed',\n  },\n})\n"
  },
  {
    "path": "src/screens/Comment/components/CommentText.tsx",
    "content": "import { memo, useMemo, useState } from 'react'\nimport { TouchableOpacity, View } from 'react-native'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\n\nconst TEXT_LIMIT = 160\nconst SUB_TEXT_LIMIT = TEXT_LIMIT * 1.2\n// const CHAR_RXP = /\\n/g\n\nexport default memo(({ text }: { text: string }) => {\n  const [show, setShow] = useState(false)\n  const theme = useTheme()\n\n  const length = useMemo(() => {\n    // text.length + (text.match(CHAR_RXP)?.length ?? 0) * 40\n    let count = 0\n    let bCount = 0\n    let subLength = 0\n    for (let i = 0; i < text.length; i++) {\n      let char = text.charAt(i)\n      if (char == '\\n') {\n        count += bCount * 24 + 20\n        bCount++\n      } else {\n        count++\n        if (char.trim() != '') bCount &&= 0\n      }\n      if (!subLength && count > TEXT_LIMIT) subLength = i\n      if (count >= SUB_TEXT_LIMIT) return subLength\n    }\n    return 0\n  }, [text])\n\n  return (\n    length ? (\n      <View>\n        {\n          show ? <Text selectable style={styles.text}>{text}</Text>\n            : <Text selectable style={styles.text}>{text.substring(0, length)} <Text color={theme['c-font-label']}>……</Text></Text>\n        }\n        <TouchableOpacity style={styles.toggle} onPress={() => { setShow(!show) }}>\n          <Text color={theme['c-primary-font']}>{show ? global.i18n.t('comment_hide_text') : global.i18n.t('comment_show_text')}</Text>\n        </TouchableOpacity>\n\n      </View>\n    ) : <Text selectable style={styles.text}>{text}</Text>\n  )\n})\n\nconst styles = createStyle({\n  text: {\n    marginTop: 5,\n    lineHeight: 19,\n  },\n  toggle: {\n    marginTop: 15,\n    alignSelf: 'flex-end',\n  },\n})\n"
  },
  {
    "path": "src/screens/Comment/components/Header.tsx",
    "content": "import { memo } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\nimport { pop } from '@/navigation'\n// import { AppColors } from '@/theme'\nimport StatusBar from '@/components/common/StatusBar'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { HEADER_HEIGHT as _HEADER_HEIGHT } from '@/config/constant'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport commonState from '@/store/common/state'\nimport { useStatusbarHeight } from '@/store/common/hook'\n\nconst HEADER_HEIGHT = scaleSizeH(_HEADER_HEIGHT)\n\nexport default memo(({ musicInfo }: {\n  musicInfo: LX.Music.MusicInfo\n}) => {\n  const t = useI18n()\n  const statusBarHeight = useStatusbarHeight()\n\n  const back = () => {\n    void pop(commonState.componentIds.comment!)\n  }\n\n  return (\n    <View style={{ height: HEADER_HEIGHT + statusBarHeight, paddingTop: statusBarHeight }}>\n      <StatusBar />\n      <View style={{ ...styles.container }}>\n        <TouchableOpacity onPress={back} style={{ ...styles.button, width: HEADER_HEIGHT }}>\n          <Icon name=\"chevron-left\" size={18} />\n        </TouchableOpacity>\n        <Text numberOfLines={1} size={16} style={styles.title}>{t('comment_title', { name: musicInfo.name, singer: musicInfo.singer })}</Text>\n        {/* <TouchableOpacity onPress={back} style={{ ...styles.button }}>\n          <Icon name=\"available_updates\" style={{ color: theme.normal }} size={24} />\n        </TouchableOpacity> */}\n      </View>\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    flexDirection: 'row',\n    alignItems: 'center',\n    height: '100%',\n    paddingRight: 40,\n    // backgroundColor: 'rgba(255, 255, 255, 0.5)',\n  },\n  button: {\n    // paddingLeft: 10,\n    // paddingRight: 10,\n    width: '100%',\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n  title: {\n    flex: 1,\n    textAlign: 'center',\n  },\n  icon: {\n    paddingLeft: 4,\n    paddingRight: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/Comment/components/List.tsx",
    "content": "import { useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react'\nimport { FlatList, type FlatListProps, RefreshControl, View } from 'react-native'\n\n// import { useMusicList } from '@/store/list/hook'\nimport CommentFloor from './CommentFloor'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { type Comment } from '../utils'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\n\ntype FlatListType = FlatListProps<Comment>\n\nexport interface ListProps {\n  onRefresh: () => void\n  onLoadMore: () => void\n}\nexport interface ListType {\n  setList: (list: Comment[]) => void\n  getList: () => Comment[]\n  setStatus: (val: Status) => void\n}\nexport type Status = 'loading' | 'refreshing' | 'end' | 'error' | 'idle'\n\nconst List = forwardRef<ListType, ListProps>(({\n  onRefresh,\n  onLoadMore,\n}, ref) => {\n  // const t = useI18n()\n  const theme = useTheme()\n  const flatListRef = useRef<FlatList>(null)\n  const [currentList, setList] = useState<Comment[]>([])\n  const [status, setStatus] = useState<Status>('idle')\n  // const currentListIdRef = useRef('')\n  // console.log('render comment list')\n\n  useImperativeHandle(ref, () => ({\n    setList(list) {\n      setList(list)\n    },\n    getList() {\n      return currentList\n    },\n    setStatus(val) {\n      setStatus(val)\n    },\n  }))\n\n  const handleLoadMore = () => {\n    if (status != 'idle') return\n    onLoadMore()\n  }\n\n  const renderItem: FlatListType['renderItem'] = ({ item }) => <CommentFloor comment={item} />\n\n  const getkey: FlatListType['keyExtractor'] = item => item.id\n\n  const refreshControl = useMemo(() => (\n    <RefreshControl\n      colors={[theme['c-primary']]}\n      // progressBackgroundColor={theme.primary}\n      refreshing={status == 'refreshing'}\n      onRefresh={onRefresh} />\n  ), [status, onRefresh, theme])\n  const footerComponent = useMemo(() => {\n    let label: FooterLabel\n    switch (status) {\n      case 'refreshing': return null\n      case 'loading':\n        label = 'list_loading'\n        break\n      case 'end':\n        label = 'list_end'\n        break\n      case 'error':\n        label = 'list_error'\n        break\n      case 'idle':\n        label = null\n        break\n    }\n    return <Footer label={label} onLoadMore={onLoadMore} />\n  }, [onLoadMore, status])\n\n  return (\n    <FlatList\n      ref={flatListRef}\n      style={styles.list}\n      data={currentList}\n      onEndReachedThreshold={0.5}\n      // maxToRenderPerBatch={4}\n      // updateCellsBatchingPeriod={80}\n      // windowSize={8}\n      removeClippedSubviews={false}\n      // initialNumToRender={12}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      // onRefresh={onRefresh}\n      // refreshing={refreshing}\n      onEndReached={handleLoadMore}\n      refreshControl={refreshControl}\n      ListFooterComponent={footerComponent}\n    />\n  )\n})\n\ntype FooterLabel = 'list_loading' | 'list_end' | 'list_error' | null\nconst Footer = ({ label, onLoadMore }: {\n  label: FooterLabel\n  onLoadMore: () => void\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const handlePress = () => {\n    if (label != 'list_error') return\n    onLoadMore()\n  }\n  return (\n    label\n      ? (\n          <View>\n            <Text onPress={handlePress} style={styles.footer} color={theme['c-font-label']}>{t(label)}</Text>\n          </View>\n        )\n      : null\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingLeft: 15,\n    paddingRight: 15,\n  },\n  footer: {\n    textAlign: 'center',\n    padding: 10,\n  },\n})\n\nexport default List\n"
  },
  {
    "path": "src/screens/Comment/index.tsx",
    "content": "import { memo, useMemo, useEffect, useRef, useState, useCallback } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\nimport PagerView, { type PagerViewOnPageSelectedEvent } from 'react-native-pager-view'\nimport Header from './components/Header'\nimport { Icon } from '@/components/common/Icon'\nimport CommentHot from './CommentHot'\nimport CommentNew from './CommentNew'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { setComponentId } from '@/core/common'\nimport PageContent from '@/components/PageContent'\nimport playerState from '@/store/player/state'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { BorderWidths } from '@/theme'\n\ntype ActiveId = 'hot' | 'new'\n\nconst BAR_HEIGHT = scaleSizeH(34)\n\nconst HeaderItem = ({ id, label, isActive, onPress }: {\n  id: ActiveId\n  label: string\n  isActive: boolean\n  onPress: (id: ActiveId) => void\n}) => {\n  const theme = useTheme()\n  // console.log(theme)\n  const components = useMemo(() => (\n    <TouchableOpacity style={styles.tabBtn} onPress={() => { !isActive && onPress(id) }}>\n      <Text color={isActive ? theme['c-primary-font-active'] : theme['c-font']}>{label}</Text>\n    </TouchableOpacity>\n  ), [isActive, theme, label, onPress, id])\n\n  return components\n}\n\nconst HotCommentPage = memo(({ activeId, musicInfo, onUpdateTotal }: {\n  activeId: ActiveId\n  musicInfo: LX.Music.MusicInfoOnline\n  onUpdateTotal: (total: number) => void\n}) => {\n  const initedRef = useRef(false)\n  const comment = useMemo(() => <CommentHot musicInfo={musicInfo} onUpdateTotal={onUpdateTotal} />, [musicInfo, onUpdateTotal])\n  switch (activeId) {\n    case 'hot':\n      if (!initedRef.current) initedRef.current = true\n      return comment\n    default:\n      return initedRef.current ? comment : null\n  }\n})\n\nconst NewCommentPage = memo(({ activeId, musicInfo, onUpdateTotal }: {\n  activeId: ActiveId\n  musicInfo: LX.Music.MusicInfoOnline\n  onUpdateTotal: (total: number) => void\n}) => {\n  const initedRef = useRef(false)\n  const comment = useMemo(() => <CommentNew musicInfo={musicInfo} onUpdateTotal={onUpdateTotal} />, [musicInfo, onUpdateTotal])\n  switch (activeId) {\n    case 'new':\n      if (!initedRef.current) initedRef.current = true\n      return comment\n    default:\n      return initedRef.current ? comment : null\n  }\n})\n\nconst TABS = [\n  'hot',\n  'new',\n] as const\nconst getMusicInfo = (musicInfo: LX.Player.PlayMusic | null) => {\n  if (!musicInfo) return null\n  return 'progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo\n}\nexport default memo(({ componentId }: {\n  componentId: string\n}) => {\n  const pagerViewRef = useRef<PagerView>(null)\n  const [activeId, setActiveId] = useState<ActiveId>('hot')\n  const [musicInfo, setMusicInfo] = useState<LX.Music.MusicInfo | null>(getMusicInfo(playerState.playMusicInfo.musicInfo))\n  const t = useI18n()\n  const theme = useTheme()\n  const [total, setTotal] = useState({ hot: 0, new: 0 })\n\n  useEffect(() => {\n    setComponentId(COMPONENT_IDS.comment, componentId)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  const tabs = useMemo(() => {\n    return [\n      { id: TABS[0], label: t('comment_tab_hot', { total: total.hot ? `(${total.hot})` : '' }) },\n      { id: TABS[1], label: t('comment_tab_new', { total: total.new ? `(${total.new})` : '' }) },\n    ] as const\n  }, [total, t])\n\n  const toggleTab = useCallback((id: ActiveId) => {\n    setActiveId(id)\n    pagerViewRef.current?.setPage(TABS.findIndex(tab => tab == id))\n  }, [])\n\n  const onPageSelected = useCallback(({ nativeEvent }: PagerViewOnPageSelectedEvent) => {\n    setActiveId(TABS[nativeEvent.position])\n  }, [])\n\n  const refreshComment = useCallback(() => {\n    if (!playerState.playMusicInfo.musicInfo) return\n    let playerMusicInfo = playerState.playMusicInfo.musicInfo\n    if ('progress' in playerMusicInfo) playerMusicInfo = playerMusicInfo.metadata.musicInfo\n\n    if (musicInfo && musicInfo.id == playerMusicInfo.id) {\n      toast(t('comment_refresh', { name: musicInfo.name }))\n      return\n    }\n    setMusicInfo(playerMusicInfo)\n  }, [musicInfo, t])\n\n  const setHotTotal = useCallback((total: number) => {\n    setTotal(totalInfo => ({ ...totalInfo, hot: total }))\n  }, [])\n  const setNewTotal = useCallback((total: number) => {\n    setTotal(totalInfo => ({ ...totalInfo, new: total }))\n  }, [])\n\n  const commentComponent = useMemo(() => {\n    return (\n      <View style={styles.container}>\n        <View style={{ ...styles.tabHeader, borderBottomColor: theme['c-border-background'], height: BAR_HEIGHT }}>\n          <View style={styles.left}>\n            {tabs.map(({ id, label }) => <HeaderItem id={id} label={label} key={id} isActive={activeId == id} onPress={toggleTab} />)}\n          </View>\n          <View>\n            <TouchableOpacity onPress={refreshComment} style={{ ...styles.btn, width: BAR_HEIGHT }}>\n              <Icon name=\"available_updates\" size={20} color={theme['c-600']} />\n            </TouchableOpacity>\n          </View>\n        </View>\n        <PagerView\n          ref={pagerViewRef}\n          onPageSelected={onPageSelected}\n          // onPageScrollStateChanged={onPageScrollStateChanged}\n          style={styles.pagerView}\n        >\n          <View collapsable={false} style={styles.pageStyle}>\n            <HotCommentPage activeId={activeId} musicInfo={musicInfo as LX.Music.MusicInfoOnline} onUpdateTotal={setHotTotal} />\n          </View>\n          <View collapsable={false} style={styles.pageStyle}>\n            <NewCommentPage activeId={activeId} musicInfo={musicInfo as LX.Music.MusicInfoOnline} onUpdateTotal={setNewTotal} />\n          </View>\n        </PagerView>\n      </View>\n    )\n  }, [activeId, musicInfo, onPageSelected, refreshComment, setHotTotal, setNewTotal, tabs, theme, toggleTab])\n\n  return (\n    <PageContent>\n      {\n        musicInfo == null\n          ? null\n          : <>\n            <Header musicInfo={musicInfo} />\n            {\n              musicInfo.source == 'local'\n                ? (\n                <View style={{ ...styles.container, alignItems: 'center', justifyContent: 'center' }}>\n                  <Text>{t('comment_not support')}</Text>\n                </View>\n                  )\n                : commentComponent\n            }\n        </>\n      }\n\n    </PageContent>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n  },\n  tabHeader: {\n    flexDirection: 'row',\n    // paddingLeft: 10,\n    paddingRight: 10,\n    // justifyContent: 'center',\n    borderBottomWidth: BorderWidths.normal,\n  },\n  left: {\n    flex: 1,\n    flexDirection: 'row',\n    paddingLeft: 5,\n  },\n  tabBtn: {\n    // flex: 1,\n    paddingLeft: 10,\n    paddingRight: 10,\n    alignItems: 'center',\n    justifyContent: 'center',\n    height: '100%',\n  },\n  btn: {\n    // flex: 1,\n    alignItems: 'center',\n    justifyContent: 'center',\n    height: '100%',\n  },\n  pagerView: {\n    flex: 1,\n  },\n  pageStyle: {\n    overflow: 'hidden',\n  },\n})\n"
  },
  {
    "path": "src/screens/Comment/utils.ts",
    "content": "import { toOldMusicInfo } from '@/utils'\nimport music from '@/utils/musicSdk'\n\nexport interface Comment {\n  id: string\n  text: string\n  images?: string[]\n  location?: string\n  timeStr?: string\n  userName: string\n  avatar?: string\n  userId?: string\n  likedCount?: number\n  replyNum?: number\n  reply: Comment[]\n}\nexport interface CommentInfo {\n  source: LX.OnlineSource\n  comments: Comment[]\n  total: number\n  page: number\n  limit: number\n  maxPage: number\n}\n\nexport const getNewComment = async(musicInfo: LX.Music.MusicInfoOnline, page: number, limit: number, retryNum = 0): Promise<CommentInfo> => {\n  let resp\n  try {\n    resp = await (music[musicInfo.source].comment.getComment(toOldMusicInfo(musicInfo), page, limit) as Promise<CommentInfo>)\n  } catch (error: any) {\n    console.log(error.message)\n    if (error.message == '取消请求' || ++retryNum > 2) throw error\n    resp = await getNewComment(musicInfo, page, limit, retryNum)\n  }\n  return resp\n}\n\nexport const getHotComment = async(musicInfo: LX.Music.MusicInfoOnline, page: number, limit: number, retryNum = 0): Promise<CommentInfo> => {\n  let resp\n  try {\n    resp = await (music[musicInfo.source].comment.getHotComment(toOldMusicInfo(musicInfo), page, limit) as Promise<CommentInfo>)\n  } catch (error: any) {\n    console.log(error.message)\n    if (error.message == '取消请求' || ++retryNum > 2) throw error\n    resp = await getHotComment(musicInfo, page, limit, retryNum)\n  }\n  return resp\n}\n\nexport const filterList = (list: Comment[]) => {\n  const set = new Set()\n  return list.filter(c => {\n    let id = String(c.id)\n    if (set.has(id)) return false\n    set.add(id)\n    return true\n  })\n}\n"
  },
  {
    "path": "src/screens/Home/Horizontal/Aside.tsx",
    "content": "import { memo } from 'react'\nimport { ScrollView, TouchableOpacity, View } from 'react-native'\nimport { useNavActiveId, useStatusbarHeight } from '@/store/common/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { Icon } from '@/components/common/Icon'\nimport { confirmDialog, createStyle, exitApp as backHome } from '@/utils/tools'\nimport { NAV_MENUS } from '@/config/constant'\nimport type { InitState } from '@/store/common/state'\n// import commonState from '@/store/common/state'\nimport { exitApp, setNavActiveId } from '@/core/common'\nimport { BorderWidths } from '@/theme'\nimport { useSettingValue } from '@/store/setting/hook'\n\nconst NAV_WIDTH = 68\n\nconst styles = createStyle({\n  container: {\n    flexGrow: 0,\n    // flex: 1,\n    // alignItems: 'center',\n    // justifyContent: 'center',\n    // padding: 10,\n    borderRightWidth: BorderWidths.normal,\n    paddingBottom: 10,\n    width: NAV_WIDTH,\n  },\n  header: {\n    paddingTop: 15,\n    paddingBottom: 15,\n    flexDirection: 'row',\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n  headerText: {\n    textAlign: 'center',\n    marginLeft: 16,\n  },\n  menus: {\n    flex: 1,\n  },\n  list: {\n    // paddingTop: 10,\n    paddingBottom: 15,\n  },\n  menuItem: {\n    flexDirection: 'row',\n    paddingTop: 15,\n    paddingBottom: 15,\n    // paddingLeft: 25,\n    // paddingRight: 25,\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  iconContent: {\n    // width: 24,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n    alignItems: 'center',\n  },\n  text: {\n    paddingLeft: 15,\n    // fontWeight: '500',\n  },\n})\n\nconst Header = () => {\n  const theme = useTheme()\n  const statusBarHeight = useStatusbarHeight()\n  return (\n    <View style={{ paddingTop: statusBarHeight }}>\n      <View style={styles.header}>\n        <Icon name=\"logo\" color={theme['c-primary-dark-100-alpha-300']} size={22} />\n        {/* <Text style={styles.headerText} size={16} color={theme['c-primary-dark-100-alpha-300']}>LX Music</Text> */}\n      </View>\n    </View>\n  )\n}\n\ntype IdType = InitState['navActiveId'] | 'nav_exit' | 'back_home'\n\nconst MenuItem = ({ id, icon, onPress }: {\n  id: IdType\n  icon: string\n  onPress: (id: IdType) => void\n}) => {\n  // const t = useI18n()\n  const activeId = useNavActiveId()\n  const theme = useTheme()\n\n  return activeId == id\n    ? <View style={styles.menuItem}>\n        <View style={styles.iconContent}>\n          <Icon name={icon} size={20} color={theme['c-primary-font-active']} />\n        </View>\n        {/* <Text style={styles.text} size={14} color={theme['c-primary-font']}>{t(id)}</Text> */}\n      </View>\n    : <TouchableOpacity style={styles.menuItem} onPress={() => { onPress(id) }}>\n        <View style={styles.iconContent}>\n          <Icon name={icon} size={20} color={theme['c-font-label']} />\n        </View>\n        {/* <Text style={styles.text} size={14}>{t(id)}</Text> */}\n      </TouchableOpacity>\n}\n\nexport default memo(() => {\n  const theme = useTheme()\n  // console.log('render drawer nav')\n  const showBackBtn = useSettingValue('common.showBackBtn')\n  const showExitBtn = useSettingValue('common.showExitBtn')\n\n  const handlePress = (id: IdType) => {\n    switch (id) {\n      case 'nav_exit':\n        void confirmDialog({\n          message: global.i18n.t('exit_app_tip'),\n          confirmButtonText: global.i18n.t('list_remove_tip_button'),\n        }).then(isExit => {\n          if (!isExit) return\n          exitApp('Exit Btn')\n        })\n        return\n      case 'back_home':\n        backHome()\n        return\n    }\n\n    global.app_event.changeMenuVisible(false)\n    setNavActiveId(id)\n  }\n\n  return (\n    <View style={{ ...styles.container, borderRightColor: theme['c-border-background'] }}>\n      <Header />\n      <ScrollView style={styles.menus}>\n        <View style={styles.list}>\n          {NAV_MENUS.map(menu => <MenuItem key={menu.id} id={menu.id} icon={menu.icon} onPress={handlePress} />)}\n        </View>\n      </ScrollView>\n      {\n        showBackBtn ? <MenuItem id=\"back_home\" icon=\"home\" onPress={handlePress} /> : null\n      }\n      {\n        showExitBtn ? <MenuItem id=\"nav_exit\" icon=\"exit2\" onPress={handlePress} /> : null\n      }\n    </View>\n  )\n})\n\n"
  },
  {
    "path": "src/screens/Home/Horizontal/Header.tsx",
    "content": "import { View } from 'react-native'\n// import Button from '@/components/common/Button'\n// import { navigations } from '@/navigation'\n// import { BorderWidths } from '@/theme'\nimport { useNavActiveId, useStatusbarHeight } from '@/store/common/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport StatusBar from '@/components/common/StatusBar'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { HEADER_HEIGHT as _HEADER_HEIGHT } from '@/config/constant'\nimport { type InitState as CommonState } from '@/store/common/state'\nimport SearchTypeSelector from '@/screens/Home/Views/Search/SearchTypeSelector'\n\nconst headerComponents: Partial<Record<CommonState['navActiveId'], React.ReactNode>> = {\n  nav_search: <SearchTypeSelector />,\n}\n\nconst HEADER_HEIGHT = _HEADER_HEIGHT * 0.8\n\n\n// const LeftTitle = () => {\n//   const id = useNavActiveId()\n//   const t = useI18n()\n\n//   return <Text style={styles.leftTitle} size={18}>{t(id)}</Text>\n// }\nconst LeftHeader = () => {\n  const id = useNavActiveId()\n  const t = useI18n()\n  const statusBarHeight = useStatusbarHeight()\n\n  return (\n    <View style={{\n      ...styles.container,\n      height: scaleSizeH(HEADER_HEIGHT) + statusBarHeight,\n      paddingTop: statusBarHeight,\n    }}>\n      <View style={styles.left}>\n        <Text style={styles.leftTitle} size={18}>{t(id)}</Text>\n      </View>\n      {headerComponents[id] ?? null}\n\n      {/* <TouchableOpacity style={styles.btn} onPress={openSetting}>\n        <Icon style={{ ...styles.btnText, color: theme['c-font'] }} name=\"setting\" size={styles.btnText.fontSize} />\n      </TouchableOpacity> */}\n    </View>\n  )\n}\n\n\n// const RightTitle = () => {\n//   const id = useNavActiveId()\n//   const t = useI18n()\n\n//   return <Text style={styles.rightTitle} size={18}>{t(id)}</Text>\n// }\nconst RightHeader = () => {\n  const t = useI18n()\n  const id = useNavActiveId()\n  const statusBarHeight = useStatusbarHeight()\n\n  return (\n    <View style={{\n      ...styles.container,\n      height: scaleSizeH(HEADER_HEIGHT) + statusBarHeight,\n      paddingTop: statusBarHeight,\n    }}>\n      <View style={styles.left}>\n        <Text style={styles.rightTitle} size={18}>{t(id)}</Text>\n      </View>\n      {headerComponents[id] ?? null}\n      {/* <TouchableOpacity style={styles.btn} onPress={openSetting}>\n        <Icon style={{ ...styles.btnText, color: theme['c-font'] }} name=\"setting\" size={styles.btnText.fontSize} />\n      </TouchableOpacity> */}\n    </View>\n  )\n}\n\nconst Header = () => {\n  const drawerLayoutPosition = useSettingValue('common.drawerLayoutPosition')\n\n  return (\n    <>\n      <StatusBar />\n      {\n        drawerLayoutPosition == 'left'\n          ? <LeftHeader />\n          : <RightHeader />\n      }\n\n    </>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    // width: '100%',\n    paddingRight: 5,\n    flexDirection: 'row',\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    zIndex: 10,\n  },\n  left: {\n    flex: 1,\n    flexDirection: 'row',\n    paddingLeft: 5,\n    alignItems: 'center',\n    height: '100%',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  btn: {\n    // flex: 1,\n    width: HEADER_HEIGHT,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    alignItems: 'center',\n    justifyContent: 'center',\n    height: '100%',\n  },\n  titleBtn: {\n    flex: 1,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    height: '100%',\n    justifyContent: 'center',\n  },\n  leftTitle: {\n    paddingLeft: 10,\n    paddingRight: 16,\n  },\n  rightTitle: {\n    paddingLeft: 16,\n    paddingRight: 16,\n  },\n})\n\nexport default Header\n"
  },
  {
    "path": "src/screens/Home/Horizontal/Main.tsx",
    "content": "import { useEffect, useMemo, useState } from 'react'\nimport Search from '../Views/Search'\nimport SongList from '../Views/SongList'\nimport Mylist from '../Views/Mylist'\nimport Leaderboard from '../Views/Leaderboard'\nimport Setting from '../Views/Setting'\nimport commonState, { type InitState as CommonState } from '@/store/common/state'\n\n\nconst Main = () => {\n  const [id, setId] = useState(commonState.navActiveId)\n\n  useEffect(() => {\n    const handleUpdate = (id: CommonState['navActiveId']) => {\n      requestAnimationFrame(() => {\n        setId(id)\n      })\n    }\n    global.state_event.on('navActiveIdUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleUpdate)\n    }\n  }, [])\n\n  const component = useMemo(() => {\n    switch (id) {\n      case 'nav_songlist': return <SongList />\n      case 'nav_top': return <Leaderboard />\n      case 'nav_love': return <Mylist />\n      case 'nav_setting': return <Setting />\n      case 'nav_search':\n      default: return <Search />\n    }\n  }, [id])\n\n  return component\n}\n\n\nexport default Main\n\n"
  },
  {
    "path": "src/screens/Home/Horizontal/index.tsx",
    "content": "import { View } from 'react-native'\nimport Aside from './Aside'\nimport PlayerBar from '@/components/player/PlayerBar'\nimport StatusBar from '@/components/common/StatusBar'\nimport Header from './Header'\nimport Main from './Main'\nimport { createStyle } from '@/utils/tools'\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    flexDirection: 'row',\n  },\n  content: {\n    flex: 1,\n    overflow: 'hidden',\n  },\n})\n\nexport default () => {\n  return (\n    <>\n      <StatusBar />\n      <View style={styles.container}>\n        <Aside />\n        <View style={styles.content}>\n          <Header />\n          <Main />\n          <PlayerBar isHome />\n        </View>\n      </View>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Vertical/Content.tsx",
    "content": "import { useEffect, useRef } from 'react'\n// import { getWindowSise, onDimensionChange } from '@/utils/tools'\nimport DrawerNav from './DrawerNav'\nimport Header from './Header'\nimport Main from './Main'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport DrawerLayoutFixed, { type DrawerLayoutFixedType } from '@/components/common/DrawerLayoutFixed'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nconst MAX_WIDTH = scaleSizeW(300)\n\nconst Content = () => {\n  const drawer = useRef<DrawerLayoutFixedType>(null)\n  const drawerLayoutPosition = useSettingValue('common.drawerLayoutPosition')\n\n  useEffect(() => {\n    const changeVisible = (visible: boolean) => {\n      if (visible) {\n        drawer.current?.openDrawer()\n      } else {\n        drawer.current?.closeDrawer()\n      }\n    }\n\n    global.app_event.on('changeMenuVisible', changeVisible)\n\n    return () => {\n      global.app_event.off('changeMenuVisible', changeVisible)\n    }\n  }, [])\n\n  const navigationView = () => <DrawerNav />\n  // console.log('render drawer content')\n\n  return (\n    <DrawerLayoutFixed\n      ref={drawer}\n      widthPercentage={0.7}\n      widthPercentageMax={MAX_WIDTH}\n      visibleNavNames={[COMPONENT_IDS.home]}\n      // drawerWidth={width}\n      drawerPosition={drawerLayoutPosition}\n      renderNavigationView={navigationView}\n    >\n      <Header />\n      <Main />\n      {/* <View style={styles.container}>\n      </View> */}\n    </DrawerLayoutFixed>\n  )\n}\n\n// const styles = createStyle({\n//   container: {\n//     flex: 1,\n//   },\n// })\n\nexport default Content\n"
  },
  {
    "path": "src/screens/Home/Vertical/DrawerNav.tsx",
    "content": "import { memo } from 'react'\nimport { ScrollView, TouchableOpacity, View } from 'react-native'\nimport { useI18n } from '@/lang'\nimport { useNavActiveId, useStatusbarHeight } from '@/store/common/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { Icon } from '@/components/common/Icon'\nimport { confirmDialog, createStyle, exitApp as backHome } from '@/utils/tools'\nimport { NAV_MENUS } from '@/config/constant'\nimport type { InitState } from '@/store/common/state'\n// import { navigations } from '@/navigation'\n// import commonState from '@/store/common/state'\nimport { exitApp, setNavActiveId } from '@/core/common'\nimport Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    // alignItems: 'center',\n    // justifyContent: 'center',\n    // padding: 10,\n  },\n  header: {\n    paddingTop: 40,\n    paddingBottom: 50,\n    flexDirection: 'row',\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n  headerText: {\n    textAlign: 'center',\n    marginLeft: 16,\n  },\n  menus: {\n    flex: 1,\n  },\n  list: {\n    paddingTop: 10,\n    paddingBottom: 10,\n  },\n  menuItem: {\n    flexDirection: 'row',\n    paddingTop: 13,\n    paddingBottom: 13,\n    paddingLeft: 25,\n    paddingRight: 25,\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  iconContent: {\n    width: 24,\n    alignItems: 'center',\n  },\n  text: {\n    paddingLeft: 20,\n    // fontWeight: '500',\n  },\n})\n\nconst Header = () => {\n  const theme = useTheme()\n  const statusBarHeight = useStatusbarHeight()\n  return (\n    <View style={{ paddingTop: statusBarHeight, backgroundColor: theme['c-primary-light-700-alpha-500'] }}>\n      <View style={styles.header}>\n        <Icon name=\"logo\" color={theme['c-primary-dark-100-alpha-300']} size={28} />\n        <Text style={styles.headerText} size={28} color={theme['c-primary-dark-100-alpha-300']}>LX Music</Text>\n      </View>\n    </View>\n  )\n}\n\ntype IdType = InitState['navActiveId'] | 'nav_exit' | 'back_home'\n\nconst MenuItem = ({ id, icon, onPress }: {\n  id: IdType\n  icon: string\n  onPress: (id: IdType) => void\n}) => {\n  const t = useI18n()\n  const activeId = useNavActiveId()\n  const theme = useTheme()\n\n  return activeId == id\n    ? <View style={styles.menuItem}>\n        <View style={styles.iconContent}>\n          <Icon name={icon} size={20} color={theme['c-primary-font-active']} />\n        </View>\n        <Text style={styles.text} color={theme['c-primary-font']}>{t(id)}</Text>\n      </View>\n    : <TouchableOpacity style={styles.menuItem} onPress={() => { onPress(id) }}>\n        <View style={styles.iconContent}>\n          <Icon name={icon} size={20} color={theme['c-font-label']} />\n        </View>\n        <Text style={styles.text}>{t(id)}</Text>\n      </TouchableOpacity>\n}\n\nexport default memo(() => {\n  const theme = useTheme()\n  // console.log('render drawer nav')\n  const showBackBtn = useSettingValue('common.showBackBtn')\n  const showExitBtn = useSettingValue('common.showExitBtn')\n\n  const handlePress = (id: IdType) => {\n    switch (id) {\n      case 'nav_exit':\n        void confirmDialog({\n          message: global.i18n.t('exit_app_tip'),\n          confirmButtonText: global.i18n.t('list_remove_tip_button'),\n        }).then(isExit => {\n          if (!isExit) return\n          exitApp('Exit Btn')\n        })\n        return\n      case 'back_home':\n        backHome()\n        return\n    }\n\n    global.app_event.changeMenuVisible(false)\n    setNavActiveId(id)\n  }\n\n\n  return (\n    <View style={{ ...styles.container, backgroundColor: theme['c-content-background'] }}>\n      <Header />\n      <ScrollView style={styles.menus}>\n        <View style={styles.list}>\n          {NAV_MENUS.map(menu => <MenuItem key={menu.id} id={menu.id} icon={menu.icon} onPress={handlePress} />)}\n        </View>\n      </ScrollView>\n\n      {\n        showBackBtn ? <MenuItem id=\"back_home\" icon=\"home\" onPress={handlePress} /> : null\n      }\n      {\n        showExitBtn ? <MenuItem id=\"nav_exit\" icon=\"exit2\" onPress={handlePress} /> : null\n      }\n    </View>\n  )\n})\n\n"
  },
  {
    "path": "src/screens/Home/Vertical/Header.tsx",
    "content": "import { View, TouchableOpacity } from 'react-native'\n// import Button from '@/components/common/Button'\n// import { navigations } from '@/navigation'\n// import { BorderWidths } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\nimport { useNavActiveId, useStatusbarHeight } from '@/store/common/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { Icon } from '@/components/common/Icon'\nimport Text from '@/components/common/Text'\nimport StatusBar from '@/components/common/StatusBar'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { HEADER_HEIGHT } from '@/config/constant'\nimport { type InitState as CommonState } from '@/store/common/state'\nimport SearchTypeSelector from '@/screens/Home/Views/Search/SearchTypeSelector'\n\nconst headerComponents: Partial<Record<CommonState['navActiveId'], React.ReactNode>> = {\n  nav_search: <SearchTypeSelector />,\n}\n\n\n// const LeftTitle = () => {\n//   const id = useNavActiveId()\n//   const t = useI18n()\n\n//   return <Text style={styles.leftTitle} size={18}>{t(id)}</Text>\n// }\nconst LeftHeader = () => {\n  const theme = useTheme()\n  const id = useNavActiveId()\n  const t = useI18n()\n  const statusBarHeight = useStatusbarHeight()\n\n  const openMenu = () => {\n    global.app_event.changeMenuVisible(true)\n  }\n\n  return (\n    <View style={{\n      ...styles.container,\n      height: scaleSizeH(HEADER_HEIGHT) + statusBarHeight,\n      paddingTop: statusBarHeight,\n    }}>\n      <View style={styles.left}>\n        <TouchableOpacity style={styles.btn} onPress={openMenu}>\n          <Icon color={theme['c-font']} name=\"menu\" size={18} />\n        </TouchableOpacity>\n        <TouchableOpacity style={styles.titleBtn} onPress={openMenu}>\n          <Text style={styles.leftTitle} size={18}>{t(id)}</Text>\n        </TouchableOpacity>\n      </View>\n      {headerComponents[id] ?? null}\n\n      {/* <TouchableOpacity style={styles.btn} onPress={openSetting}>\n        <Icon style={{ ...styles.btnText, color: theme['c-font'] }} name=\"setting\" size={styles.btnText.fontSize} />\n      </TouchableOpacity> */}\n    </View>\n  )\n}\n\n\n// const RightTitle = () => {\n//   const id = useNavActiveId()\n//   const t = useI18n()\n\n//   return <Text style={styles.rightTitle} size={18}>{t(id)}</Text>\n// }\nconst RightHeader = () => {\n  const theme = useTheme()\n  const t = useI18n()\n  const id = useNavActiveId()\n  const statusBarHeight = useStatusbarHeight()\n\n  const openMenu = () => {\n    global.app_event.changeMenuVisible(true)\n  }\n  return (\n    <View style={{\n      ...styles.container,\n      height: scaleSizeH(HEADER_HEIGHT) + statusBarHeight,\n      paddingTop: statusBarHeight,\n    }}>\n      <View style={styles.left}>\n        <TouchableOpacity style={styles.titleBtn} onPress={openMenu}>\n          <Text style={styles.rightTitle} size={18}>{t(id)}</Text>\n        </TouchableOpacity>\n      </View>\n      {headerComponents[id] ?? null}\n      <TouchableOpacity style={styles.btn} onPress={openMenu}>\n        <Icon color={theme['c-font']} name=\"menu\" size={18} />\n      </TouchableOpacity>\n      {/* <TouchableOpacity style={styles.btn} onPress={openSetting}>\n        <Icon style={{ ...styles.btnText, color: theme['c-font'] }} name=\"setting\" size={styles.btnText.fontSize} />\n      </TouchableOpacity> */}\n    </View>\n  )\n}\n\nconst Header = () => {\n  const drawerLayoutPosition = useSettingValue('common.drawerLayoutPosition')\n\n  return (\n    <>\n      <StatusBar />\n      {\n        drawerLayoutPosition == 'left'\n          ? <LeftHeader />\n          : <RightHeader />\n      }\n\n    </>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    // width: '100%',\n    paddingRight: 5,\n    flexDirection: 'row',\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    zIndex: 10,\n  },\n  left: {\n    flex: 1,\n    flexDirection: 'row',\n    paddingLeft: 5,\n    alignItems: 'center',\n    height: '100%',\n  },\n  btn: {\n    // flex: 1,\n    width: HEADER_HEIGHT,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    alignItems: 'center',\n    justifyContent: 'center',\n    height: '100%',\n  },\n  titleBtn: {\n    flex: 1,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    height: '100%',\n    justifyContent: 'center',\n  },\n  leftTitle: {\n    paddingLeft: 14,\n    paddingRight: 16,\n  },\n  rightTitle: {\n    paddingLeft: 16,\n    paddingRight: 16,\n  },\n})\n\nexport default Header\n"
  },
  {
    "path": "src/screens/Home/Vertical/Main.tsx",
    "content": "import { useCallback, useEffect, useMemo, useRef, useState, type ComponentRef } from 'react'\nimport { View } from 'react-native'\nimport Search from '../Views/Search'\nimport SongList from '../Views/SongList'\nimport Mylist from '../Views/Mylist'\nimport Leaderboard from '../Views/Leaderboard'\nimport Setting from '../Views/Setting'\nimport commonState, { type InitState as CommonState } from '@/store/common/state'\nimport { createStyle } from '@/utils/tools'\nimport PagerView, { type PageScrollStateChangedNativeEvent, type PagerViewOnPageSelectedEvent } from 'react-native-pager-view'\nimport { setNavActiveId } from '@/core/common'\nimport settingState from '@/store/setting/state'\n\nconst hideKeys = [\n  'list.isShowAlbumName',\n  'list.isShowInterval',\n  'theme.fontShadow',\n] as Readonly<Array<keyof LX.AppSetting>>\n\nconst SearchPage = () => {\n  const [visible, setVisible] = useState(commonState.navActiveId == 'nav_search')\n  const component = useMemo(() => <Search />, [])\n  useEffect(() => {\n    let currentId: CommonState['navActiveId'] = commonState.navActiveId\n    const handleNavIdUpdate = (id: CommonState['navActiveId']) => {\n      currentId = id\n      if (id == 'nav_search') {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n      }\n    }\n    const handleHide = () => {\n      if (currentId != 'nav_setting') return\n      setVisible(false)\n    }\n    const handleConfigUpdated = (keys: Array<keyof LX.AppSetting>) => {\n      if (keys.some(k => hideKeys.includes(k))) handleHide()\n    }\n    global.state_event.on('navActiveIdUpdated', handleNavIdUpdate)\n    global.state_event.on('themeUpdated', handleHide)\n    global.state_event.on('languageChanged', handleHide)\n    global.state_event.on('configUpdated', handleConfigUpdated)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleNavIdUpdate)\n      global.state_event.off('themeUpdated', handleHide)\n      global.state_event.off('languageChanged', handleHide)\n      global.state_event.off('configUpdated', handleConfigUpdated)\n    }\n  }, [])\n\n  return visible ? component : null\n}\nconst SongListPage = () => {\n  const [visible, setVisible] = useState(commonState.navActiveId == 'nav_songlist')\n  const component = useMemo(() => <SongList />, [])\n  useEffect(() => {\n    let currentId: CommonState['navActiveId'] = commonState.navActiveId\n    const handleNavIdUpdate = (id: CommonState['navActiveId']) => {\n      currentId = id\n      if (id == 'nav_songlist') {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n      }\n    }\n    const handleHide = () => {\n      if (currentId != 'nav_setting') return\n      setVisible(false)\n    }\n    const handleConfigUpdated = (keys: Array<keyof LX.AppSetting>) => {\n      if (keys.some(k => hideKeys.includes(k))) handleHide()\n    }\n    global.state_event.on('navActiveIdUpdated', handleNavIdUpdate)\n    global.state_event.on('themeUpdated', handleHide)\n    global.state_event.on('languageChanged', handleHide)\n    global.state_event.on('configUpdated', handleConfigUpdated)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleNavIdUpdate)\n      global.state_event.off('themeUpdated', handleHide)\n      global.state_event.off('languageChanged', handleHide)\n      global.state_event.on('configUpdated', handleConfigUpdated)\n    }\n  }, [])\n\n  return visible ? component : null\n  // return activeId == 1 || activeId == 0  ? SongList : null\n}\nconst LeaderboardPage = () => {\n  const [visible, setVisible] = useState(commonState.navActiveId == 'nav_top')\n  const component = useMemo(() => <Leaderboard />, [])\n  useEffect(() => {\n    let currentId: CommonState['navActiveId'] = commonState.navActiveId\n    const handleNavIdUpdate = (id: CommonState['navActiveId']) => {\n      currentId = id\n      if (id == 'nav_top') {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n      }\n    }\n    const handleHide = () => {\n      if (currentId != 'nav_setting') return\n      setVisible(false)\n    }\n    const handleConfigUpdated = (keys: Array<keyof LX.AppSetting>) => {\n      if (keys.some(k => hideKeys.includes(k))) handleHide()\n    }\n    global.state_event.on('navActiveIdUpdated', handleNavIdUpdate)\n    global.state_event.on('themeUpdated', handleHide)\n    global.state_event.on('languageChanged', handleHide)\n    global.state_event.on('configUpdated', handleConfigUpdated)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleNavIdUpdate)\n      global.state_event.off('themeUpdated', handleHide)\n      global.state_event.off('languageChanged', handleHide)\n      global.state_event.on('configUpdated', handleConfigUpdated)\n    }\n  }, [])\n\n  return visible ? component : null\n}\nconst MylistPage = () => {\n  const [visible, setVisible] = useState(commonState.navActiveId == 'nav_love')\n  const component = useMemo(() => <Mylist />, [])\n  useEffect(() => {\n    let currentId: CommonState['navActiveId'] = commonState.navActiveId\n    const handleNavIdUpdate = (id: CommonState['navActiveId']) => {\n      currentId = id\n      if (id == 'nav_love') {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n      }\n    }\n    const handleHide = () => {\n      if (currentId != 'nav_setting') return\n      setVisible(false)\n    }\n    const handleConfigUpdated = (keys: Array<keyof LX.AppSetting>) => {\n      if (keys.some(k => hideKeys.includes(k))) handleHide()\n    }\n    global.state_event.on('navActiveIdUpdated', handleNavIdUpdate)\n    global.state_event.on('themeUpdated', handleHide)\n    global.state_event.on('languageChanged', handleHide)\n    global.state_event.on('configUpdated', handleConfigUpdated)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleNavIdUpdate)\n      global.state_event.off('themeUpdated', handleHide)\n      global.state_event.off('languageChanged', handleHide)\n      global.state_event.on('configUpdated', handleConfigUpdated)\n    }\n  }, [])\n\n  return visible ? component : null\n}\nconst SettingPage = () => {\n  const [visible, setVisible] = useState(commonState.navActiveId == 'nav_setting')\n  const component = useMemo(() => <Setting />, [])\n  useEffect(() => {\n    const handleNavIdUpdate = (id: CommonState['navActiveId']) => {\n      if (id == 'nav_setting') {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n      }\n    }\n    global.state_event.on('navActiveIdUpdated', handleNavIdUpdate)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleNavIdUpdate)\n    }\n  }, [])\n  return visible ? component : null\n}\n\nconst viewMap = {\n  nav_search: 0,\n  nav_songlist: 1,\n  nav_top: 2,\n  nav_love: 3,\n  nav_setting: 4,\n}\nconst indexMap = [\n  'nav_search',\n  'nav_songlist',\n  'nav_top',\n  'nav_love',\n  'nav_setting',\n] as const\n\nconst Main = () => {\n  const pagerViewRef = useRef<ComponentRef<typeof PagerView>>(null)\n  let activeIndexRef = useRef(viewMap[commonState.navActiveId])\n  // const isScrollingRef = useRef(false)\n  // const scrollPositionRef = useRef(-1)\n\n  // const handlePageScroll = useCallback(({ nativeEvent }) => {\n  //   console.log(nativeEvent.offset, activeIndexRef.current)\n  //   // if (activeIndexRef.current == -1) return\n  //   // if (nativeEvent.offset == 0) {\n  //   //   isScrollingRef.current = false\n\n  //   //   const index = nativeEvent.position\n  //   //   if (activeIndexRef.current == index) return\n  //   //   activeIndexRef.current = index\n  //   //   setNavActiveIndex(index)\n  //   // } else if (!isScrollingRef.current) {\n  //   //   isScrollingRef.current = true\n  //   // }\n  // }, [setNavActiveIndex])\n\n  const onPageSelected = useCallback(({ nativeEvent }: PagerViewOnPageSelectedEvent) => {\n    // console.log(nativeEvent)\n    activeIndexRef.current = nativeEvent.position\n    if (activeIndexRef.current != viewMap[commonState.navActiveId]) {\n      setNavActiveId(indexMap[activeIndexRef.current])\n    }\n  }, [])\n\n  const onPageScrollStateChanged = useCallback(({ nativeEvent }: PageScrollStateChangedNativeEvent) => {\n    // console.log(nativeEvent)\n    const idle = nativeEvent.pageScrollState == 'idle'\n    if (global.lx.homePagerIdle != idle) global.lx.homePagerIdle = idle\n    // if (nativeEvent.pageScrollState != 'idle') return\n    // if (scrollPositionRef.current != commonState.navActiveIndex) {\n    //   setNavActiveIndex(scrollPositionRef.current)\n    // }\n    // if (activeIndexRef.current == -1) return\n    // if (nativeEvent.offset == 0) {\n    //   isScrollingRef.current = false\n\n    //   const index = nativeEvent.position\n    //   if (activeIndexRef.current == index) return\n    //   activeIndexRef.current = index\n    //   setNavActiveIndex(index)\n    // } else if (!isScrollingRef.current) {\n    //   isScrollingRef.current = true\n    // }\n  }, [])\n\n  useEffect(() => {\n    const handleUpdate = (id: CommonState['navActiveId']) => {\n      const index = viewMap[id]\n      if (activeIndexRef.current == index) return\n      activeIndexRef.current = index\n      pagerViewRef.current?.setPageWithoutAnimation(index)\n    }\n    const handleConfigUpdate = (keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) => {\n      if (!keys.includes('common.homePageScroll')) return\n      pagerViewRef.current?.setScrollEnabled(setting['common.homePageScroll']!)\n    }\n    // window.requestAnimationFrame(() => pagerViewRef.current && pagerViewRef.current.setPage(activeIndexRef.current))\n    global.state_event.on('navActiveIdUpdated', handleUpdate)\n    global.state_event.on('configUpdated', handleConfigUpdate)\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleUpdate)\n      global.state_event.off('configUpdated', handleConfigUpdate)\n    }\n  }, [])\n\n\n  const component = useMemo(() => (\n    <PagerView ref={pagerViewRef}\n      initialPage={activeIndexRef.current}\n      // onPageScroll={handlePageScroll}\n      offscreenPageLimit={1}\n      onPageSelected={onPageSelected}\n      onPageScrollStateChanged={onPageScrollStateChanged}\n      scrollEnabled={settingState.setting['common.homePageScroll']}\n      style={styles.pagerView}\n    >\n      <View collapsable={false} key=\"nav_search\" style={styles.pageStyle}>\n        <SearchPage />\n      </View>\n      <View collapsable={false} key=\"nav_songlist\" style={styles.pageStyle}>\n        <SongListPage />\n      </View>\n      <View collapsable={false} key=\"nav_top\" style={styles.pageStyle}>\n        <LeaderboardPage />\n      </View>\n      <View collapsable={false} key=\"nav_love\" style={styles.pageStyle}>\n        <MylistPage />\n      </View>\n      <View collapsable={false} key=\"nav_setting\" style={styles.pageStyle}>\n        <SettingPage />\n      </View>\n      {/* <View collapsable={false} key=\"nav_search\" style={styles.pageStyle}>\n        <Search />\n      </View>\n      <View collapsable={false} key=\"nav_songlist\" style={styles.pageStyle}>\n        <SongList />\n      </View>\n      <View collapsable={false} key=\"nav_top\" style={styles.pageStyle}>\n        <Leaderboard />\n      </View>\n      <View collapsable={false} key=\"nav_love\" style={styles.pageStyle}>\n        <Mylist />\n      </View>\n      <View collapsable={false} key=\"nav_setting\" style={styles.pageStyle}>\n        <Setting />\n      </View> */}\n    </PagerView>\n  ), [onPageScrollStateChanged, onPageSelected])\n\n  return component\n}\n\nconst styles = createStyle({\n  pagerView: {\n    flex: 1,\n    overflow: 'hidden',\n  },\n  pageStyle: {\n    // alignItems: 'center',\n    // padding: 20,\n  },\n})\n\n\nexport default Main\n\n"
  },
  {
    "path": "src/screens/Home/Vertical/index.tsx",
    "content": "import Content from './Content'\nimport PlayerBar from '@/components/player/PlayerBar'\n\nexport default () => {\n  return (\n    <>\n      <Content />\n      <PlayerBar isHome />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Download/index.js",
    "content": "import {\n  // StyleSheet,\n  View,\n  // Button,\n  Text,\n} from 'react-native'\n\n// import Menu from '@/components/Menu'\n\nexport default () => {\n  return <View>\n    <Text>下载</Text>\n  </View>\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/BoardsList/List.tsx",
    "content": "import { forwardRef, useImperativeHandle, useState } from 'react'\nimport { View, ScrollView } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport { type Position } from './ListMenu'\nimport ListItem, { type ListItemProps } from './ListItem'\nimport { type BoardItem } from '@/store/leaderboard/state'\n\nexport interface ListProps {\n  onBoundChange: (listId: string) => void\n  onShowMenu: (info: { listId: string, name: string, index: number }, position: Position) => void\n}\nexport interface ListType {\n  setList: (list: BoardItem[], activeId: string) => void\n  hideMenu: () => void\n}\n\nexport default forwardRef<ListType, ListProps>(({ onBoundChange, onShowMenu }, ref) => {\n  const [activeId, setActiveId] = useState('')\n  const [longPressIndex, setLongPressIndex] = useState(-1)\n  const [list, setList] = useState<BoardItem[]>([])\n\n  useImperativeHandle(ref, () => ({\n    setList(list, activeId) {\n      setList(list)\n      setActiveId(activeId)\n    },\n    hideMenu() {\n      setLongPressIndex(-1)\n    },\n  }), [])\n\n  const handleBoundChange = (item: BoardItem) => {\n    setActiveId(item.id)\n    onBoundChange(item.id)\n  }\n\n  const handleShowMenu: ListItemProps['onShowMenu'] = (listId, name, index, position: Position) => {\n    setLongPressIndex(index)\n    onShowMenu({ listId, name, index }, position)\n  }\n\n  return (\n    <ScrollView style={styles.scrollView} keyboardShouldPersistTaps={'always'}>\n      <View>\n        {\n          list.map((item, index) => {\n            return (\n              <ListItem\n                key={item.id}\n                item={item}\n                index={index}\n                longPressIndex={longPressIndex}\n                activeId={activeId}\n                onShowMenu={handleShowMenu}\n                onBoundChange={handleBoundChange}\n              />\n            )\n          })\n        }\n      </View>\n    </ScrollView>\n  )\n})\n\n\nconst styles = createStyle({\n  scrollView: {\n    flexShrink: 1,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/BoardsList/ListItem.tsx",
    "content": "import { useCallback, useRef } from 'react'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport Button, { type BtnType } from '@/components/common/Button'\nimport { createStyle } from '@/utils/tools'\nimport { type BoardItem } from '@/store/leaderboard/state'\nimport { Icon } from '@/components/common/Icon'\n\n// index={index}\n// longPressIndex={longPressIndex}\n// activeId={activeId}\n// showMenu={showMenu}\n// onBoundChange={handleBoundChange}\nexport interface ListItemProps {\n  item: BoardItem\n  index: number\n  longPressIndex: number\n  activeId: string\n  onShowMenu: (id: string, name: string, index: number, position: { x: number, y: number, w: number, h: number }) => void\n  onBoundChange: (item: BoardItem) => void\n}\n\nexport default ({ item, activeId, index, longPressIndex, onBoundChange, onShowMenu }: ListItemProps) => {\n  const theme = useTheme()\n  const buttonRef = useRef<BtnType>(null)\n\n  const setPosition = useCallback(() => {\n    if (buttonRef.current?.measure) {\n      buttonRef.current.measure((fx, fy, width, height, px, py) => {\n        // console.log(fx, fy, width, height, px, py)\n        onShowMenu(item.id, item.name, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n      })\n    }\n  }, [index, item, onShowMenu])\n\n  const active = activeId == item.id\n\n  return (\n    <Button\n      ref={buttonRef}\n      style={{ ...styles.button, backgroundColor: index == longPressIndex ? theme['c-button-background-active'] : undefined }}\n      key={item.id} onLongPress={setPosition}\n      onPress={() => { onBoundChange(item) }}\n    >\n      {\n        active\n          ? <Icon style={styles.listActiveIcon} name=\"chevron-right\" size={12} color={theme['c-primary-font']} />\n          : null\n      }\n      <Text style={styles.listName} size={14} textBreakStrategy=\"simple\" color={active ? theme['c-primary-font-active'] : theme['c-font']} numberOfLines={1}>{item.name}</Text>\n    </Button>\n  )\n}\n\nconst styles = createStyle({\n  button: {\n    paddingLeft: 5,\n    paddingRight: 10,\n    paddingTop: 10,\n    paddingBottom: 10,\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  listActiveIcon: {\n    // width: 18,\n    marginLeft: 3,\n    // paddingRight: 5,\n    textAlign: 'center',\n  },\n  listName: {\n    height: '100%',\n    justifyContent: 'center',\n    paddingLeft: 6,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/BoardsList/ListMenu.tsx",
    "content": "import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport { useI18n } from '@/lang'\nimport Menu, { type MenuType, type Position } from '@/components/common/Menu'\n\nexport interface SelectInfo {\n  listId: string\n  name: string\n  index: number\n}\nconst initSelectInfo = {}\n\nexport interface ListMenuProps {\n  onPlay: (selectInfo: SelectInfo) => void\n  onCollect: (selectInfo: SelectInfo) => void\n  onHideMenu: () => void\n}\nexport interface ListMenuType {\n  show: (selectInfo: SelectInfo, position: Position) => void\n}\n\nexport type {\n  Position,\n}\n\nexport default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {\n  const t = useI18n()\n  const [visible, setVisible] = useState(false)\n  const menuRef = useRef<MenuType>(null)\n  const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo, position) {\n      selectInfoRef.current = selectInfo\n      if (visible) menuRef.current?.show(position)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          menuRef.current?.show(position)\n        })\n      }\n    },\n  }))\n\n  const menus = useMemo(() => {\n    return [\n      { action: 'play', label: t('play') },\n      { action: 'collect', label: t('collect') },\n    ] as const\n  }, [t])\n\n  const handleMenuPress = ({ action }: typeof menus[number]) => {\n    const selectInfo = selectInfoRef.current\n    switch (action) {\n      case 'play':\n        props.onPlay(selectInfo)\n        break\n      case 'collect':\n        props.onCollect(selectInfo)\n        break\n      default:\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <Menu ref={menuRef} menus={menus} onPress={handleMenuPress} onHide={props.onHideMenu} />\n      : null\n  )\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/BoardsList/index.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { StyleSheet, View } from 'react-native'\nimport List, { type ListType, type ListProps } from './List'\nimport ListMenu, { type ListMenuType, type Position } from './ListMenu'\nimport { type BoardItem } from '@/store/leaderboard/state'\n\n\nexport interface BoardsListProps {\n  onBoundChange: (listId: string) => void\n  onPlay: (listId: string) => void\n  onCollect: (listId: string, name: string) => void\n}\nexport interface BoardsListType {\n  setList: (list: BoardItem[], activeId: string) => void\n}\n\nexport default forwardRef<BoardsListType, BoardsListProps>(({ onBoundChange, onPlay, onCollect }, ref) => {\n  const listRef = useRef<ListType>(null)\n  const listMenuRef = useRef<ListMenuType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setList(list, listId) {\n      listRef.current?.setList(list, listId)\n    },\n  }), [])\n\n  const handleShowMenu: ListProps['onShowMenu'] = ({ listId, name, index }, position: Position) => {\n    listMenuRef.current?.show({\n      listId,\n      index,\n      name,\n    }, position)\n  }\n\n  return (\n    <View style={styles.container}>\n      <List\n        ref={listRef}\n        onBoundChange={onBoundChange}\n        onShowMenu={handleShowMenu} />\n      <ListMenu\n        ref={listMenuRef}\n        onHideMenu={() => listRef.current?.hideMenu()}\n        onPlay={({ listId }) => { onPlay(listId) }}\n        onCollect={({ listId, name }) => { onCollect(listId, name) }}\n      />\n    </View>\n  )\n})\n\n\nconst styles = StyleSheet.create({\n  container: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Horizontal/LeftBar.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { View } from 'react-native'\n\nimport SourceSelector, {\n  type SourceSelectorType as _SourceSelectorType,\n  // type SourceSelectorProps as _SourceSelectorProps,\n} from '@/components/SourceSelector'\nimport BoardsList, { type BoardsListType, type BoardsListProps } from '../BoardsList'\nimport { BorderWidths } from '@/theme'\nimport { createStyle } from '@/utils/tools'\nimport { handleCollect, handlePlay } from '../listAction'\nimport boardState, { type InitState } from '@/store/leaderboard/state'\nimport { useTheme } from '@/store/theme/hook'\nimport { getBoardsList } from '@/core/leaderboard'\n\ntype Sources = Readonly<InitState['sources']>\n// type SourceSelectorProps = _SourceSelectorProps<Sources>\ntype SourceSelectorType = _SourceSelectorType<Sources>\n\nexport interface LeftBarProps {\n  onChangeList: (source: LX.OnlineSource, id: string) => void\n}\n\nexport interface LeftBarType {\n  setBound: (source: LX.OnlineSource, id: string) => void\n}\n\nexport default forwardRef<LeftBarType, LeftBarProps>(({ onChangeList }, ref) => {\n  const theme = useTheme()\n  const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  const boardsListRef = useRef<BoardsListType>(null)\n  const boundInfo = useRef<{ source: LX.OnlineSource, id: string | null }>({ source: 'kw', id: null })\n  useImperativeHandle(ref, () => ({\n    setBound(source, listId) {\n      boundInfo.current = { source, id: listId }\n      sourceSelectorRef.current?.setSourceList(boardState.sources, source)\n      void getBoardsList(source).then(list => {\n        boardsListRef.current?.setList(list, listId)\n      })\n    },\n  }), [])\n\n\n  const onSourceChange = (source: LX.OnlineSource) => {\n    boundInfo.current.source = source\n    void getBoardsList(source).then(list => {\n      const id = list[0].id\n      requestAnimationFrame(() => {\n        boardsListRef.current?.setList(list, id)\n        requestAnimationFrame(() => {\n          onChangeList(source, id)\n        })\n      })\n    })\n  }\n  const onBoundChange: BoardsListProps['onBoundChange'] = (id) => {\n    boundInfo.current.id = id\n    onChangeList(boundInfo.current.source, id)\n  }\n  const onPlay: BoardsListProps['onPlay'] = (id) => {\n    boundInfo.current.id = id\n    void handlePlay(id, boardState.listDetailInfo.list)\n  }\n  const onCollect: BoardsListProps['onCollect'] = (id, name) => {\n    boundInfo.current.id = id\n    void handleCollect(id, name, boundInfo.current.source)\n  }\n\n  return (\n    <View style={{ ...styles.container, borderRightColor: theme['c-list-header-border-bottom'] }}>\n      <View style={styles.selector}>\n        <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} />\n      </View>\n      <BoardsList\n        ref={boardsListRef}\n        onBoundChange={onBoundChange}\n        onPlay={onPlay}\n        onCollect={onCollect}\n      />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexDirection: 'column',\n    width: '26%',\n    maxWidth: 180,\n    minWidth: 110,\n    flexGrow: 0,\n    flexShrink: 0,\n    borderRightWidth: BorderWidths.normal,\n  },\n  selector: {\n    height: 38,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Horizontal/index.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { View } from 'react-native'\nimport { createStyle } from '@/utils/tools'\n\nimport LeftBar, { type LeftBarType, type LeftBarProps } from './LeftBar'\nimport MusicList, { type MusicListType } from '../MusicList'\nimport { getLeaderboardSetting, saveLeaderboardSetting } from '@/utils/data'\n// import { BorderWidths } from '@/theme'\n// import { useTheme } from '@/store/theme/hook'\n\n\nexport default () => {\n  const leftBarRef = useRef<LeftBarType>(null)\n  const musicListRef = useRef<MusicListType>(null)\n  const isUnmountedRef = useRef(false)\n  // const theme = useTheme()\n\n  const handleChangeBound: LeftBarProps['onChangeList'] = (source, id) => {\n    musicListRef.current?.loadList(source, id)\n    void saveLeaderboardSetting({\n      source,\n      boardId: id,\n    })\n  }\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    void getLeaderboardSetting().then(({ source, boardId }) => {\n      leftBarRef.current?.setBound(source, boardId)\n      musicListRef.current?.loadList(source, boardId)\n    })\n\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  return (\n    <View style={styles.container}>\n      <LeftBar\n        ref={leftBarRef}\n        onChangeList={handleChangeBound}\n      />\n      <MusicList\n        ref={musicListRef}\n      />\n    </View>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    width: '100%',\n    flex: 1,\n    flexDirection: 'row',\n    // borderTopWidth: BorderWidths.normal,\n  },\n  content: {\n    flex: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/MusicList.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\nimport OnlineList, { type OnlineListType, type OnlineListProps } from '@/components/OnlineList'\nimport { clearListDetail, getListDetail, setListDetail, setListDetailInfo } from '@/core/leaderboard'\nimport boardState from '@/store/leaderboard/state'\nimport { handlePlay } from './listAction'\n\n// export type MusicListProps = Pick<OnlineListProps,\n// 'onLoadMore'\n// | 'onPlayList'\n// | 'onRefresh'\n// >\n\nexport interface MusicListType {\n  loadList: (source: LX.OnlineSource, listId: string) => void\n}\n\nexport default forwardRef<MusicListType, {}>((props, ref) => {\n  const listRef = useRef<OnlineListType>(null)\n  const isUnmountedRef = useRef(false)\n  useImperativeHandle(ref, () => ({\n    async loadList(source, id) {\n      const listDetailInfo = boardState.listDetailInfo\n      listRef.current?.setList([])\n      if (listDetailInfo.id == id && listDetailInfo.source == source && listDetailInfo.list.length) {\n        requestAnimationFrame(() => {\n          listRef.current?.setList(listDetailInfo.list)\n        })\n      } else {\n        listRef.current?.setStatus('loading')\n        const page = 1\n        setListDetailInfo(id)\n        return getListDetail(id, page).then((listDetail) => {\n          const result = setListDetail(listDetail, id, page)\n          if (isUnmountedRef.current) return\n          requestAnimationFrame(() => {\n            listRef.current?.setList(result.list)\n            listRef.current?.setStatus(boardState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n          })\n        }).catch(() => {\n          if (boardState.listDetailInfo.list.length && page == 1) clearListDetail()\n          listRef.current?.setStatus('error')\n        })\n      }\n    },\n  }), [])\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  const handlePlayList: OnlineListProps['onPlayList'] = (index) => {\n    const listDetailInfo = boardState.listDetailInfo\n    // console.log(boardState.listDetailInfo)\n    void handlePlay(listDetailInfo.id, listDetailInfo.list, index)\n  }\n  const handleRefresh: OnlineListProps['onRefresh'] = () => {\n    const page = 1\n    listRef.current?.setStatus('refreshing')\n    getListDetail(boardState.listDetailInfo.id, page, true).then((listDetail) => {\n      const result = setListDetail(listDetail, boardState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list)\n      listRef.current?.setStatus(boardState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (boardState.listDetailInfo.list.length && page == 1) clearListDetail()\n      listRef.current?.setStatus('error')\n    })\n  }\n  const handleLoadMore: OnlineListProps['onLoadMore'] = () => {\n    listRef.current?.setStatus('loading')\n    const page = boardState.listDetailInfo.list.length ? boardState.listDetailInfo.page + 1 : 1\n    getListDetail(boardState.listDetailInfo.id, page).then((listDetail) => {\n      const result = setListDetail(listDetail, boardState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list, true)\n      listRef.current?.setStatus(boardState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (boardState.listDetailInfo.list.length && page == 1) clearListDetail()\n      listRef.current?.setStatus('error')\n    })\n  }\n\n  return <OnlineList\n    ref={listRef}\n    onPlayList={handlePlayList}\n    onRefresh={handleRefresh}\n    onLoadMore={handleLoadMore}\n    checkHomePagerIdle\n    rowType='medium'\n   />\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Vertical/HeaderBar/ActiveListName.tsx",
    "content": "import { forwardRef, useImperativeHandle, useState } from 'react'\nimport { TouchableOpacity } from 'react-native'\n\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\nexport interface ActiveListNameProps {\n  onShowBound: () => void\n}\nexport interface ActiveListNameType {\n  setBound: (id: string, name: string) => void\n}\n\nexport default forwardRef<ActiveListNameType, ActiveListNameProps>(({ onShowBound }, ref) => {\n  const theme = useTheme()\n  let [currentListName, setCurrentListName] = useState('')\n\n  useImperativeHandle(ref, () => ({\n    setBound(id, name) {\n      setCurrentListName(name)\n    },\n  }), [])\n\n  return (\n    <TouchableOpacity onPress={onShowBound} style={styles.currentList}>\n      <Text numberOfLines={1} style={styles.currentListText} color={theme['c-button-font']}>{currentListName}</Text>\n    </TouchableOpacity>\n  )\n})\n\n\nconst styles = createStyle({\n  currentList: {\n    flex: 1,\n    flexDirection: 'row',\n    paddingRight: 2,\n    // height: 36,\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  currentListIcon: {\n    paddingLeft: 15,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 0,\n  },\n  currentListText: {\n    flex: 1,\n    // minWidth: 70,\n    // paddingLeft: 10,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Vertical/HeaderBar/SourceSelector.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { StyleSheet, View, type ViewStyle } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport SourceSelector, {\n  type SourceSelectorType as _SourceSelectorType,\n  type SourceSelectorProps as _SourceSelectorProps,\n} from '@/components/SourceSelector'\nimport leaderboardState, { type Source, type InitState } from '@/store/leaderboard/state'\n\ntype Sources = Readonly<InitState['sources']>\ntype SourceSelectorCommonProps = _SourceSelectorProps<Sources>\ntype SourceSelectorCommonType = _SourceSelectorType<Sources>\n\nexport interface SourceSelectorProps {\n  onSourceChange: SourceSelectorCommonProps['onSourceChange']\n  style?: ViewStyle\n}\n\nexport interface SourceSelectorType {\n  setSource: (source: Source) => void\n}\n\nexport default forwardRef<SourceSelectorType, SourceSelectorProps>(({ style, onSourceChange }, ref) => {\n  const sourceSelectorRef = useRef<SourceSelectorCommonType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setSource(source) {\n      sourceSelectorRef.current?.setSourceList(leaderboardState.sources, source)\n    },\n  }), [])\n\n\n  return (\n    <View style={StyleSheet.compose<ViewStyle, ViewStyle, ViewStyle>(styles.selector, style)}>\n      <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} center />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  selector: {\n    // width: 86,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Vertical/HeaderBar/index.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { View } from 'react-native'\n\n// import { useGetter, useDispatch } from '@/store'\n// import Tag from './Tag'\n// import OpenList from './OpenList'\nimport { createStyle } from '@/utils/tools'\n// import { BorderWidths } from '@/theme'\nimport SourceSelector, {\n  type SourceSelectorType,\n} from './SourceSelector'\nimport { useTheme } from '@/store/theme/hook'\n// import { BorderWidths } from '@/theme'\nimport ActiveListName, { type ActiveListNameType } from './ActiveListName'\nimport { BorderWidths } from '@/theme'\n\nexport interface HeaderBarProps {\n  onShowBound: () => void\n  onSourceChange: (source: LX.OnlineSource) => void\n}\n\nexport interface HeaderBarType {\n  setBound: (source: LX.OnlineSource, id: string, name: string) => void\n}\n\n\nexport default forwardRef<HeaderBarType, HeaderBarProps>(({ onShowBound, onSourceChange }, ref) => {\n  const activeListNameRef = useRef<ActiveListNameType>(null)\n  const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    setBound(source, id, name) {\n      sourceSelectorRef.current?.setSource(source)\n      activeListNameRef.current?.setBound(id, name)\n    },\n  }), [])\n\n\n  return (\n    <View style={{ ...styles.currentList, borderBottomColor: theme['c-border-background'] }}>\n      <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} />\n      <ActiveListName ref={activeListNameRef} onShowBound={onShowBound} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  currentList: {\n    flexDirection: 'row',\n    height: 38,\n    zIndex: 2,\n    // paddingRight: 10,\n    borderBottomWidth: BorderWidths.normal,\n  },\n  selector: {\n    width: 86,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/Vertical/index.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport { View } from 'react-native'\nimport { createStyle } from '@/utils/tools'\n\nimport MusicList, { type MusicListType } from '../MusicList'\nimport { getLeaderboardSetting, saveLeaderboardSetting } from '@/utils/data'\nimport DrawerLayoutFixed, { type DrawerLayoutFixedType } from '@/components/common/DrawerLayoutFixed'\nimport HeaderBar, { type HeaderBarType, type HeaderBarProps } from './HeaderBar'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\n// import { BorderWidths } from '@/theme'\n// import { useTheme } from '@/store/theme/hook'\nimport BoardsList, { type BoardsListType, type BoardsListProps } from '../BoardsList'\nimport type { InitState as CommonState } from '@/store/common/state'\nimport settingState from '@/store/setting/state'\nimport { getBoardsList } from '@/core/leaderboard'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { handleCollect, handlePlay } from '../listAction'\nimport boardState from '@/store/leaderboard/state'\n\n\nconst MAX_WIDTH = scaleSizeW(200)\n\nexport default () => {\n  const drawer = useRef<DrawerLayoutFixedType>(null)\n  const theme = useTheme()\n  const musicListRef = useRef<MusicListType>(null)\n  const isUnmountedRef = useRef(false)\n  const boardsListRef = useRef<BoardsListType>(null)\n  const headerBarRef = useRef<HeaderBarType>(null)\n  const boundInfo = useRef<{ source: LX.OnlineSource, id: string | null }>({ source: 'kw', id: null })\n  // const [width, setWidth] = useState(0)\n\n  const handleBoundChange = (source: LX.OnlineSource, id: string) => {\n    musicListRef.current?.loadList(source, id)\n    void saveLeaderboardSetting({\n      source,\n      boardId: id,\n    })\n  }\n  const onBoundChange: BoardsListProps['onBoundChange'] = (id) => {\n    boundInfo.current.id = id\n    void getBoardsList(boundInfo.current.source).then(list => {\n      requestAnimationFrame(() => {\n        const bound = list.find(l => l.id == id)\n        headerBarRef.current?.setBound(boundInfo.current.source, id, bound?.name ?? 'Unknown')\n      })\n    })\n    handleBoundChange(boundInfo.current.source, id)\n    requestAnimationFrame(() => {\n      drawer.current?.closeDrawer()\n    })\n  }\n  const onPlay: BoardsListProps['onPlay'] = (id) => {\n    boundInfo.current.id = id\n    void handlePlay(id, boardState.listDetailInfo.list)\n  }\n  const onCollect: BoardsListProps['onCollect'] = (id, name) => {\n    boundInfo.current.id = id\n    void handleCollect(id, name, boundInfo.current.source)\n  }\n  const onShowBound = () => {\n    requestAnimationFrame(() => {\n      drawer.current?.openDrawer()\n    })\n  }\n  const onSourceChange: HeaderBarProps['onSourceChange'] = (source) => {\n    boundInfo.current.source = source\n    void getBoardsList(source).then(list => {\n      const id = list[0].id\n      const name = list[0].name\n      requestAnimationFrame(() => {\n        boardsListRef.current?.setList(list, id)\n        headerBarRef.current?.setBound(source, id, name ?? 'Unknown')\n        requestAnimationFrame(() => {\n          handleBoundChange(source, id)\n        })\n      })\n    })\n  }\n\n  const navigationView = () => {\n    return (\n      <BoardsList\n        ref={boardsListRef}\n        onBoundChange={onBoundChange}\n        onCollect={onCollect}\n        onPlay={onPlay}\n      />\n    )\n  }\n\n  // const theme = useTheme()\n\n\n  useEffect(() => {\n    const handleFixDrawer = (id: CommonState['navActiveId']) => {\n      if (id == 'nav_top') drawer.current?.fixWidth()\n    }\n    global.state_event.on('navActiveIdUpdated', handleFixDrawer)\n\n\n    isUnmountedRef.current = false\n    void getLeaderboardSetting().then(({ source, boardId }) => {\n      boundInfo.current.source = source\n      boundInfo.current.id = boardId\n      void getBoardsList(source).then(list => {\n        const bound = list.find(l => l.id == boardId)\n        boardsListRef.current?.setList(list, boardId)\n        headerBarRef.current?.setBound(source, boardId, bound?.name ?? 'Unknown')\n      })\n      musicListRef.current?.loadList(source, boardId)\n    })\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleFixDrawer)\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  return (\n    <DrawerLayoutFixed\n      ref={drawer}\n      visibleNavNames={[COMPONENT_IDS.home]}\n      // drawerWidth={width}\n      widthPercentage={0.82}\n      widthPercentageMax={MAX_WIDTH}\n      drawerPosition={settingState.setting['common.drawerLayoutPosition']}\n      renderNavigationView={navigationView}\n      drawerBackgroundColor={theme['c-content-background']}\n      style={{ elevation: 1 }}\n    >\n      <View style={styles.container}>\n        <HeaderBar ref={headerBarRef} onShowBound={onShowBound} onSourceChange={onSourceChange} />\n        <MusicList ref={musicListRef} />\n      </View>\n    </DrawerLayoutFixed>\n    // <View style={styles.container}>\n    //   <LeftBar\n    //     ref={leftBarRef}\n    //     onChangeList={handleChangeBound}\n    //   />\n    //   <MusicList\n    //     ref={musicListRef}\n    //   />\n    // </View>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    width: '100%',\n    flex: 1,\n    flexDirection: 'column',\n    // borderTopWidth: BorderWidths.normal,\n  },\n  // content: {\n  //   flex: 1,\n  // },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/index.tsx",
    "content": "import { useHorizontalMode } from '@/utils/hooks'\nimport Vertical from './Vertical'\nimport Horizontal from './Horizontal'\n// import { AppColors } from '@/theme'\n\nexport default () => {\n  const isHorizontalMode = useHorizontalMode()\n\n  return isHorizontalMode\n    ? <Horizontal />\n    : <Vertical />\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Leaderboard/listAction.ts",
    "content": "import { createList, setTempList } from '@/core/list'\nimport { playList } from '@/core/player/player'\nimport { getListDetail, getListDetailAll } from '@/core/leaderboard'\nimport { LIST_IDS } from '@/config/constant'\nimport listState from '@/store/list/state'\nimport syncSourceList from '@/core/syncSourceList'\nimport { confirmDialog, toMD5, toast } from '@/utils/tools'\n\n\nconst getListId = (id: string) => `board__${id}`\n\nexport const handlePlay = async(id: string, list?: LX.Music.MusicInfoOnline[], index = 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    void 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 (listState.tempListMeta.id == listId) {\n      await setTempList(listId, [...fullList])\n    }\n  } else {\n    await setTempList(listId, [...fullList])\n    void playList(LIST_IDS.TEMP, index)\n  }\n}\n\nexport const handleCollect = async(id: string, name: string, source: LX.OnlineSource) => {\n  const listId = getListId(id)\n  const targetList = listState.userList.find(l => l.sourceListId == listId)\n  if (targetList) {\n    const confirm = await confirmDialog({\n      message: global.i18n.t('duplicate_list_tip', { name: targetList.name }),\n      cancelButtonText: global.i18n.t('list_import_part_button_cancel'),\n      confirmButtonText: global.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 createList({\n    name,\n    id: `${source}_${toMD5(listId)}`,\n    list,\n    source,\n    sourceListId: listId,\n  })\n  toast(global.i18n.t('collect_success'))\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/ActiveList.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'\nimport { TouchableOpacity } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\nimport { BorderWidths } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\nimport { useActiveListId, useListFetching } from '@/store/list/hook'\nimport listState from '@/store/list/state'\nimport { createStyle } from '@/utils/tools'\nimport { getListPrevSelectId } from '@/utils/data'\nimport { setActiveList } from '@/core/list'\nimport Text from '@/components/common/Text'\nimport { LIST_IDS } from '@/config/constant'\nimport Loading from '@/components/common/Loading'\nimport { useSettingValue } from '@/store/setting/hook'\n\nexport interface ActiveListProps {\n  onShowSearchBar: () => void\n  onScrollToTop: () => void\n}\nexport interface ActiveListType {\n  setVisibleBar: (visible: boolean) => void\n}\n\nexport default forwardRef<ActiveListType, ActiveListProps>(({ onShowSearchBar, onScrollToTop }, ref) => {\n  const theme = useTheme()\n  const currentListId = useActiveListId()\n  const fetching = useListFetching(currentListId)\n  const langId = useSettingValue('common.langId')\n  const currentListName = useMemo(() => {\n    switch (currentListId) {\n      case LIST_IDS.TEMP:\n        return global.i18n.t('list_name_temp')\n      case LIST_IDS.DEFAULT:\n        return global.i18n.t('list_name_default')\n      case LIST_IDS.LOVE:\n        return global.i18n.t('list_name_love')\n      default:\n        return listState.allList.find(l => l.id === currentListId)?.name ?? ''\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [currentListId, langId])\n  const [visibleBar, setVisibleBar] = useState(true)\n\n  useImperativeHandle(ref, () => ({\n    setVisibleBar(visible) {\n      setVisibleBar(visible)\n    },\n  }))\n\n  const showList = () => {\n    global.app_event.changeLoveListVisible(true)\n  }\n\n  useEffect(() => {\n    void getListPrevSelectId().then((id) => {\n      setActiveList(id)\n    })\n  }, [])\n\n  return (\n    <TouchableOpacity onPress={showList} onLongPress={onScrollToTop} style={{ ...styles.currentList, opacity: visibleBar ? 1 : 0, borderBottomColor: theme['c-border-background'] }}>\n      <Icon style={styles.currentListIcon} color={theme['c-button-font']} name=\"chevron-right\" size={12} />\n      { fetching ? <Loading color={theme['c-button-font']} style={styles.loading} /> : null }\n      <Text style={styles.currentListText} numberOfLines={1} color={theme['c-button-font']}>{currentListName}</Text>\n      <TouchableOpacity style={styles.currentListBtns} onPress={onShowSearchBar}>\n        <Icon color={theme['c-button-font']} name=\"search-2\" />\n      </TouchableOpacity>\n    </TouchableOpacity>\n  )\n})\n\n\nconst styles = createStyle({\n  currentList: {\n    flexDirection: 'row',\n    paddingRight: 2,\n    height: 36,\n    alignItems: 'center',\n    borderBottomWidth: BorderWidths.normal,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  currentListIcon: {\n    paddingLeft: 15,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 0,\n  },\n  currentListText: {\n    flex: 1,\n    // minWidth: 70,\n    // paddingLeft: 10,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n  },\n  loading: {\n    marginRight: 5,\n  },\n  currentListBtns: {\n    width: 46,\n    justifyContent: 'center',\n    alignItems: 'center',\n    height: '100%',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/List.tsx",
    "content": "import { playList } from '@/core/player/player'\nimport { useMemo, useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'\nimport { FlatList, type NativeScrollEvent, type NativeSyntheticEvent, type FlatListProps } from 'react-native'\n\nimport listState from '@/store/list/state'\nimport playerState from '@/store/player/state'\nimport { getListPosition, getListPrevSelectId, saveListPosition } from '@/utils/data'\n// import { useMusicList } from '@/store/list/hook'\nimport { getListMusics, setActiveList } from '@/core/list'\nimport ListItem, { ITEM_HEIGHT } from './ListItem'\nimport { createStyle, getRowInfo } from '@/utils/tools'\nimport { usePlayInfo, usePlayMusicInfo } from '@/store/player/hook'\nimport type { Position } from './ListMenu'\nimport type { SelectMode } from './MultipleModeBar'\nimport { useActiveListId } from '@/store/list/hook'\nimport { useSettingValue } from '@/store/setting/hook'\n\ntype FlatListType = FlatListProps<LX.Music.MusicInfo>\n\nexport interface ListProps {\n  onShowMenu: (musicInfo: LX.Music.MusicInfo, index: number, position: Position) => void\n  onMuiltSelectMode: () => void\n  onSelectAll: (isAll: boolean) => void\n}\nexport interface ListType {\n  setIsMultiSelectMode: (isMultiSelectMode: boolean) => void\n  setSelectMode: (mode: SelectMode) => void\n  selectAll: (isAll: boolean) => void\n  getSelectedList: () => LX.List.ListMusics\n  scrollToInfo: (info: LX.Music.MusicInfo) => void\n  scrollToTop: () => void\n}\n\nconst usePlayIndex = () => {\n  const activeListId = useActiveListId()\n  const playMusicInfo = usePlayMusicInfo()\n  const playInfo = usePlayInfo()\n\n  const playIndex = useMemo(() => {\n    return playMusicInfo.listId == activeListId ? playInfo.playIndex : -1\n  }, [activeListId, playInfo.playIndex, playMusicInfo.listId])\n\n  return playIndex\n}\n\n\nconst List = forwardRef<ListType, ListProps>(({ onShowMenu, onMuiltSelectMode, onSelectAll }, ref) => {\n  // const t = useI18n()\n  const flatListRef = useRef<FlatList>(null)\n  const [currentList, setList] = useState<LX.List.ListMusics>([])\n  const listFirstScrollRef = useRef(false)\n  const isMultiSelectModeRef = useRef(false)\n  const selectModeRef = useRef<SelectMode>('single')\n  const prevSelectIndexRef = useRef(-1)\n  const [selectedList, setSelectedList] = useState<LX.List.ListMusics>([])\n  const selectedListRef = useRef<LX.List.ListMusics>([])\n  const currentListIdRef = useRef('')\n  const waitJumpListPositionRef = useRef(false)\n  const rowInfo = useRef(getRowInfo())\n  const isShowAlbumName = useSettingValue('list.isShowAlbumName')\n  const isShowInterval = useSettingValue('list.isShowInterval')\n  // console.log('render music list')\n\n  useImperativeHandle(ref, () => ({\n    setIsMultiSelectMode(isMultiSelectMode) {\n      isMultiSelectModeRef.current = isMultiSelectMode\n      if (!isMultiSelectMode) {\n        prevSelectIndexRef.current = -1\n        handleUpdateSelectedList([])\n      }\n    },\n    setSelectMode(mode) {\n      selectModeRef.current = mode\n    },\n    selectAll(isAll) {\n      let list: LX.List.ListMusics\n      if (isAll) {\n        list = [...currentList]\n      } else {\n        list = []\n      }\n      selectedListRef.current = list\n      setSelectedList(list)\n    },\n    getSelectedList() {\n      return selectedListRef.current\n    },\n    scrollToInfo(info) {\n      void getListMusics(listState.activeListId).then((list) => {\n        const index = list.findIndex(m => m.id == info.id)\n        if (index < 0) return\n        flatListRef.current?.scrollToIndex({ index: Math.floor(index / (rowInfo.current.rowNum ?? 1)), viewPosition: 0.3, animated: true })\n      })\n    },\n    scrollToTop() {\n      flatListRef.current?.scrollToOffset({\n        offset: 0,\n        animated: true,\n      })\n    },\n  }))\n\n  useEffect(() => {\n    let isUpdateingList = true\n    const updateList = (id: string) => {\n      if (currentListIdRef.current == id) return\n      isUpdateingList = true\n      setList([])\n      currentListIdRef.current = id\n      void Promise.all([getListMusics(id), getListPosition(id)]).then(([list, position]) => {\n        requestAnimationFrame(() => {\n          if (currentListIdRef.current != id) return\n          selectedListRef.current = []\n          setSelectedList([])\n          setList([...list])\n          requestAnimationFrame(() => {\n            isUpdateingList = false\n            listFirstScrollRef.current = true\n            if (waitJumpListPositionRef.current) {\n              waitJumpListPositionRef.current = false\n              if (playerState.playMusicInfo.listId == id && playerState.playInfo.playIndex > -1) {\n                try {\n                  flatListRef.current?.scrollToIndex({ index: Math.floor(playerState.playInfo.playIndex / (rowInfo.current.rowNum ?? 1)), viewPosition: 0.3, animated: false })\n                  return\n                } catch {}\n              }\n            }\n            flatListRef.current?.scrollToOffset({ offset: position, animated: false })\n          })\n        })\n      })\n    }\n    const handleChange = (ids: string[]) => {\n      if (!ids.includes(listState.activeListId)) return\n      const id = listState.activeListId\n      void getListMusics(id).then((list) => {\n        if (currentListIdRef.current != id) return\n        selectedListRef.current = []\n        setSelectedList([])\n        setList([...list])\n      })\n    }\n\n    const handleJumpPosition = () => {\n      requestAnimationFrame(() => {\n        const listId = playerState.playMusicInfo.listId\n        if (!listId) return\n        if (listId != listState.activeListId) {\n          setActiveList(listId)\n          if (currentListIdRef.current != listId) waitJumpListPositionRef.current = true\n        } else if (playerState.playInfo.playIndex > -1) {\n          if (isUpdateingList) waitJumpListPositionRef.current = true\n          else {\n            try {\n              flatListRef.current?.scrollToIndex({ index: Math.floor(playerState.playInfo.playIndex / (rowInfo.current.rowNum ?? 1)), viewPosition: 0.3, animated: true })\n            } catch {}\n          }\n        }\n      })\n    }\n    if (global.lx.jumpMyListPosition) {\n      global.lx.jumpMyListPosition = false\n      if (playerState.playMusicInfo.listId) {\n        waitJumpListPositionRef.current = true\n        updateList(playerState.playMusicInfo.listId)\n      } else void getListPrevSelectId().then(updateList)\n    } else void getListPrevSelectId().then(updateList)\n\n    global.state_event.on('mylistToggled', updateList)\n    global.app_event.on('myListMusicUpdate', handleChange)\n    global.app_event.on('jumpListPosition', handleJumpPosition)\n\n    return () => {\n      global.state_event.off('mylistToggled', updateList)\n      global.app_event.off('myListMusicUpdate', handleChange)\n      global.app_event.off('jumpListPosition', handleJumpPosition)\n    }\n  }, [])\n\n  const activeIndex = usePlayIndex()\n  const handlePlay = (index: number) => {\n    void playList(listState.activeListId, index)\n  }\n\n  const handleUpdateSelectedList = (newList: LX.List.ListMusics) => {\n    if (selectedListRef.current.length && newList.length == currentList.length) onSelectAll(true)\n    else if (selectedListRef.current.length == currentList.length) onSelectAll(false)\n    selectedListRef.current = newList\n    setSelectedList(newList)\n  }\n  const handleSelect = (item: LX.Music.MusicInfo, pressIndex: number) => {\n    let newList: LX.List.ListMusics\n    if (selectModeRef.current == 'single') {\n      prevSelectIndexRef.current = pressIndex\n      const index = selectedListRef.current.indexOf(item)\n      if (index < 0) {\n        newList = [...selectedListRef.current, item]\n      } else {\n        newList = [...selectedListRef.current]\n        newList.splice(index, 1)\n      }\n    } else {\n      if (selectedListRef.current.length) {\n        const prevIndex = prevSelectIndexRef.current\n        const currentIndex = pressIndex\n        if (prevIndex == currentIndex) {\n          newList = []\n        } else if (currentIndex > prevIndex) {\n          newList = currentList.slice(prevIndex, currentIndex + 1)\n        } else {\n          newList = currentList.slice(currentIndex, prevIndex + 1)\n          newList.reverse()\n        }\n      } else {\n        newList = [item]\n        prevSelectIndexRef.current = pressIndex\n      }\n    }\n\n    handleUpdateSelectedList(newList)\n  }\n\n  const handlePress = (item: LX.Music.MusicInfo, index: number) => {\n    // console.log(global.lx.homePagerIdle)\n    requestAnimationFrame(() => {\n      // console.log(global.lx.homePagerIdle)\n      if (!global.lx.homePagerIdle) return\n      if (isMultiSelectModeRef.current) {\n        handleSelect(item, index)\n      } else {\n        handlePlay(index)\n      }\n    })\n  }\n\n  const handleLongPress = (item: LX.Music.MusicInfo, index: number) => {\n    if (isMultiSelectModeRef.current) return\n    prevSelectIndexRef.current = index\n    handleUpdateSelectedList([item])\n    onMuiltSelectMode()\n  }\n\n  const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {\n    if (listFirstScrollRef.current) {\n      listFirstScrollRef.current = false\n      return\n    }\n    void saveListPosition(listState.activeListId, nativeEvent.contentOffset.y)\n  }\n\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => (\n    <ListItem\n      item={item}\n      index={index}\n      activeIndex={activeIndex}\n      onPress={handlePress}\n      onLongPress={handleLongPress}\n      onShowMenu={onShowMenu}\n      selectedList={selectedList}\n      rowInfo={rowInfo.current}\n      isShowAlbumName={isShowAlbumName}\n      isShowInterval={isShowInterval}\n    />\n  )\n  const getkey: FlatListType['keyExtractor'] = item => item.id\n  const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n\n  return (\n    <FlatList\n      ref={flatListRef}\n      onScroll={handleScroll}\n      style={styles.list}\n      data={currentList}\n      maxToRenderPerBatch={4}\n      numColumns={rowInfo.current.rowNum}\n      horizontal={false}\n      // updateCellsBatchingPeriod={80}\n      windowSize={8}\n      removeClippedSubviews={true}\n      initialNumToRender={12}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      extraData={activeIndex}\n      getItemLayout={getItemLayout}\n    />\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n})\n\nexport default List\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/ListItem.tsx",
    "content": "import { memo, useRef } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\nimport { LIST_ITEM_HEIGHT } from '@/config/constant'\n// import { BorderWidths } from '@/theme'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle, type RowInfo } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useAssertApiSupport } from '@/store/common/hook'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport Text from '@/components/common/Text'\nimport Badge from '@/components/common/Badge'\n\nexport const ITEM_HEIGHT = scaleSizeH(LIST_ITEM_HEIGHT)\n\n\nexport default memo(({ item, index, activeIndex, onPress, onShowMenu, onLongPress, selectedList, rowInfo, isShowAlbumName, isShowInterval }: {\n  item: LX.Music.MusicInfo\n  index: number\n  activeIndex: number\n  onPress: (item: LX.Music.MusicInfo, index: number) => void\n  onLongPress: (item: LX.Music.MusicInfo, index: number) => void\n  onShowMenu: (item: LX.Music.MusicInfo, index: number, position: { x: number, y: number, w: number, h: number }) => void\n  selectedList: LX.Music.MusicInfo[]\n  rowInfo: RowInfo\n  isShowAlbumName: boolean\n  isShowInterval: boolean\n}) => {\n  const theme = useTheme()\n\n  const isSelected = selectedList.includes(item)\n  // console.log(item.name, selectedList, selectedList.includes(item))\n  const isSupported = useAssertApiSupport(item.source)\n  const moreButtonRef = useRef<TouchableOpacity>(null)\n  const handleShowMenu = () => {\n    if (moreButtonRef.current?.measure) {\n      moreButtonRef.current.measure((fx, fy, width, height, px, py) => {\n        // console.log(fx, fy, width, height, px, py)\n        onShowMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n      })\n    }\n  }\n  const active = activeIndex == index\n\n  const singer = `${item.singer}${isShowAlbumName && item.meta.albumName ? ` · ${item.meta.albumName}` : ''}`\n\n  return (\n    <View style={{ ...styles.listItem, width: rowInfo.rowWidth, height: ITEM_HEIGHT, backgroundColor: isSelected ? theme['c-primary-background-hover'] : 'rgba(0,0,0,0)', opacity: isSupported ? 1 : 0.5 }}>\n      <TouchableOpacity style={styles.listItemLeft} onPress={() => { onPress(item, index) }} onLongPress={() => { onLongPress(item, index) }}>\n        {\n          active\n            ? <Icon style={styles.sn} name=\"play-outline\" size={13} color={theme['c-primary-font']} />\n            : <Text style={styles.sn} size={13} color={theme['c-300']}>{index + 1}</Text>\n        }\n        <View style={styles.itemInfo}>\n          {/* <View style={styles.listItemTitle}> */}\n          <Text color={active ? theme['c-primary-font'] : theme['c-font']} numberOfLines={1}>{item.name}</Text>\n          {/* </View> */}\n          <View style={styles.listItemSingle}>\n            <Badge>{item.source.toUpperCase()}</Badge>\n            <Text style={styles.listItemSingleText} size={11} color={active ? theme['c-primary-alpha-200'] : theme['c-500']} numberOfLines={1}>\n              {singer}\n            </Text>\n          </View>\n        </View>\n        {\n          isShowInterval ? (\n            <Text size={12} color={active ? theme['c-primary-alpha-400'] : theme['c-250']} numberOfLines={1}>{item.interval}</Text>\n          ) : null\n        }\n      </TouchableOpacity>\n      {/* <View style={styles.listItemRight}> */}\n      <TouchableOpacity onPress={handleShowMenu} ref={moreButtonRef} style={styles.moreButton}>\n        <Icon name=\"dots-vertical\" style={{ color: theme['c-350'] }} size={12} />\n      </TouchableOpacity>\n      {/* </View> */}\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return !!(prevProps.item === nextProps.item &&\n    prevProps.index === nextProps.index &&\n    prevProps.isShowAlbumName === nextProps.isShowAlbumName &&\n    prevProps.isShowInterval === nextProps.isShowInterval &&\n    prevProps.activeIndex != nextProps.index &&\n    nextProps.activeIndex != nextProps.index &&\n    nextProps.selectedList.includes(nextProps.item) == prevProps.selectedList.includes(nextProps.item)\n  )\n})\n\n\nconst styles = createStyle({\n  listItem: {\n    // width: '50%',\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    // paddingLeft: 10,\n    paddingRight: 2,\n    alignItems: 'center',\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  listItemLeft: {\n    flex: 1,\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  sn: {\n    width: 38,\n    // fontSize: 12,\n    textAlign: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    paddingLeft: 3,\n    paddingRight: 3,\n  },\n  itemInfo: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n    paddingRight: 2,\n  },\n  // listItemTitle: {\n  //   flexGrow: 0,\n  //   flexShrink: 1,\n  // },\n  listItemSingle: {\n    paddingTop: 3,\n    flexDirection: 'row',\n    // alignItems: 'flex-end',\n  },\n  listItemSingleText: {\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    flexGrow: 0,\n    flexShrink: 1,\n    fontWeight: '300',\n    // fontSize: 15,\n  },\n  // listItemBadge: {\n  //   // fontSize: 10,\n  //   paddingLeft: 5,\n  //   paddingTop: 2,\n  //   alignSelf: 'flex-start',\n  // },\n  listItemRight: {\n    flexGrow: 0,\n    flexShrink: 0,\n    flexBasis: 'auto',\n    justifyContent: 'center',\n  },\n\n  moreButton: {\n    height: '80%',\n    paddingLeft: 16,\n    paddingRight: 16,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/ListMenu.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport { useI18n } from '@/lang'\nimport Menu, { type Menus, type MenuType, type Position } from '@/components/common/Menu'\nimport { hasDislike } from '@/core/dislikeList'\nimport { existsFile } from '@/utils/fs'\n\nexport interface SelectInfo {\n  musicInfo: LX.Music.MusicInfo\n  selectedList: LX.Music.MusicInfo[]\n  index: number\n  listId: string\n  single: boolean\n}\nconst initSelectInfo = {}\n\nexport interface ListMenuProps {\n  onPlay: (selectInfo: SelectInfo) => void\n  onPlayLater: (selectInfo: SelectInfo) => void\n  onAdd: (selectInfo: SelectInfo) => void\n  onMove: (selectInfo: SelectInfo) => void\n  onEditMetadata: (selectInfo: SelectInfo) => void\n  onCopyName: (selectInfo: SelectInfo) => void\n  onChangePosition: (selectInfo: SelectInfo) => void\n  onToggleSource: (selectInfo: SelectInfo) => void\n  onMusicSourceDetail: (selectInfo: SelectInfo) => void\n  onDislikeMusic: (selectInfo: SelectInfo) => void\n  onRemove: (selectInfo: SelectInfo) => void\n}\nexport interface ListMenuType {\n  show: (selectInfo: SelectInfo, position: Position) => void\n}\n\nexport type {\n  Position,\n}\n\nconst hasEditMetadata = async(musicInfo: LX.Music.MusicInfo) => {\n  if (musicInfo.source != 'local') return false\n  return existsFile(musicInfo.meta.filePath)\n}\nexport default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {\n  const t = useI18n()\n  const [visible, setVisible] = useState(false)\n  const menuRef = useRef<MenuType>(null)\n  const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)\n  const [menus, setMenus] = useState<Menus>([])\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo, position) {\n      selectInfoRef.current = selectInfo\n      handleSetMenu(selectInfo.musicInfo)\n      if (visible) menuRef.current?.show(position)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          menuRef.current?.show(position)\n        })\n      }\n    },\n  }))\n\n  const handleSetMenu = (musicInfo: LX.Music.MusicInfo) => {\n    let edit_metadata = false\n    const menu = [\n      { action: 'play', label: t('play') },\n      { action: 'playLater', label: t('play_later') },\n      // { action: 'download', label: '下载' },\n      { action: 'add', label: t('add_to') },\n      { action: 'move', label: t('move_to') },\n      { action: 'changePosition', label: t('change_position') },\n      { action: 'toggleSource', label: t('toggle_source') },\n      { action: 'copyName', label: t('copy_name') },\n      { action: 'musicSourceDetail', disabled: musicInfo.source == 'local', label: t('music_source_detail') },\n      // { action: 'musicSearch', label: t('music_search') },\n      { action: 'dislike', disabled: hasDislike(musicInfo), label: t('dislike') },\n      { action: 'remove', label: t('delete') },\n    ]\n    if (musicInfo.source == 'local') menu.splice(5, 0, { action: 'editMetadata', disabled: !edit_metadata, label: t('edit_metadata') })\n    setMenus(menu)\n    void Promise.all([hasEditMetadata(musicInfo)]).then(([_edit_metadata]) => {\n      // console.log(_edit_metadata)\n      let isUpdated = false\n      if (edit_metadata != _edit_metadata) {\n        edit_metadata = _edit_metadata\n        isUpdated ||= true\n      }\n\n      if (isUpdated) {\n        menu[menu.findIndex(m => m.action == 'editMetadata')].disabled = !edit_metadata\n        setMenus([...menu])\n      }\n    })\n  }\n\n  const handleMenuPress = ({ action }: typeof menus[number]) => {\n    const selectInfo = selectInfoRef.current\n    switch (action) {\n      case 'play':\n        props.onPlay(selectInfo)\n        break\n      case 'playLater':\n        props.onPlayLater(selectInfo)\n\n        break\n      case 'add':\n        props.onAdd(selectInfo)\n        // isMoveRef.current = false\n        // selectedListRef.current.length\n        //   ? setVisibleMusicMultiAddModal(true)\n        //   : setVisibleMusicAddModal(true)\n        break\n      case 'move':\n        props.onMove(selectInfo)\n        // isMoveRef.current = true\n        // selectedListRef.current.length\n        //   ? setVisibleMusicMultiAddModal(true)\n        //   : setVisibleMusicAddModal(true)\n        break\n      case 'editMetadata':\n        props.onEditMetadata(selectInfo)\n        break\n      case 'copyName':\n        props.onCopyName(selectInfo)\n        break\n      case 'changePosition':\n        props.onChangePosition(selectInfo)\n        // setVIsibleMusicPosition(true)\n        break\n      case 'toggleSource':\n        props.onToggleSource(selectInfo)\n        // setVIsibleMusicPosition(true)\n        break\n      case 'musicSourceDetail':\n        props.onMusicSourceDetail(selectInfo)\n        // setVIsibleMusicPosition(true)\n        break\n      case 'dislike':\n        props.onDislikeMusic(selectInfo)\n        break\n      case 'remove':\n        props.onRemove(selectInfo)\n        break\n      default:\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <Menu ref={menuRef} menus={menus} onPress={handleMenuPress} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/ListMusicSearch.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useEffect } from 'react'\nimport SearchTipList, { type SearchTipListProps as _SearchTipListProps, type SearchTipListType } from '@/components/SearchTipList'\nimport { debounce } from '@/utils'\nimport { searchListMusic } from './listAction'\nimport Button from '@/components/common/Button'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { View } from 'react-native'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { getListMusics } from '@/core/list'\nimport listState from '@/store/list/state'\n\ntype SearchTipListProps = _SearchTipListProps<LX.Music.MusicInfo>\ninterface ListMusicSearchProps {\n  onScrollToInfo: (info: LX.Music.MusicInfo) => void\n}\nexport const ITEM_HEIGHT = scaleSizeH(46)\n\nexport interface ListMusicSearchType {\n  search: (keyword: string, height: number) => void\n  hide: () => void\n}\n\nexport const debounceSearchList = debounce((text: string, list: LX.List.ListMusics, callback: (list: LX.List.ListMusics) => void) => {\n  // console.log(reslutList)\n  callback(searchListMusic(list, text))\n}, 200)\n\n\nexport default forwardRef<ListMusicSearchType, ListMusicSearchProps>(({ onScrollToInfo }, ref) => {\n  const searchTipListRef = useRef<SearchTipListType<LX.Music.MusicInfo>>(null)\n  const [visible, setVisible] = useState(false)\n  const currentListIdRef = useRef('')\n  const currentKeywordRef = useRef('')\n  const theme = useTheme()\n\n  const handleShowList = (keyword: string, height: number) => {\n    searchTipListRef.current?.setHeight(height)\n    currentKeywordRef.current = keyword\n    const id = currentListIdRef.current = listState.activeListId\n    if (keyword) {\n      void getListMusics(id).then(list => {\n        debounceSearchList(keyword, list, (list) => {\n          if (currentListIdRef.current != id) return\n          searchTipListRef.current?.setList(list)\n        })\n      })\n    } else {\n      searchTipListRef.current?.setList([])\n    }\n  }\n\n  useImperativeHandle(ref, () => ({\n    search(keyword, height) {\n      if (visible) handleShowList(keyword, height)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShowList(keyword, height)\n        })\n      }\n    },\n    hide() {\n      currentKeywordRef.current = ''\n      currentListIdRef.current = ''\n      searchTipListRef.current?.setList([])\n    },\n  }))\n\n  useEffect(() => {\n    const updateList = (id: string) => {\n      currentListIdRef.current = id\n      if (!currentKeywordRef.current) return\n      void getListMusics(listState.activeListId).then(list => {\n        debounceSearchList(currentKeywordRef.current, list, (list) => {\n          if (currentListIdRef.current != id) return\n          searchTipListRef.current?.setList(list)\n        })\n      })\n    }\n    const handleChange = (ids: string[]) => {\n      if (!ids.includes(listState.activeListId)) return\n      updateList(listState.activeListId)\n    }\n\n    global.state_event.on('mylistToggled', updateList)\n    global.app_event.on('myListMusicUpdate', handleChange)\n\n    return () => {\n      global.state_event.off('mylistToggled', updateList)\n      global.app_event.off('myListMusicUpdate', handleChange)\n    }\n  }, [])\n\n  const renderItem = ({ item, index }: { item: LX.Music.MusicInfo, index: number }) => {\n    return (\n      <Button style={styles.item} onPress={() => { onScrollToInfo(item) }} key={index}>\n        <View style={styles.itemName}>\n          <Text numberOfLines={1}>{item.name}</Text>\n          <Text style={styles.subName} numberOfLines={1} size={12} color={theme['c-font-label']}>{item.singer} ({item.meta.albumName})</Text>\n        </View>\n        <Text style={styles.itemSource} size={12} color={theme['c-font-label']}>{item.source}</Text>\n      </Button>\n    )\n  }\n  const getkey: SearchTipListProps['keyExtractor'] = item => item.id\n  const getItemLayout: SearchTipListProps['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n\n  return (\n    visible\n      ? <SearchTipList\n          ref={searchTipListRef}\n          renderItem={renderItem}\n          onPressBg={() => searchTipListRef.current?.setList([])}\n          keyExtractor={getkey}\n          getItemLayout={getItemLayout}\n        />\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  item: {\n    height: ITEM_HEIGHT,\n    flexDirection: 'row',\n    alignItems: 'center',\n    paddingLeft: 15,\n    paddingRight: 15,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  itemName: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n  subName: {\n    marginTop: 2,\n  },\n  itemSource: {\n    flexGrow: 0,\n    flexShrink: 0,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/ListSearchBar.tsx",
    "content": "import { useState, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'\nimport { Animated, View, TouchableOpacity } from 'react-native'\n\nimport Text from '@/components/common/Text'\nimport Input, { type InputType } from '@/components/common/Input'\n\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { BorderWidths } from '@/theme'\n\ninterface SearchInputProps {\n  onSearch: (keywork: string) => void\n}\ntype SearchInputType = InputType\n\nconst SearchInput = forwardRef<SearchInputType, SearchInputProps>(({ onSearch }, ref) => {\n  const [text, setText] = useState('')\n\n  const handleChangeText = (text: string) => {\n    setText(text)\n    onSearch(text.trim())\n  }\n\n  return (\n    <Input\n      onChangeText={handleChangeText}\n      placeholder=\"Search for something...\"\n      value={text}\n      style={styles.input}\n      // onFocus={showTipList}\n      clearBtn\n      ref={ref}\n    />\n  )\n})\n\n\nexport interface ListSearchBarProps {\n  onSearch: (keywork: string) => void\n  onExitSearch: () => void\n}\nexport interface ListSearchBarType {\n  show: () => void\n  hide: () => void\n}\n\nexport default forwardRef<ListSearchBarType, ListSearchBarProps>(({ onSearch, onExitSearch }, ref) => {\n  const t = useI18n()\n  // const isGetDetailFailedRef = useRef(false)\n  const [visible, setVisible] = useState(false)\n  const [animatePlayed, setAnimatPlayed] = useState(true)\n  const animFade = useRef(new Animated.Value(0)).current\n  const animTranslateY = useRef(new Animated.Value(0)).current\n  const searchInputRef = useRef<SearchInputType>(null)\n\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    show() {\n      handleShow()\n      requestAnimationFrame(() => {\n        searchInputRef.current?.focus()\n      })\n    },\n    hide() {\n      handleHide()\n    },\n  }))\n\n\n  const handleShow = useCallback(() => {\n    // console.log('show List')\n    setVisible(true)\n    setAnimatPlayed(false)\n    requestAnimationFrame(() => {\n      animTranslateY.setValue(-20)\n\n      Animated.parallel([\n        Animated.timing(animFade, {\n          toValue: 0.92,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n        Animated.timing(animTranslateY, {\n          toValue: 0,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n      ]).start(() => {\n        setAnimatPlayed(true)\n      })\n    })\n  }, [animFade, animTranslateY])\n\n  const handleHide = useCallback(() => {\n    setAnimatPlayed(false)\n    Animated.parallel([\n      Animated.timing(animFade, {\n        toValue: 0,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n      Animated.timing(animTranslateY, {\n        toValue: -20,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n    ]).start(finished => {\n      if (!finished) return\n      setVisible(false)\n      setAnimatPlayed(true)\n    })\n  }, [animFade, animTranslateY])\n\n\n  const animaStyle = useMemo(() => ({\n    ...styles.container,\n    // backgroundColor: theme['c-content-background'],\n    borderBottomColor: theme['c-border-background'],\n    opacity: animFade, // Bind opacity to animated value\n    transform: [\n      { translateY: animTranslateY },\n    ],\n  }), [animFade, animTranslateY, theme])\n\n  const component = useMemo(() => {\n    return (\n      <Animated.View style={animaStyle}>\n        <View style={styles.content}>\n          <SearchInput ref={searchInputRef} onSearch={onSearch} />\n        </View>\n        <TouchableOpacity onPress={onExitSearch} style={styles.btn}>\n          <Text color={theme['c-button-font']}>{t('list_select_cancel')}</Text>\n        </TouchableOpacity>\n      </Animated.View>\n    )\n  }, [animaStyle, onSearch, onExitSearch, theme, t])\n\n  return !visible && animatePlayed ? null : component\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    position: 'absolute',\n    left: 0,\n    top: 0,\n    width: '100%',\n    height: '100%',\n    flexDirection: 'row',\n    paddingLeft: 10,\n    borderBottomWidth: BorderWidths.normal,\n  },\n  content: {\n    flexDirection: 'row',\n    flex: 1,\n  },\n  input: {\n    height: '100%',\n  },\n  btn: {\n    // flex: 1,\n    paddingLeft: 15,\n    paddingRight: 15,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/MultipleModeBar.tsx",
    "content": "import { useState, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'\nimport { Animated, View, TouchableOpacity } from 'react-native'\n\nimport Text from '@/components/common/Text'\nimport Button from '@/components/common/Button'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport { BorderWidths } from '@/theme'\n\nexport type SelectMode = 'single' | 'range'\n\nexport interface MultipleModeBarProps {\n  onSwitchMode: (mode: SelectMode) => void\n  onSelectAll: (isAll: boolean) => void\n  onExitSelectMode: () => void\n}\nexport interface MultipleModeBarType {\n  show: () => void\n  setVisibleBar: (visible: boolean) => void\n  setIsSelectAll: (isAll: boolean) => void\n  setSwitchMode: (mode: SelectMode) => void\n  exitSelectMode: () => void\n}\n\nexport default forwardRef<MultipleModeBarType, MultipleModeBarProps>(({ onSelectAll, onSwitchMode, onExitSelectMode }, ref) => {\n  // const isGetDetailFailedRef = useRef(false)\n  const [visible, setVisible] = useState(false)\n  const [animatePlayed, setAnimatPlayed] = useState(true)\n  const animFade = useRef(new Animated.Value(0)).current\n  const animTranslateY = useRef(new Animated.Value(0)).current\n  const [selectMode, setSelectMode] = useState<SelectMode>('single')\n  const [isSelectAll, setIsSelectAll] = useState(false)\n  const [visibleBar, setVisibleBar] = useState(true)\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    show() {\n      handleShow()\n    },\n    setVisibleBar(visible) {\n      setVisibleBar(visible)\n    },\n    setIsSelectAll(isAll) {\n      setIsSelectAll(isAll)\n    },\n    setSwitchMode(mode: SelectMode) {\n      setSelectMode(mode)\n    },\n    exitSelectMode() {\n      handleHide()\n    },\n  }))\n\n  const handleShow = useCallback(() => {\n    // console.log('show List')\n    setVisible(true)\n    setAnimatPlayed(false)\n    requestAnimationFrame(() => {\n      animTranslateY.setValue(-20)\n\n      Animated.parallel([\n        Animated.timing(animFade, {\n          toValue: 0.92,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n        Animated.timing(animTranslateY, {\n          toValue: 0,\n          duration: 200,\n          useNativeDriver: true,\n        }),\n      ]).start(() => {\n        setAnimatPlayed(true)\n      })\n    })\n  }, [animFade, animTranslateY])\n\n  const handleHide = useCallback(() => {\n    setAnimatPlayed(false)\n    Animated.parallel([\n      Animated.timing(animFade, {\n        toValue: 0,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n      Animated.timing(animTranslateY, {\n        toValue: -20,\n        duration: 200,\n        useNativeDriver: true,\n      }),\n    ]).start(finished => {\n      if (!finished) return\n      setVisible(false)\n      setAnimatPlayed(true)\n    })\n  }, [animFade, animTranslateY])\n\n\n  const animaStyle = useMemo(() => ({\n    ...styles.container,\n    // backgroundColor: theme['c-content-background'],\n    borderBottomColor: theme['c-border-background'],\n    opacity: visibleBar ? animFade : 0, // Bind opacity to animated value\n    transform: [\n      { translateY: animTranslateY },\n    ],\n  }), [animFade, animTranslateY, theme, visibleBar])\n\n  const handleSelectAll = useCallback(() => {\n    const selectAll = !isSelectAll\n    setIsSelectAll(selectAll)\n    onSelectAll(selectAll)\n  }, [isSelectAll, onSelectAll])\n\n  const component = useMemo(() => {\n    return (\n      <Animated.View style={animaStyle}>\n        <View style={styles.switchBtn}>\n          <Button onPress={() => { onSwitchMode('single') }} style={{ ...styles.btn, backgroundColor: selectMode == 'single' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>\n            <Text color={theme['c-button-font']}>{global.i18n.t('list_select_single')}</Text>\n          </Button>\n          <Button onPress={() => { onSwitchMode('range') }} style={{ ...styles.btn, backgroundColor: selectMode == 'range' ? theme['c-button-background'] : 'rgba(0,0,0,0)' }}>\n            <Text color={theme['c-button-font']}>{global.i18n.t('list_select_range')}</Text>\n          </Button>\n        </View>\n        <TouchableOpacity onPress={handleSelectAll} style={styles.btn}>\n          <Text color={theme['c-button-font']}>{global.i18n.t(isSelectAll ? 'list_select_unall' : 'list_select_all')}</Text>\n        </TouchableOpacity>\n        <TouchableOpacity onPress={onExitSelectMode} style={styles.btn}>\n          <Text color={theme['c-button-font']}>{global.i18n.t('list_select_cancel')}</Text>\n        </TouchableOpacity>\n      </Animated.View>\n    )\n  }, [animaStyle, selectMode, theme, handleSelectAll, isSelectAll, onExitSelectMode, onSwitchMode])\n\n  return !visible && animatePlayed ? null : component\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    position: 'absolute',\n    left: 0,\n    top: 0,\n    width: '100%',\n    height: '100%',\n    flexDirection: 'row',\n    borderBottomWidth: BorderWidths.normal,\n  },\n  switchBtn: {\n    flexDirection: 'row',\n    flex: 1,\n  },\n  btn: {\n    // flex: 1,\n    paddingLeft: 18,\n    paddingRight: 18,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/MusicPositionModal.tsx",
    "content": "import { useState, useRef, useImperativeHandle, forwardRef } from 'react'\nimport { View } from 'react-native'\n\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\n\ninterface TitleType {\n  updateTitle: (musicInfo: SelectInfo['musicInfo'], selectedList: SelectInfo['selectedList']) => void\n}\nconst Title = forwardRef<TitleType, {}>((props, ref) => {\n  const [title, setTitle] = useState('')\n\n  useImperativeHandle(ref, () => ({\n    updateTitle(musicInfo, selectedList) {\n      setTitle(selectedList.length\n        ? global.i18n.t('change_position_music_multi_title', { num: selectedList.length })\n        : global.i18n.t('change_position_music_title', { name: musicInfo.name }))\n    },\n  }))\n\n  return (\n    <Text style={{ marginBottom: 5 }}>{title}</Text>\n  )\n})\n\ninterface PositionInputType {\n  getText: () => string\n  setText: (text: string) => void\n  focus: () => void\n}\nconst PositionInput = forwardRef<PositionInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setText(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      placeholder={t('change_position_tip')}\n      value={text}\n      onChangeText={setText}\n      ref={inputRef}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\n\nexport interface SelectInfo {\n  musicInfo: LX.Music.MusicInfo\n  selectedList: LX.Music.MusicInfo[]\n  index: number\n  listId: string\n  single: boolean\n}\nconst initSelectInfo = {}\n\ninterface MusicPositionModalProps {\n  onUpdatePosition: (info: SelectInfo, position: number) => void\n}\n\nexport interface MusicPositionModalType {\n  show: (listInfo: SelectInfo) => void\n}\n\n\nexport default forwardRef<MusicPositionModalType, MusicPositionModalProps>(({ onUpdatePosition }, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const titleRef = useRef<TitleType>(null)\n  const inputRef = useRef<PositionInputType>(null)\n  const selectedInfo = useRef<SelectInfo>(initSelectInfo as SelectInfo)\n  const [visible, setVisible] = useState(false)\n\n  const handleShow = () => {\n    alertRef.current?.setVisible(true)\n    requestAnimationFrame(() => {\n      titleRef.current?.updateTitle(selectedInfo.current.musicInfo, selectedInfo.current.selectedList)\n      setTimeout(() => {\n        inputRef.current?.focus()\n      }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show(listInfo) {\n      selectedInfo.current = listInfo\n\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n\n  const verify = () => {\n    let result = /^[1-9]\\d*/.exec(inputRef.current?.getText() ?? '')\n    let num = result ? parseInt(result[0]) : ''\n    inputRef.current?.setText(num.toString())\n    return num\n  }\n  const handleSetMusicPosition = () => {\n    let num = verify()\n    if (num == '') return\n    alertRef.current?.setVisible(false)\n    onUpdatePosition(selectedInfo.current, num as number - 1)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleSetMusicPosition}\n          onHide={() => inputRef.current?.setText('')}\n        >\n        <View style={styles.content}>\n          <Title ref={titleRef} />\n          <PositionInput ref={inputRef} />\n        </View>\n      </ConfirmAlert>\n      : null\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 260,\n    borderRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n  },\n\n  // tagTypeList: {\n  //   flexDirection: 'row',\n  //   flexWrap: 'wrap',\n  // },\n  // tagButton: {\n  //   // marginRight: 10,\n  //   borderRadius: 4,\n  //   marginRight: 10,\n  //   marginBottom: 10,\n  // },\n  // tagButtonText: {\n  //   paddingLeft: 12,\n  //   paddingRight: 12,\n  //   paddingTop: 8,\n  //   paddingBottom: 8,\n  // },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/MusicToggleModal.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useCallback, memo, useEffect } from 'react'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport { FlatList, ScrollView, TouchableOpacity, View, type FlatListProps as _FlatListProps } from 'react-native'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\nimport { Icon } from '@/components/common/Icon'\nimport { useHorizontalMode, useUnmounted } from '@/utils/hooks'\nimport { useI18n } from '@/lang'\nimport Button from '@/components/common/Button'\nimport { useSourceListI18n } from '@/components/SourceSelector'\nimport { searchMusic } from '@/utils/musicSdk'\nimport { toNewMusicInfo } from '@/utils'\nimport { handleShowMusicSourceDetail, handleToggleSource } from './listAction'\nimport { BorderRadius, BorderWidths } from '@/theme'\nimport playerState from '@/store/player/state'\nimport { LIST_IDS } from '@/config/constant'\nimport { addTempPlayList } from '@/core/player/tempPlayList'\nimport { playNext } from '@/core/player/player'\n\ntype FlatListProps = _FlatListProps<LX.Music.MusicInfoOnline>\nconst ITEM_HEIGHT = scaleSizeH(56)\n\n\nconst Tabs = <T extends LX.OnlineSource>({ list, source, onChangeSource }: {\n  list: T[]\n  source: T | ''\n  onChangeSource: (source: T) => void\n}) => {\n  const list_t = useSourceListI18n(list)\n  const theme = useTheme()\n  const scrollViewRef = useRef<ScrollView>(null)\n\n  return (\n    <ScrollView ref={scrollViewRef} style={styles.tabContainer} keyboardShouldPersistTaps={'always'} horizontal>\n      {\n        list_t.map(s => (\n          <TouchableOpacity\n            style={{ ...styles.tabButton, borderBottomColor: source == s.action ? theme['c-primary-background-active'] : 'transparent' }}\n            onPress={() => {\n              onChangeSource(s.action as T)\n            }}\n            key={s.action}\n          >\n            <Text style={styles.tabButtonText} color={source == s.action ? theme['c-primary-font-active'] : theme['c-font']}>{s.label}</Text>\n          </TouchableOpacity>\n        ))\n      }\n    </ScrollView>\n  )\n}\n\nconst Empty = ({ loading, error, onReload }: { loading: boolean, error: boolean, onReload: () => void }) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const label = loading\n    ? t('list_loading')\n    : error\n      ? t('list_error')\n      : t('no_item')\n  return (\n    <View style={styles.noitem}>\n      {\n        error ? (\n          <Text onPress={onReload} color={theme['c-font-label']}>{label}</Text>\n        ) : (\n          <Text color={theme['c-font-label']}>{label}</Text>\n        )\n      }\n    </View>\n  )\n}\n\nconst ListItem = memo(({ info, onPlay, onOpenDetail }: {\n  info: LX.Music.MusicInfoOnline\n  onPlay: (info: LX.Music.MusicInfoOnline) => void\n  onOpenDetail: (info: LX.Music.MusicInfoOnline) => void\n}) => {\n  const theme = useTheme()\n\n  return (\n    <View style={{ ...styles.listItem, height: ITEM_HEIGHT }} onStartShouldSetResponder={() => true}>\n      {/* <View style={styles.listItemLabel}>\n        <Text style={styles.sn} size={13} color={theme['c-300']}>{info.index + 1}</Text>\n      </View> */}\n      <View style={styles.listItemInfo}>\n        <Text color={theme['c-font']} size={14} numberOfLines={1}>{info.name}</Text>\n        <View style={styles.listItemAlbum}>\n          <Text color={theme['c-font']} size={12} numberOfLines={1}>\n            {info.singer}\n            {\n              info.meta.albumName ? (\n                <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>\n              ) : null\n            }\n          </Text>\n        </View>\n      </View>\n      <View style={styles.listItemLabel}>\n        {/* <Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{ info.source }</Text> */}\n        <Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{info.interval}</Text>\n      </View>\n      <View style={styles.listItemBtns}>\n        <Button style={styles.listItemBtn} onPress={() => { onOpenDetail(info) }}>\n          <Icon name=\"share\" style={{ color: theme['c-button-font'] }} size={18} />\n        </Button>\n        <Button style={styles.listItemBtn} onPress={() => { onPlay(info) }}>\n          <Icon name=\"play\" style={{ color: theme['c-button-font'] }} size={18} />\n        </Button>\n      </View>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return prevProps.info === nextProps.info\n})\n\nconst List = ({ source, lists, onPlay }: {\n  source: LX.OnlineSource | ''\n  lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>>\n  onPlay: (info: LX.Music.MusicInfoOnline) => void\n}) => {\n  const [list, setList] = useState<LX.Music.MusicInfoOnline[]>([])\n  const isFirstRef = useRef(true)\n  useEffect(() => {\n    if (isFirstRef.current) {\n      setList(lists[source as LX.OnlineSource] ?? [])\n      isFirstRef.current = false\n      return\n    }\n    requestAnimationFrame(() => {\n      setList(lists[source as LX.OnlineSource] ?? [])\n    })\n  }, [lists, source])\n\n  const openDetail = useCallback((musicInfo: LX.Music.MusicInfoOnline) => {\n    void handleShowMusicSourceDetail(musicInfo)\n  }, [])\n\n  const renderItem = useCallback(({ item }: { item: LX.Music.MusicInfoOnline, index: number }) => {\n    return <ListItem info={item} onPlay={onPlay} onOpenDetail={openDetail} />\n  }, [onPlay, openDetail])\n  const getkey = useCallback<NonNullable<FlatListProps['keyExtractor']>>(item => item.id, [])\n  const getItemLayout = useCallback<NonNullable<FlatListProps['getItemLayout']>>((data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }, [])\n\n  return (\n    <FlatList\n      style={styles.list}\n      maxToRenderPerBatch={4}\n      windowSize={8}\n      removeClippedSubviews={true}\n      initialNumToRender={12}\n      data={list}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      getItemLayout={getItemLayout}\n    />\n  )\n}\n\nconst SourceDetail = ({ info, onConfirm, toggleSource }: { info: LX.Music.MusicInfo, onConfirm: (info: LX.Music.MusicInfoOnline) => void, toggleSource: LX.Music.MusicInfoOnline | null }) => {\n  const theme = useTheme()\n  const isHorizontalMode = useHorizontalMode()\n  const t = useI18n()\n\n  return isHorizontalMode ? (\n    <View style={styles.detailContainer}>\n      <View style={styles.detailContainerX}>\n      <View style={styles.detailInfo}>\n        <View style={styles.detailInfoName}>\n          <Text style={styles.detailInfoNameText} color={theme['c-font']} size={13} numberOfLines={2}>\n            {info.name}\n          </Text>\n          <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.source}</Text>\n          <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.interval}</Text>\n        </View>\n        <View style={styles.listItemAlbum}>\n          <Text color={theme['c-font']} size={12} numberOfLines={1}>\n            {info.singer}\n            {\n              info.meta.albumName ? (\n                <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>\n              ) : null\n            }\n          </Text>\n        </View>\n      </View>\n      {\n        toggleSource ? (\n          <>\n            <Text>→</Text>\n            <View style={styles.detailInfo}>\n              <View style={styles.detailInfoName}>\n                <Text style={styles.detailInfoNameText} color={theme['c-font']} size={13} numberOfLines={2}>\n                  {toggleSource.name}\n                </Text>\n                <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.source}</Text>\n                <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.interval}</Text>\n              </View>\n              <View style={styles.listItemAlbum}>\n                <Text color={theme['c-font']} size={12} numberOfLines={1}>\n                  {toggleSource.singer}\n                  {\n                    toggleSource.meta.albumName ? (\n                      <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({toggleSource.meta.albumName})</Text>\n                    ) : null\n                  }\n                </Text>\n              </View>\n            </View>\n          </>\n        ) : null\n      }\n      </View>\n      <Button\n        onPress={() => {\n          onConfirm(toggleSource!)\n        }}\n        style={{ ...styles.button, backgroundColor: theme['c-button-background'] }}\n        disabled={!toggleSource}\n      >\n        <Text color={theme['c-button-font']}>{t('music_toggle__confirm')}</Text>\n      </Button>\n    </View>\n  ) : (\n    <View style={styles.detailContainer}>\n      <View style={styles.detailContainerY}>\n        <View style={styles.detailInfo}>\n          <View style={styles.detailInfoName}>\n            <Text style={styles.detailInfoNameText} color={theme['c-font']} size={14} numberOfLines={2}>\n              {info.name}\n            </Text>\n            <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.source}</Text>\n            <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.interval}</Text>\n          </View>\n          <View style={styles.listItemAlbum}>\n            <Text color={theme['c-font']} size={12} numberOfLines={1}>\n              {info.singer}\n              {\n                info.meta.albumName ? (\n                  <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>\n                ) : null\n              }\n            </Text>\n          </View>\n        </View>\n        {\n          toggleSource ? (\n            <>\n              <Text>↓</Text>\n              <View style={styles.detailInfo}>\n                <View style={styles.detailInfoName}>\n                  <Text style={styles.detailInfoNameText} color={theme['c-font']} size={14} numberOfLines={2}>\n                    {toggleSource.name}\n                  </Text>\n                  <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.source}</Text>\n                  <Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.interval}</Text>\n                </View>\n                <View style={styles.listItemAlbum}>\n                  <Text color={theme['c-font']} size={12} numberOfLines={1}>\n                    {toggleSource.singer}\n                    {\n                      toggleSource.meta.albumName ? (\n                        <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({toggleSource.meta.albumName})</Text>\n                      ) : null\n                    }\n                  </Text>\n                </View>\n              </View>\n            </>\n          ) : null\n        }\n      </View>\n      <Button\n        onPress={() => {\n          onConfirm(toggleSource!)\n        }}\n        style={{ ...styles.button, backgroundColor: theme['c-button-background'] }}\n        disabled={!toggleSource || toggleSource.id == info.id}\n      >\n        <Text color={theme['c-button-font']}>{t('music_toggle__confirm')}</Text>\n      </Button>\n    </View>\n  )\n}\n\n\ninterface ModalType {\n  show: (info: SelectInfo) => void\n}\nconst initInfo = {}\n\nconst Modal = forwardRef<ModalType, {}>((props, ref) => {\n  const infoRef = useRef<SelectInfo>(initInfo as SelectInfo)\n  const [sourceInfo, setSourceInfo] = useState<{\n    sourceInfo: LX.OnlineSource[]\n    lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>>\n    loading: boolean\n    error: boolean\n  }>({ sourceInfo: [], lists: {}, loading: false, error: false })\n  const [source, setSource] = useState<LX.OnlineSource | ''>('')\n  const dialogRef = useRef<DialogType>(null)\n  const isUnmountedRef = useUnmounted()\n  const [toggleSource, setToggleSource] = useState<LX.Music.MusicInfoOnline | null>(null)\n\n  const handlePlay = useCallback((musicInfo: LX.Music.MusicInfoOnline) => {\n    setToggleSource(musicInfo)\n    const isPlaying = !!playerState.playMusicInfo.musicInfo\n    addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])\n    if (isPlaying) void playNext()\n  }, [])\n\n  const loadData = useCallback((selectInfo: SelectInfo = infoRef.current) => {\n    setSourceInfo({ sourceInfo: [], lists: {}, loading: true, error: false })\n    searchMusic({\n      name: selectInfo.musicInfo.name,\n      singer: selectInfo.musicInfo.singer,\n      source: '',\n    }).then((result: Array<{ source: LX.OnlineSource, list: LX.Music.MusicInfoOnline[] }>) => {\n      if (isUnmountedRef.current) return\n      const tags: LX.OnlineSource[] = []\n      const lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>> = {}\n      for (const s of result) {\n        tags.push(s.source)\n        lists[s.source] = s.list.map(s => toNewMusicInfo(s) as LX.Music.MusicInfoOnline)\n      }\n      setSourceInfo({ sourceInfo: tags, lists, loading: false, error: false })\n      if (tags.length) setSource(tags[0])\n    }).catch(() => {\n      if (isUnmountedRef.current) return\n      setSourceInfo({ ...sourceInfo, error: true })\n    })\n  }, [isUnmountedRef, sourceInfo])\n  useImperativeHandle(ref, () => ({\n    show(info) {\n      infoRef.current = info\n      setSource('')\n      loadData(info)\n      requestAnimationFrame(() => {\n        dialogRef.current?.setVisible(true)\n      })\n    },\n  }))\n\n  const confirmToggleSource = useCallback(async(musicInfo: LX.Music.MusicInfoOnline) => {\n    const isClose = await handleToggleSource(infoRef.current.listId, infoRef.current.musicInfo, musicInfo)\n    if (isClose) dialogRef.current?.setVisible(false)\n  }, [])\n\n  return (\n    <Dialog ref={dialogRef}>\n      <View style={styles.container}>\n        {\n          sourceInfo.sourceInfo.length\n            ? (<>\n                <Tabs\n                  list={sourceInfo.sourceInfo}\n                  source={source}\n                  onChangeSource={setSource}\n                />\n                <List\n                  source={source}\n                  lists={sourceInfo.lists}\n                  onPlay={handlePlay}\n                />\n              </>)\n            : <Empty loading={sourceInfo.loading} error={sourceInfo.error} onReload={loadData} />\n        }\n        <SourceDetail info={infoRef.current.musicInfo} onConfirm={confirmToggleSource} toggleSource={toggleSource} />\n      </View>\n    </Dialog>\n  )\n})\n\nexport interface SelectInfo {\n  musicInfo: LX.Music.MusicInfo\n  listId: string\n}\nexport interface MusicToggleModalType {\n  show: (listInfo: SelectInfo) => void\n}\n\nexport default forwardRef<MusicToggleModalType, {}>((props, ref) => {\n  const musicAddModalRef = useRef<ModalType>(null)\n  const [visible, setVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(musicInfo) {\n      if (visible) musicAddModalRef.current?.show(musicInfo)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          musicAddModalRef.current?.show(musicInfo)\n        })\n      }\n    },\n  }))\n\n  return (\n    visible\n      ? <Modal ref={musicAddModalRef} />\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    flexGrow: 1,\n    flexShrink: 1,\n    width: 600,\n    maxWidth: '100%',\n  },\n  tabContainer: {\n    flexGrow: 0,\n    flexShrink: 0,\n    // paddingLeft: 5,\n    // paddingRight: 5,\n    paddingVertical: 6,\n  },\n  tabButton: {\n    // height: 38,\n    // lineHeight: 38,\n    justifyContent: 'center',\n    paddingHorizontal: 6,\n    // width: 80,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    borderBottomWidth: BorderWidths.normal3,\n  },\n  tabButtonText: {\n    // height: 38,\n    // lineHeight: 38,\n    textAlign: 'center',\n    paddingHorizontal: 2,\n    paddingVertical: 5,\n  },\n  list: {\n    flexGrow: 1,\n    flexShrink: 1,\n  },\n  listItem: {\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    alignItems: 'center',\n  },\n  // sn: {\n  //   width: 38,\n  //   // fontSize: 12,\n  //   textAlign: 'center',\n  //   // backgroundColor: 'rgba(0,0,0,0.2)',\n  //   paddingLeft: 3,\n  //   paddingRight: 3,\n  // },\n  listItemInfo: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    paddingLeft: 15,\n    paddingRight: 5,\n  },\n  listItemAlbum: {\n    flexDirection: 'row',\n    marginTop: 3,\n  },\n  listItemLabel: {\n    flex: 0,\n  },\n  listItemLabelText: {\n    paddingHorizontal: 5,\n  },\n  listItemBtns: {\n    flex: 0,\n    flexDirection: 'row',\n    gap: 5,\n    paddingHorizontal: 8,\n  },\n  listItemBtn: {\n    padding: 8,\n  },\n\n  detailContainer: {\n    flexDirection: 'row',\n    gap: 5,\n    alignItems: 'center',\n    paddingHorizontal: 10,\n    paddingVertical: 10,\n  },\n  detailContainerY: {\n    flexDirection: 'column',\n    flexGrow: 1,\n    flexShrink: 1,\n    gap: 5,\n  },\n  detailContainerX: {\n    flexDirection: 'row',\n    flexGrow: 1,\n    flexShrink: 1,\n    gap: 5,\n    alignItems: 'center',\n  },\n  detailInfo: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'column',\n    // width: '50%',\n    justifyContent: 'center',\n  },\n  detailInfoName: {\n    gap: 5,\n    flexDirection: 'row',\n    flexGrow: 0,\n    flexShrink: 1,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  detailInfoNameText: {\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    flexShrink: 1,\n    flexGrow: 0,\n  },\n  detailInfoLabelText: {\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  noitem: {\n    flexGrow: 1,\n    flexShrink: 1,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n  button: {\n    borderRadius: BorderRadius.normal,\n    paddingHorizontal: 10,\n    paddingVertical: 8,\n    alignItems: 'center',\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/index.tsx",
    "content": "import { useCallback, useRef } from 'react'\n\nimport listState from '@/store/list/state'\nimport ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu'\nimport { handleDislikeMusic, handlePlay, handlePlayLater, handleRemove, handleShare, handleShowMusicSourceDetail, handleUpdateMusicInfo, handleUpdateMusicPosition } from './listAction'\nimport List, { type ListType } from './List'\nimport ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal'\nimport ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal'\nimport { createStyle } from '@/utils/tools'\nimport { type LayoutChangeEvent, View } from 'react-native'\nimport ActiveList, { type ActiveListType } from './ActiveList'\nimport MultipleModeBar, { type SelectMode, type MultipleModeBarType } from './MultipleModeBar'\nimport ListSearchBar, { type ListSearchBarType } from './ListSearchBar'\nimport ListMusicSearch, { type ListMusicSearchType } from './ListMusicSearch'\nimport MusicPositionModal, { type MusicPositionModalType } from './MusicPositionModal'\nimport MetadataEditModal, { type MetadataEditType, type MetadataEditProps } from '@/components/MetadataEditModal'\nimport MusicToggleModal, { type MusicToggleModalType } from './MusicToggleModal'\n\n\nexport default () => {\n  // const t = useI18n()\n  const activeListRef = useRef<ActiveListType>(null)\n  const listMusicSearchRef = useRef<ListMusicSearchType>(null)\n  const listRef = useRef<ListType>(null)\n  const multipleModeBarRef = useRef<MultipleModeBarType>(null)\n  const listSearchBarRef = useRef<ListSearchBarType>(null)\n  const listMusicAddRef = useRef<ListMusicAddType>(null)\n  const listMusicMultiAddRef = useRef<ListAddMultiType>(null)\n  const musicPositionModalRef = useRef<MusicPositionModalType>(null)\n  const metadataEditTypeRef = useRef<MetadataEditType>(null)\n  const listMenuRef = useRef<ListMenuType>(null)\n  const musicToggleModalRef = useRef<MusicToggleModalType>(null)\n  const layoutHeightRef = useRef<number>(0)\n  const isShowMultipleModeBar = useRef(false)\n  const isShowSearchBarModeBar = useRef(false)\n  const selectedInfoRef = useRef<SelectInfo>()\n  // console.log('render index list')\n\n  const hancelMultiSelect = useCallback(() => {\n    if (isShowSearchBarModeBar.current) {\n      multipleModeBarRef.current?.setVisibleBar(false)\n    } else activeListRef.current?.setVisibleBar(false)\n    isShowMultipleModeBar.current = true\n    multipleModeBarRef.current?.show()\n    listRef.current?.setIsMultiSelectMode(true)\n  }, [])\n  const hancelExitSelect = useCallback(() => {\n    if (isShowSearchBarModeBar.current) {\n      multipleModeBarRef.current?.setVisibleBar(true)\n    } else activeListRef.current?.setVisibleBar(true)\n    // console.log('hancelExitSelect', isShowSearchBarModeBar.current)\n    multipleModeBarRef.current?.exitSelectMode()\n    listRef.current?.setIsMultiSelectMode(false)\n    isShowMultipleModeBar.current = false\n  }, [])\n  const hancelSwitchSelectMode = useCallback((mode: SelectMode) => {\n    multipleModeBarRef.current?.setSwitchMode(mode)\n    listRef.current?.setSelectMode(mode)\n  }, [])\n  const hancelScrollToTop = useCallback(() => {\n    listRef.current?.scrollToTop()\n  }, [])\n\n  const showMenu = useCallback((musicInfo: LX.Music.MusicInfo, index: number, position: Position) => {\n    listMenuRef.current?.show({\n      musicInfo,\n      index,\n      listId: listState.activeListId,\n      single: false,\n      selectedList: listRef.current!.getSelectedList(),\n    }, position)\n  }, [])\n  const handleShowSearch = useCallback(() => {\n    isShowSearchBarModeBar.current = true\n    if (isShowMultipleModeBar.current) {\n      multipleModeBarRef.current?.setVisibleBar(false)\n    } else activeListRef.current?.setVisibleBar(false)\n    listSearchBarRef.current?.show()\n  }, [])\n  const handleExitSearch = useCallback(() => {\n    isShowSearchBarModeBar.current = false\n    listMusicSearchRef.current?.hide()\n    listSearchBarRef.current?.hide()\n    // console.log('handleExitSearch', isShowMultipleModeBar.current)\n    if (isShowMultipleModeBar.current) {\n      multipleModeBarRef.current?.setVisibleBar(true)\n    } else activeListRef.current?.setVisibleBar(true)\n  }, [])\n  const handleScrollToInfo = useCallback((info: LX.Music.MusicInfo) => {\n    listRef.current?.scrollToInfo(info)\n    handleExitSearch()\n  }, [handleExitSearch])\n  const onLayout = useCallback((e: LayoutChangeEvent) => {\n    layoutHeightRef.current = e.nativeEvent.layout.height\n  }, [])\n\n  const handleAddMusic = useCallback((info: SelectInfo) => {\n    if (info.selectedList.length) {\n      listMusicMultiAddRef.current?.show({ selectedList: info.selectedList, listId: info.listId, isMove: false })\n    } else {\n      listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: info.listId, isMove: false })\n    }\n  }, [])\n  const handleMoveMusic = useCallback((info: SelectInfo) => {\n    if (info.selectedList.length) {\n      listMusicMultiAddRef.current?.show({ selectedList: info.selectedList, listId: info.listId, isMove: true })\n    } else {\n      listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: info.listId, isMove: true })\n    }\n  }, [])\n  const handleEditMetadata = useCallback((info: SelectInfo) => {\n    if (info.musicInfo.source != 'local') return\n    selectedInfoRef.current = info\n    metadataEditTypeRef.current?.show(info.musicInfo.meta.filePath)\n  }, [])\n  const handleUpdateMetadata = useCallback<MetadataEditProps['onUpdate']>((info) => {\n    if (!selectedInfoRef.current || selectedInfoRef.current.musicInfo.source != 'local') return\n    handleUpdateMusicInfo(selectedInfoRef.current.listId, selectedInfoRef.current.musicInfo, info)\n  }, [])\n\n\n  return (\n    <View style={styles.container}>\n      <View style={{ zIndex: 2 }}>\n        <ActiveList ref={activeListRef} onShowSearchBar={handleShowSearch} onScrollToTop={hancelScrollToTop} />\n        <MultipleModeBar\n          ref={multipleModeBarRef}\n          onSwitchMode={hancelSwitchSelectMode}\n          onSelectAll={isAll => listRef.current?.selectAll(isAll)}\n          onExitSelectMode={hancelExitSelect}\n        />\n        <ListSearchBar\n          ref={listSearchBarRef}\n          onSearch={keyword => listMusicSearchRef.current?.search(keyword, layoutHeightRef.current)}\n          onExitSearch={handleExitSearch}\n        />\n      </View>\n      <View style={{ flex: 1 }} onLayout={onLayout}>\n        <List\n          ref={listRef}\n          onShowMenu={showMenu}\n          onMuiltSelectMode={hancelMultiSelect}\n          onSelectAll={isAll => multipleModeBarRef.current?.setIsSelectAll(isAll)}\n        />\n        <ListMusicSearch\n          ref={listMusicSearchRef}\n          onScrollToInfo={handleScrollToInfo}\n        />\n      </View>\n      <ListMusicAdd ref={listMusicAddRef} onAdded={hancelExitSelect} />\n      <ListMusicMultiAdd ref={listMusicMultiAddRef} onAdded={hancelExitSelect} />\n      <MusicPositionModal ref={musicPositionModalRef}\n        onUpdatePosition={(info, postion) => { handleUpdateMusicPosition(postion, info.listId, info.musicInfo, info.selectedList, hancelExitSelect) }} />\n      <ListMenu\n        ref={listMenuRef}\n        onPlay={info => { handlePlay(info.listId, info.index) }}\n        onPlayLater={info => { hancelExitSelect(); handlePlayLater(info.listId, info.musicInfo, info.selectedList, hancelExitSelect) }}\n        onRemove={info => { hancelExitSelect(); handleRemove(info.listId, info.musicInfo, info.selectedList, hancelExitSelect) }}\n        onDislikeMusic={info => { void handleDislikeMusic(info.musicInfo) }}\n        onCopyName={info => { handleShare(info.musicInfo) }}\n        onMusicSourceDetail={info => { void handleShowMusicSourceDetail(info.musicInfo) }}\n        onAdd={handleAddMusic}\n        onMove={handleMoveMusic}\n        onEditMetadata={handleEditMetadata}\n        onChangePosition={info => musicPositionModalRef.current?.show(info)}\n        onToggleSource={info => musicToggleModalRef.current?.show(info)}\n      />\n      <MetadataEditModal\n        ref={metadataEditTypeRef}\n        onUpdate={handleUpdateMetadata}\n      />\n      <MusicToggleModal ref={musicToggleModalRef} />\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    flexDirection: 'column',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MusicList/listAction.ts",
    "content": "import { addListMusics, removeListMusics, updateListMusicPosition, updateListMusics } from '@/core/list'\nimport { playList, playListById, playNext } from '@/core/player/player'\nimport { addTempPlayList } from '@/core/player/tempPlayList'\nimport settingState from '@/store/setting/state'\nimport { similar, sortInsert, toOldMusicInfo } from '@/utils'\nimport { confirmDialog, openUrl, shareMusic, toast } from '@/utils/tools'\nimport { addDislikeInfo, hasDislike } from '@/core/dislikeList'\nimport playerState from '@/store/player/state'\n\nimport type { SelectInfo } from './ListMenu'\nimport { type Metadata } from '@/components/MetadataEditModal'\nimport musicSdk from '@/utils/musicSdk'\nimport { getListMusicSync } from '@/utils/listManage'\n\nexport const handlePlay = (listId: SelectInfo['listId'], index: SelectInfo['index']) => {\n  void playList(listId, index)\n}\nexport const handlePlayLater = (listId: SelectInfo['listId'], musicInfo: SelectInfo['musicInfo'], selectedList: SelectInfo['selectedList'], onCancelSelect: () => void) => {\n  if (selectedList.length) {\n    addTempPlayList(selectedList.map(s => ({ listId, musicInfo: s })))\n    onCancelSelect()\n  } else {\n    addTempPlayList([{ listId, musicInfo }])\n  }\n}\n\nexport const handleRemove = (listId: SelectInfo['listId'], musicInfo: SelectInfo['musicInfo'], selectedList: SelectInfo['selectedList'], onCancelSelect: () => void) => {\n  if (selectedList.length) {\n    void confirmDialog({\n      message: global.i18n.t('list_remove_music_multi_tip', { num: selectedList.length }),\n      confirmButtonText: global.i18n.t('list_remove_tip_button'),\n    }).then(isRemove => {\n      if (!isRemove) return\n      void removeListMusics(listId, selectedList.map(s => s.id))\n      onCancelSelect()\n    })\n  } else {\n    void removeListMusics(listId, [musicInfo.id])\n  }\n}\n\nexport const handleUpdateMusicPosition = (position: number, listId: SelectInfo['listId'], musicInfo: SelectInfo['musicInfo'], selectedList: SelectInfo['selectedList'], onCancelSelect: () => void) => {\n  if (selectedList.length) {\n    void updateListMusicPosition(listId, position, selectedList.map(s => s.id))\n    onCancelSelect()\n  } else {\n    // console.log(listId, position, [musicInfo.id])\n    void updateListMusicPosition(listId, position, [musicInfo.id])\n  }\n}\n\nexport const handleUpdateMusicInfo = (listId: SelectInfo['listId'], musicInfo: LX.Music.MusicInfoLocal, newInfo: Metadata) => {\n  void updateListMusics([\n    {\n      id: listId,\n      musicInfo: {\n        ...musicInfo,\n        name: newInfo.name,\n        singer: newInfo.singer,\n        meta: {\n          ...musicInfo.meta,\n          albumName: newInfo.albumName,\n        },\n      },\n    },\n  ])\n}\n\n\nexport const handleShare = (musicInfo: SelectInfo['musicInfo']) => {\n  shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo)\n}\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\nexport const handleShowMusicSourceDetail = async(minfo: SelectInfo['musicInfo']) => {\n  const url = musicSdk[minfo.source as LX.OnlineSource]?.getMusicDetailPageUrl(toOldMusicInfo(minfo))\n  if (!url) return\n  void openUrl(url)\n}\n\n\nexport const handleDislikeMusic = async(musicInfo: SelectInfo['musicInfo']) => {\n  const confirm = await confirmDialog({\n    message: musicInfo.singer ? global.i18n.t('lists_dislike_music_singer_tip', { name: musicInfo.name, singer: musicInfo.singer }) : global.i18n.t('lists_dislike_music_tip', { name: musicInfo.name }),\n    cancelButtonText: global.i18n.t('cancel_button_text_2'),\n    confirmButtonText: global.i18n.t('confirm_button_text'),\n    bgClose: false,\n  })\n  if (!confirm) return\n  await addDislikeInfo([{ name: musicInfo.name, singer: musicInfo.singer }])\n  toast(global.i18n.t('lists_dislike_music_add_tip'))\n  if (hasDislike(playerState.playMusicInfo.musicInfo)) {\n    void playNext(true)\n  }\n}\n\nexport const handleToggleSource = async(listId: string, musicInfo: LX.Music.MusicInfo, toggleMusicInfo: LX.Music.MusicInfoOnline) => {\n  const list = getListMusicSync(listId)\n  const oldId = musicInfo.id\n  let oldIdx = list.findIndex(m => m.id == oldId)\n  if (oldIdx < 0) {\n    void addListMusics(listId, [toggleMusicInfo], settingState.setting['list.addMusicLocationType'])\n    return true\n  }\n  const id = toggleMusicInfo.id\n  const index = list.findIndex(m => m.id == id)\n  const removeIds = [oldId]\n  if (index > -1) {\n    if (!await confirmDialog({\n      message: global.i18n.t('music_toggle__duplicate_tip'),\n      cancelButtonText: global.i18n.t('dialog_cancel'),\n      confirmButtonText: global.i18n.t('dialog_confirm'),\n    })) return false\n    removeIds.push(id)\n  }\n  void removeListMusics(listId, removeIds).then(async() => {\n    await addListMusics(listId, [toggleMusicInfo], 'bottom')\n    if (index != -1 && index < oldIdx) oldIdx--\n    await updateListMusicPosition(listId, oldIdx, [id])\n    if (playerState.playMusicInfo.listId == listId && playerState.playMusicInfo.musicInfo?.id == oldId) {\n      void playListById(listId, toggleMusicInfo.id)\n    }\n  })\n  return true\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useCallback, memo, useEffect } from 'react'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport { FlatList, TouchableOpacity, View, type FlatListProps as _FlatListProps } from 'react-native'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\nimport { type DuplicateMusicItem, filterDuplicateMusic } from './utils'\nimport { getListMusics, removeListMusics } from '@/core/list'\nimport { Icon } from '@/components/common/Icon'\nimport { useUnmounted } from '@/utils/hooks'\nimport { playList } from '@/core/player/player'\nimport { useI18n } from '@/lang'\nimport { handleRemove } from '../MusicList/listAction'\nimport Button from '@/components/common/Button'\n\ntype FlatListProps = _FlatListProps<DuplicateMusicItem>\nconst ITEM_HEIGHT = scaleSizeH(56)\n\nconst Title = ({ title }: {\n  title: string\n}) => {\n  return (\n    <Text style={styles.title} size={16}>\n      {title}\n    </Text>\n  )\n}\n\nconst Empty = () => {\n  const theme = useTheme()\n  const t = useI18n()\n\n  return (\n    <View style={styles.noitem}>\n      <Text color={theme['c-font-label']}>{t('no_item')}</Text>\n    </View>\n  )\n}\n\nconst ListItem = memo(({ info, index, onRemove, onPlay, selectedList, onPress }: {\n  info: DuplicateMusicItem\n  index: number\n  selectedList: DuplicateMusicItem[]\n  onPlay: (info: DuplicateMusicItem) => void\n  onRemove: (idx: number) => void\n  onPress: (info: DuplicateMusicItem) => void\n}) => {\n  const theme = useTheme()\n  const isSelected = selectedList.includes(info)\n\n  return (\n    <View style={{ ...styles.listItem, height: ITEM_HEIGHT, backgroundColor: isSelected ? theme['c-primary-background-hover'] : 'rgba(0,0,0,0)' }} onStartShouldSetResponder={() => true}>\n      {/* <View style={styles.listItemLabel}>\n        <Text style={styles.sn} size={13} color={theme['c-300']}>{info.index + 1}</Text>\n      </View> */}\n      <TouchableOpacity style={styles.listItemInfo} onPress={() => { onPress(info) }}>\n        <Text color={theme['c-font']} size={14} numberOfLines={1}>{info.musicInfo.name}</Text>\n        <View style={styles.listItemAlbum}>\n          <Text color={theme['c-font']} size={12} numberOfLines={1}>\n            {info.musicInfo.singer}\n            {\n              info.musicInfo.meta.albumName ? (\n                <Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.musicInfo.meta.albumName})</Text>\n              ) : null\n            }\n          </Text>\n        </View>\n      </TouchableOpacity>\n      <View style={styles.listItemLabel}>\n        <Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{ info.musicInfo.source }</Text>\n        <Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{info.musicInfo.interval}</Text>\n      </View>\n      <View style={styles.listItemBtns}>\n        <Button style={styles.listItemBtn} onPress={() => { onPlay(info) }}>\n          <Icon name=\"play-outline\" style={{ color: theme['c-button-font'] }} size={18} />\n        </Button>\n        <Button style={styles.listItemBtn} onPress={() => { onRemove(index) }}>\n          <Icon name=\"remove\" style={{ color: theme['c-button-font'] }} size={18} />\n        </Button>\n      </View>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return prevProps.info === nextProps.info &&\n  prevProps.index === nextProps.index &&\n  nextProps.selectedList.includes(nextProps.info) == prevProps.selectedList.includes(nextProps.info)\n})\n\nconst handleRemoveList = (list: DuplicateMusicItem[], index: number) => {\n  let prev = list[index - 1]\n  let cur = list[index]\n  let next = list[index + 1]\n  let count = 1\n  if (prev?.group != cur.group) {\n    if (next?.group == cur.group && list[index + 2]?.group != cur.group) {\n      count = 2\n    }\n  } else if (next?.group != cur.group) {\n    if (prev?.group == cur.group && list[index - 2]?.group != cur.group) {\n      index -= 1\n      count = 2\n    }\n  }\n\n  return list.splice(index, count)\n}\nconst List = ({ listId }: { listId: string }) => {\n  const [list, setList] = useState<DuplicateMusicItem[]>([])\n  const [selectedList, setSelectedList] = useState<DuplicateMusicItem[]>([])\n  const dataRef = useRef<[DuplicateMusicItem[], DuplicateMusicItem[]]>([[], []])\n  const isUnmountedRef = useUnmounted()\n\n  const handleFilterList = useCallback(() => {\n    if (isUnmountedRef.current) return\n    void getListMusics(listId).then((list) => {\n      if (isUnmountedRef.current) return\n      void filterDuplicateMusic(list).then((l) => {\n        if (isUnmountedRef.current) return\n        setSelectedList(dataRef.current[1] = [])\n        setList(dataRef.current[0] = l)\n      })\n    })\n  }, [isUnmountedRef, listId])\n  const handlePlay = useCallback((info: DuplicateMusicItem) => {\n    const { musicInfo } = info\n    void getListMusics(listId).then((list) => {\n      const idx = list.findIndex(m => m.id == musicInfo.id)\n      if (idx < 0) return\n      void playList(listId, idx)\n    })\n  }, [listId])\n  const handleRemovePress = useCallback((index: number) => {\n    const selectedList = dataRef.current[1]\n    const list = dataRef.current[0]\n    if (selectedList.length) {\n      handleRemove(listId, list[index].musicInfo, selectedList.map(m => m.musicInfo), () => {\n        let newList = [...list]\n        for (const item of selectedList) {\n          let idx = newList.indexOf(item)\n          if (idx < 0) continue\n          handleRemoveList(newList, idx)\n        }\n        setList(dataRef.current[0] = newList)\n        setSelectedList(dataRef.current[1] = [])\n      })\n      return\n    }\n    let newList = [...list]\n    let curItem = list[index]\n    const rmItem = handleRemoveList(newList, index)\n    let newSelectList = [...selectedList]\n    for (const item of rmItem) {\n      let idx = newSelectList.indexOf(item)\n      if (idx < 0) continue\n      newSelectList.splice(idx, 1)\n    }\n    setSelectedList(dataRef.current[1] = newSelectList)\n\n    requestAnimationFrame(() => {\n      void removeListMusics(listId, [curItem.musicInfo.id])\n    })\n    setList(dataRef.current[0] = newList)\n  }, [listId])\n  const handleSelect = useCallback((info: DuplicateMusicItem) => {\n    setSelectedList(selectedList => {\n      let nList = [...selectedList]\n      let idx = nList.indexOf(info)\n      if (idx < 0) nList.push(info)\n      else nList.splice(idx, 1)\n      dataRef.current[1] = nList\n      return nList\n    })\n  }, [])\n\n  useEffect(handleFilterList, [handleFilterList])\n\n  const renderItem = useCallback(({ item, index }: { item: DuplicateMusicItem, index: number }) => {\n    return <ListItem info={item} index={index} onPlay={handlePlay} onRemove={handleRemovePress} selectedList={selectedList} onPress={handleSelect} />\n  }, [handlePlay, handleRemovePress, handleSelect, selectedList])\n  const getkey = useCallback<NonNullable<FlatListProps['keyExtractor']>>(item => item.id, [])\n  const getItemLayout = useCallback<NonNullable<FlatListProps['getItemLayout']>>((data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }, [])\n\n  return (\n    list.length ? (\n      <FlatList\n        style={styles.list}\n        maxToRenderPerBatch={4}\n        windowSize={8}\n        removeClippedSubviews={true}\n        initialNumToRender={12}\n        data={list}\n        renderItem={renderItem}\n        keyExtractor={getkey}\n        getItemLayout={getItemLayout}\n      />\n    ) : (\n      <Empty />\n    )\n  )\n}\n\nexport interface ModalType {\n  show: (info: LX.List.MyListInfo) => void\n}\nconst initInfo = {}\n\nconst Modal = forwardRef<ModalType, {}>((props, ref) => {\n  const [info, setInfo] = useState<LX.List.MyListInfo>(initInfo as LX.List.MyListInfo)\n  const dialogRef = useRef<DialogType>(null)\n  useImperativeHandle(ref, () => ({\n    show(info) {\n      setInfo(info)\n\n      requestAnimationFrame(() => {\n        dialogRef.current?.setVisible(true)\n      })\n    },\n  }))\n  const handleHide = () => {\n    requestAnimationFrame(() => {\n      const ninfo = { ...info, name: '' }\n      setInfo(ninfo as LX.List.MyListInfo)\n    })\n  }\n\n  return (\n    <Dialog ref={dialogRef} onHide={handleHide}>\n      {\n        info.name\n          ? (<>\n              <Title title={info.name} />\n              <List listId={info.id} />\n            </>)\n          : null\n      }\n    </Dialog>\n  )\n})\n\nexport interface DuplicateMusicType {\n  show: (info: LX.List.MyListInfo) => void\n}\n\nexport default forwardRef<DuplicateMusicType, {}>((props, ref) => {\n  const musicAddModalRef = useRef<ModalType>(null)\n  const [visible, setVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(listInfo) {\n      if (visible) musicAddModalRef.current?.show(listInfo)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          musicAddModalRef.current?.show(listInfo)\n        })\n      }\n    },\n  }))\n\n  return (\n    visible\n      ? <Modal ref={musicAddModalRef} />\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    // flexGrow: 1,\n  },\n  title: {\n    textAlign: 'center',\n    paddingVertical: 15,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  list: {\n    flexGrow: 0,\n  },\n  listItem: {\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    alignItems: 'center',\n  },\n  // sn: {\n  //   width: 38,\n  //   // fontSize: 12,\n  //   textAlign: 'center',\n  //   // backgroundColor: 'rgba(0,0,0,0.2)',\n  //   paddingLeft: 3,\n  //   paddingRight: 3,\n  // },\n  listItemInfo: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n    paddingLeft: 15,\n    paddingRight: 5,\n  },\n  listItemAlbum: {\n    flexDirection: 'row',\n    marginTop: 3,\n  },\n  listItemLabel: {\n    flex: 0,\n  },\n  listItemLabelText: {\n    paddingHorizontal: 5,\n  },\n  listItemBtns: {\n    flex: 0,\n    flexDirection: 'row',\n    gap: 5,\n    paddingHorizontal: 8,\n  },\n  listItemBtn: {\n    padding: 8,\n  },\n  noitem: {\n    paddingVertical: 35,\n    alignItems: 'center',\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/List.tsx",
    "content": "import { memo, useEffect, useRef } from 'react'\nimport { View, TouchableOpacity, FlatList, type NativeScrollEvent, type NativeSyntheticEvent, type FlatListProps } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\n\nimport { useTheme } from '@/store/theme/hook'\nimport { useActiveListId, useListFetching, useMyList } from '@/store/list/hook'\nimport { createStyle } from '@/utils/tools'\nimport { LIST_SCROLL_POSITION_KEY } from '@/config/constant'\nimport { getListPosition, saveListPosition } from '@/utils/data'\nimport { setActiveList } from '@/core/list'\nimport Text from '@/components/common/Text'\nimport { type Position } from './ListMenu'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport Loading from '@/components/common/Loading'\n\ntype FlatListType = FlatListProps<LX.List.MyListInfo>\n\nconst ITEM_HEIGHT = scaleSizeH(40)\n\nconst ListItem = memo(({ item, index, activeId, onPress, onShowMenu }: {\n  onPress: (item: LX.List.MyListInfo) => void\n  index: number\n  activeId: string\n  item: LX.List.MyListInfo\n  onShowMenu: (item: LX.List.MyListInfo, index: number, position: { x: number, y: number, w: number, h: number }) => void\n}) => {\n  const theme = useTheme()\n  const moreButtonRef = useRef<TouchableOpacity>(null)\n  const fetching = useListFetching(item.id)\n\n  const active = activeId == item.id\n\n  const handleShowMenu = () => {\n    if (moreButtonRef.current?.measure) {\n      moreButtonRef.current.measure((fx, fy, width, height, px, py) => {\n        // console.log(fx, fy, width, height, px, py)\n        onShowMenu(item, index, { x: Math.ceil(px), y: Math.ceil(py), w: Math.ceil(width), h: Math.ceil(height) })\n      })\n    }\n  }\n\n  const handlePress = () => {\n    onPress(item)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, height: ITEM_HEIGHT }}>\n      {\n        active\n          ? <Icon style={styles.listActiveIcon} name=\"chevron-right\" size={12} color={theme['c-primary-font']} />\n          : null\n      }\n      { fetching ? <Loading color={active ? theme['c-primary-font'] : theme['c-font']} style={styles.loading} /> : null }\n      <TouchableOpacity style={styles.listName} onPress={handlePress}>\n        <Text numberOfLines={1} color={active ? theme['c-primary-font'] : theme['c-font']}>{item.name}</Text>\n      </TouchableOpacity>\n      <TouchableOpacity onPress={handleShowMenu} ref={moreButtonRef} style={styles.listMoreBtn}>\n        <Icon name=\"dots-vertical\" color={theme['c-350']} size={12} />\n      </TouchableOpacity>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return !!(prevProps.item === nextProps.item &&\n    prevProps.index === nextProps.index &&\n    prevProps.item.name == nextProps.item.name &&\n    prevProps.activeId != nextProps.item.id &&\n    nextProps.activeId != nextProps.item.id\n  )\n})\n\n\nexport default ({ onShowMenu }: {\n  onShowMenu: (info: { listInfo: LX.List.MyListInfo, index: number }, position: Position) => void\n}) => {\n  const flatListRef = useRef<FlatList>(null)\n  const allList = useMyList()\n  const activeListId = useActiveListId()\n\n  const handleToggleList = (item: LX.List.MyListInfo) => {\n    // setVisiblePanel(false)\n    global.app_event.changeLoveListVisible(false)\n    requestAnimationFrame(() => {\n      setActiveList(item.id)\n    })\n  }\n\n\n  const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {\n    void saveListPosition(LIST_SCROLL_POSITION_KEY, nativeEvent.contentOffset.y)\n  }\n\n  const showMenu = (listInfo: LX.List.MyListInfo, index: number, position: Position) => {\n    onShowMenu({ listInfo, index }, position)\n  }\n\n  useEffect(() => {\n    void getListPosition(LIST_SCROLL_POSITION_KEY).then((offset) => {\n      flatListRef.current?.scrollToOffset({ offset, animated: false })\n    })\n  }, [])\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => (\n    <ListItem\n      key={item.id}\n      item={item}\n      index={index}\n      activeId={activeListId}\n      onPress={handleToggleList}\n      onShowMenu={showMenu}\n    />\n  )\n  const getkey: FlatListType['keyExtractor'] = item => item.id\n  const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n\n  return (\n    <FlatList\n      ref={flatListRef}\n      onScroll={handleScroll}\n      style={styles.container}\n      data={allList}\n      maxToRenderPerBatch={9}\n      // updateCellsBatchingPeriod={80}\n      windowSize={9}\n      removeClippedSubviews={true}\n      initialNumToRender={18}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      // extraData={activeIndex}\n      getItemLayout={getItemLayout}\n    />\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    flexShrink: 1,\n    flexGrow: 0,\n  },\n  // listContainer: {\n  //   // borderBottomWidth: BorderWidths.normal2,\n  // },\n\n  listItem: {\n    height: 'auto',\n    flexDirection: 'row',\n    alignItems: 'center',\n    paddingRight: 5,\n    paddingLeft: 5,\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  listActiveIcon: {\n    // width: 18,\n    marginLeft: 3,\n    // paddingRight: 5,\n    textAlign: 'center',\n  },\n  loading: {\n    marginLeft: 5,\n  },\n  listName: {\n    height: '100%',\n    // height: 46,\n    // paddingTop: 12,\n    // paddingBottom: 12,\n    justifyContent: 'center',\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingLeft: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  // listNameText: {\n  //   // height: 46,\n  //   fontSize: 14,\n  // },\n  listMoreBtn: {\n    height: '100%',\n    width: 36,\n    // height: 46,\n    // paddingTop: 12,\n    // paddingBottom: 12,\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/ListImportExport.tsx",
    "content": "import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'\nimport { LXM_FILE_EXT_RXP } from '@/config/constant'\nimport { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { handleExport, handleImport, handleImportMediaFile } from './listAction'\n\nexport interface SelectInfo {\n  listInfo: LX.List.MyListInfo\n  // selectedList: LX.Music.MusicInfo[]\n  index: number\n  // listId: string\n  // single: boolean\n  action: 'import' | 'export' | 'selectFile'\n}\nconst initSelectInfo = {}\n\n// export interface ListImportExportProps {\n//   // onRename: (listInfo: LX.List.UserListInfo) => void\n//   // onImport: (index: number) => void\n//   // onExport: (listInfo: LX.List.MyListInfo) => void\n//   // onSync: (listInfo: LX.List.UserListInfo) => void\n//   // onRemove: (listInfo: LX.List.MyListInfo) => void\n// }\nexport interface ListImportExportType {\n  import: (listInfo: LX.List.MyListInfo, index: number) => void\n  export: (listInfo: LX.List.MyListInfo, index: number) => void\n  selectFile: (listInfo: LX.List.MyListInfo, index: number) => void\n}\n\nexport default forwardRef<ListImportExportType, {}>((props, ref) => {\n  const [visible, setVisible] = useState(false)\n  const choosePathRef = useRef<ChoosePathType>(null)\n  const selectInfoRef = useRef<SelectInfo>((initSelectInfo as SelectInfo))\n  // console.log('render import export')\n\n  useImperativeHandle(ref, () => ({\n    import(listInfo, index) {\n      selectInfoRef.current = {\n        action: 'import',\n        listInfo,\n        index,\n      }\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('list_import_part_desc'),\n          dirOnly: false,\n          filter: LXM_FILE_EXT_RXP,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('list_import_part_desc'),\n            dirOnly: false,\n            filter: LXM_FILE_EXT_RXP,\n          })\n        })\n      }\n    },\n    export(listInfo, index) {\n      selectInfoRef.current = {\n        action: 'export',\n        listInfo,\n        index,\n      }\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('list_export_part_desc'),\n          dirOnly: true,\n          filter: LXM_FILE_EXT_RXP,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('list_export_part_desc'),\n            dirOnly: true,\n            filter: LXM_FILE_EXT_RXP,\n          })\n        })\n      }\n    },\n    selectFile(listInfo, index) {\n      selectInfoRef.current = {\n        action: 'selectFile',\n        listInfo,\n        index,\n      }\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('list_select_local_file_desc'),\n          dirOnly: true,\n          isPersist: true,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('list_select_local_file_desc'),\n            dirOnly: true,\n            isPersist: true,\n          })\n        })\n      }\n    },\n  }))\n\n\n  const onConfirmPath = (path: string) => {\n    switch (selectInfoRef.current.action) {\n      case 'import':\n        handleImport(path, selectInfoRef.current.index)\n        break\n      case 'export':\n        handleExport(selectInfoRef.current.listInfo, path)\n        break\n      case 'selectFile':\n        void handleImportMediaFile(selectInfoRef.current.listInfo, path)\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <ChoosePath ref={choosePathRef} onConfirm={onConfirmPath} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/ListMenu.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport { useI18n } from '@/lang'\nimport Menu, { type Menus, type MenuType, type Position } from '@/components/common/Menu'\nimport { LIST_IDS } from '@/config/constant'\nimport musicSdk from '@/utils/musicSdk'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport listState from '@/store/list/state'\n\nexport interface SelectInfo {\n  listInfo: LX.List.MyListInfo\n  // selectedList: LX.Music.MusicInfo[]\n  index: number\n  // listId: string\n  // single: boolean\n}\nconst initSelectInfo = {}\n\nconst menuItemWidth = scaleSizeW(110)\n\n\nexport interface ListMenuProps {\n  onNew: (position: number) => void\n  onRename: (listInfo: LX.List.UserListInfo) => void\n  onSort: (listInfo: LX.List.MyListInfo) => void\n  onDuplicateMusic: (listInfo: LX.List.MyListInfo) => void\n  onImport: (listInfo: LX.List.MyListInfo, index: number) => void\n  onExport: (listInfo: LX.List.MyListInfo, index: number) => void\n  onSync: (listInfo: LX.List.UserListInfo) => void\n  onSelectLocalFile: (listInfo: LX.List.MyListInfo, index: number) => void\n  onRemove: (listInfo: LX.List.UserListInfo) => void\n}\nexport interface ListMenuType {\n  show: (selectInfo: SelectInfo, position: Position) => void\n}\n\nexport type {\n  Position,\n}\n\nexport default forwardRef<ListMenuType, ListMenuProps>(({\n  onNew,\n  onRename,\n  onSort,\n  onDuplicateMusic,\n  onImport,\n  onExport,\n  onSync,\n  onSelectLocalFile,\n  onRemove,\n}, ref) => {\n  const t = useI18n()\n  const menuRef = useRef<MenuType>(null)\n  const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)\n  const [menus, setMenus] = useState<Menus>([])\n  const [visible, setVisible] = useState(false)\n\n  useImperativeHandle(ref, () => ({\n    show(selectInfo, position) {\n      selectInfoRef.current = selectInfo\n      handleSetMenu(selectInfo.listInfo)\n      if (visible) menuRef.current?.show(position)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          menuRef.current?.show(position)\n        })\n      }\n    },\n  }))\n\n  const handleSetMenu = (listInfo: LX.List.MyListInfo) => {\n    let rename = false\n    let sync = false\n    let remove = false\n    let local_file = !listState.fetchingListStatus[listInfo.id]\n    let userList: LX.List.UserListInfo\n    switch (listInfo.id) {\n      case LIST_IDS.DEFAULT:\n      case LIST_IDS.LOVE:\n        break\n      default:\n        userList = listInfo as LX.List.UserListInfo\n        rename = true\n        remove = true\n        sync = !!(userList.source && musicSdk[userList.source]?.songList)\n        break\n    }\n\n    setMenus([\n      { action: 'new', label: t('list_create') },\n      { action: 'rename', disabled: !rename, label: t('list_rename') },\n      { action: 'sort', label: t('list_sort') },\n      { action: 'duplicateMusic', label: t('lists__duplicate') },\n      { action: 'local_file', disabled: !local_file, label: t('list_select_local_file') },\n      { action: 'sync', disabled: !sync || !local_file, label: t('list_sync') },\n      { action: 'import', label: t('list_import') },\n      { action: 'export', label: t('list_export') },\n      // { action: 'changePosition', label: t('change_position') },\n      { action: 'remove', disabled: !remove, label: t('list_remove') },\n    ])\n  }\n\n  const handleMenuPress = ({ action }: typeof menus[number]) => {\n    const selectInfo = selectInfoRef.current\n    switch (action) {\n      case 'new':\n        onNew(Math.max(selectInfo.index - 1, 0))\n        break\n      case 'rename':\n        onRename(selectInfo.listInfo as LX.List.UserListInfo)\n        break\n      case 'sort':\n        onSort(selectInfo.listInfo)\n        break\n      case 'duplicateMusic':\n        onDuplicateMusic(selectInfo.listInfo)\n        break\n      case 'import':\n        onImport(selectInfo.listInfo, selectInfo.index)\n        break\n      case 'export':\n        onExport(selectInfo.listInfo, selectInfo.index)\n        break\n      case 'sync':\n        onSync(selectInfo.listInfo as LX.List.UserListInfo)\n        break\n        // case 'changePosition':\n\n        //   break\n      case 'local_file':\n        onSelectLocalFile(selectInfo.listInfo, selectInfo.index)\n        break\n      case 'remove':\n        onRemove(selectInfo.listInfo as LX.List.UserListInfo)\n        break\n\n      default:\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <Menu\n          ref={menuRef}\n          menus={menus}\n          onPress={handleMenuPress}\n          width={menuItemWidth}\n        />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/ListMusicSort.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { InteractionManager, View } from 'react-native'\nimport { createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport CheckBox from '@/components/common/CheckBox'\nimport { getListMusics, setFetchingListStatus, updateListMusicPosition } from '@/core/list'\nimport { sortListMusicInfo } from './utils'\n\nconst Title = ({ title }: {\n  title: string\n}) => {\n  return (\n    <Text style={styles.title} size={16}>\n      {title}\n    </Text>\n  )\n}\n\nexport interface FormType {\n  reset: () => void\n  getForm: () => ([FieldName | undefined, FieldType | undefined])\n}\nconst fieldNames = ['name', 'singer', 'album', 'time', 'source'] as const\nconst fieldTypes = ['up', 'down', 'random'] as const\ntype FieldName = typeof fieldNames[number]\ntype FieldType = typeof fieldTypes[number]\nconst CheckBoxItem = <T extends FieldName | FieldType>({ id, isActive, disabled, change }: {\n  id: T\n  isActive: boolean\n  disabled?: boolean\n  change: (id: T) => void\n}) => {\n  const t = useI18n()\n  return (\n    <CheckBox\n      marginBottom={3}\n      disabled={disabled}\n      check={isActive}\n      label={t(`list_sort_modal_by_${id}`)}\n      onChange={() => { change(id) }} need />\n  )\n}\nconst Form = forwardRef<FormType, {}>((props, ref) => {\n  const t = useI18n()\n  const [name, setName] = useState<FieldName>()\n  const [type, setType] = useState<FieldType>()\n  useImperativeHandle(ref, () => ({\n    reset() {\n      setName(undefined)\n      setType(undefined)\n    },\n    getForm() {\n      return [name, type]\n    },\n  }))\n\n  return (\n    <View>\n      <View style={styles.formSection}>\n        <Text>{t('list_sort_modal_by_field')}</Text>\n        <View style={styles.formList}>\n          {fieldNames.map(n => <CheckBoxItem key={n} id={n} isActive={name == n} change={setName} disabled={type == 'random'} />)}\n        </View>\n      </View>\n      <View style={styles.formSection}>\n        <Text>{t('list_sort_modal_by_type')}</Text>\n        <View style={styles.formList}>\n          {fieldTypes.map(n => <CheckBoxItem key={n} id={n} isActive={type == n} change={setType} />)}\n        </View>\n      </View>\n    </View>\n  )\n})\n\nexport interface ListMusicSortType {\n  show: (listInfo: LX.List.MyListInfo) => void\n}\nconst initSelectInfo = {}\n\n// const getName = (id: string, name: string) => {\n//   switch (id) {\n//     case LIST_IDS.DEFAULT:\n//       return global.i18n.t(name)\n//     case LIST_IDS.LOVE:\n//       return global.i18n.t(name)\n//     default:\n//       return name\n//   }\n// }\n\nexport default forwardRef<ListMusicSortType, {}>((props, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const [title, setTitle] = useState('')\n  const selectedListInfo = useRef<LX.List.MyListInfo>(initSelectInfo as LX.List.MyListInfo)\n  const formTypeRef = useRef<FormType>(null)\n  const [visible, setVisible] = useState(false)\n\n  const handleShow = () => {\n    alertRef.current?.setVisible(true)\n  }\n  useImperativeHandle(ref, () => ({\n    show(listInfo) {\n      setTitle(listInfo.name)\n      selectedListInfo.current = listInfo\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleSort = async() => {\n    const [name, type] = formTypeRef.current!.getForm()\n    // console.log(type, name)\n    if (!type || (!name && type != 'random')) return\n    const id = selectedListInfo.current.id\n    let list = [...(await getListMusics(id))]\n    setFetchingListStatus(id, true)\n    requestAnimationFrame(() => {\n      void InteractionManager.runAfterInteractions(() => {\n        list = sortListMusicInfo(list, type, name!, global.i18n.locale)\n        void updateListMusicPosition(id, 0, list.map(m => m.id))\n        setFetchingListStatus(id, false)\n      })\n    })\n\n    alertRef.current?.setVisible(false)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleSort}\n        >\n          <View style={styles.renameContent}>\n            <Title title={title} />\n            <Form ref={formTypeRef} />\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  renameContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  title: {\n    textAlign: 'center',\n    // paddingTop: 15,\n    paddingBottom: 25,\n  },\n  formSection: {\n    marginBottom: 15,\n  },\n  formList: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/ListNameEdit.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { createUserList, updateUserList } from '@/core/list'\nimport { confirmDialog, createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport listState from '@/store/list/state'\n\ninterface NameInputType {\n  setName: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst NameInput = forwardRef<NameInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const [text, setText] = useState('')\n  const [placeholder, setPlaceholder] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setName(text) {\n      setText(text)\n      setPlaceholder(text || global.i18n.t('list_create_input_placeholder'))\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={placeholder}\n      value={text}\n      onChangeText={setText}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\n\nexport interface ListNameEditType {\n  showCreate: (position: number) => void\n  show: (listInfo: LX.List.UserListInfo) => void\n}\nconst initSelectInfo = {}\n\n\nexport default forwardRef<ListNameEditType, {}>((props, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const nameInputRef = useRef<NameInputType>(null)\n  const [position, setPosition] = useState(0)\n  const selectedListInfo = useRef<LX.List.UserListInfo>(initSelectInfo as LX.List.UserListInfo)\n  const [visible, setVisible] = useState(false)\n\n  const handleShow = () => {\n    alertRef.current?.setVisible(true)\n    const name = position == -1 ? '' : (selectedListInfo.current.name ?? '')\n    requestAnimationFrame(() => {\n      nameInputRef.current?.setName(name)\n      setTimeout(() => {\n        nameInputRef.current?.focus()\n      }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    showCreate(position) {\n      setPosition(position)\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n    show(listInfo) {\n      setPosition(-1)\n      selectedListInfo.current = listInfo\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleRename = () => {\n    let name = nameInputRef.current?.getText() ?? ''\n    if (!name.length) return\n    if (name.length > 100) name = name.substring(0, 100)\n    if (position == -1) {\n      void updateUserList([{ ...selectedListInfo.current, name }])\n    } else {\n      void (listState.userList.some(l => l.name == name) ? confirmDialog({\n        message: global.i18n.t('list_duplicate_tip'),\n      }) : Promise.resolve(true)).then(confirmed => {\n        if (!confirmed) return\n        const now = Date.now()\n        void createUserList(position, [{ id: `userlist_${now}`, name, locationUpdateTime: now }])\n      })\n    }\n    alertRef.current?.setVisible(false)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleRename}\n        >\n          <View style={styles.renameContent}>\n            <Text style={{ marginBottom: 5 }}>{ position == -1 ? global.i18n.t('list_rename_title') : global.i18n.t('list_create')}</Text>\n            <NameInput ref={nameInputRef} />\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  renameContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 290,\n    borderRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/index.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\n\nimport ListMenu, { type ListMenuType } from './ListMenu'\nimport ListNameEdit, { type ListNameEditType } from './ListNameEdit'\nimport List from './List'\nimport ListImportExport, { type ListImportExportType } from './ListImportExport'\nimport { handleRemove, handleSync } from './listAction'\nimport ListMusicSort, { type ListMusicSortType } from './ListMusicSort'\nimport DuplicateMusic, { type DuplicateMusicType } from './DuplicateMusic'\n\n\nexport default () => {\n  const [visible, setVisible] = useState(false)\n  const listMenuRef = useRef<ListMenuType>(null)\n  const listNameEditRef = useRef<ListNameEditType>(null)\n  const listMusicSortRef = useRef<ListMusicSortType>(null)\n  const duplicateMusicRef = useRef<DuplicateMusicType>(null)\n  const listImportExportRef = useRef<ListImportExportType>(null)\n\n  useEffect(() => {\n    let isInited = false\n    const changeVisible = (visibleList: boolean) => {\n      if (visibleList && !isInited) {\n        requestAnimationFrame(() => {\n          setVisible(true)\n        })\n        isInited = true\n      }\n    }\n    global.app_event.on('changeLoveListVisible', changeVisible)\n\n    return () => {\n      global.app_event.off('changeLoveListVisible', changeVisible)\n    }\n  }, [])\n\n  return (\n    visible\n      ? <>\n          <List onShowMenu={(info, position) => listMenuRef.current?.show(info, position)} />\n          <ListNameEdit ref={listNameEditRef} />\n          <ListMusicSort ref={listMusicSortRef} />\n          <DuplicateMusic ref={duplicateMusicRef} />\n          <ListImportExport ref={listImportExportRef} />\n          <ListMenu\n            ref={listMenuRef}\n            onNew={index => listNameEditRef.current?.showCreate(index)}\n            onRename={info => listNameEditRef.current?.show(info)}\n            onSort={info => listMusicSortRef.current?.show(info)}\n            onDuplicateMusic={info => duplicateMusicRef.current?.show(info)}\n            onImport={(info, position) => listImportExportRef.current?.import(info, position)}\n            onExport={(info, position) => listImportExportRef.current?.export(info, position)}\n            onRemove={info => { handleRemove(info) }}\n            onSync={info => { handleSync(info) }}\n            onSelectLocalFile={(info, position) => listImportExportRef.current?.selectFile(info, position)}\n          />\n          {/* <ImportExport actionType={actionType} visible={isShowChoosePath} hide={() => setShowChoosePath(false)} selectedListRef={selectedListRef} /> */}\n        </>\n      : null\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/listAction.ts",
    "content": "import { addListMusics, getListMusics, removeListMusics, removeUserList, setFetchingListStatus, updateListMusics } from '@/core/list'\nimport { confirmDialog, handleReadFile, handleSaveFile, showImportTip, toast } from '@/utils/tools'\nimport syncSourceList from '@/core/syncSourceList'\nimport { log } from '@/utils/log'\nimport { filterFileName, filterMusicList, formatPlayTime2, toNewMusicInfo } from '@/utils'\nimport { handleImportListPart } from '@/screens/Home/Views/Setting/settings/Backup/actions'\nimport { readMetadata, scanAudioFiles, type MusicMetadataFull } from '@/utils/localMediaMetadata'\nimport settingState from '@/store/setting/state'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { type FileType } from '@/utils/fs'\n\nexport const handleRemove = (listInfo: LX.List.UserListInfo) => {\n  void confirmDialog({\n    message: global.i18n.t('list_remove_tip', { name: listInfo.name }),\n    confirmButtonText: global.i18n.t('list_remove_tip_button'),\n  }).then(isRemove => {\n    if (!isRemove) return\n    void removeUserList([listInfo.id])\n  })\n}\n\nconst readListData = async(path: string) => {\n  let configData: any\n  try {\n    configData = await handleReadFile(path)\n  } catch (error: any) {\n    log.error(error.stack)\n    throw error\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      break\n    default:\n      showImportTip(configData.type as string)\n      return null\n  }\n  return listData\n}\n\nexport const handleImport = (path: string, position: number) => {\n  toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))\n  void readListData(path).then(async listData => {\n    if (listData == null) return\n    void handleImportListPart(listData, position)\n  }).catch((err) => {\n    log.error(err)\n    toast(global.i18n.t('setting_backup_part_import_list_tip_error'))\n  })\n}\n\nconst exportList = async(listInfo: LX.List.MyListInfo, path: string) => {\n  const data = JSON.parse(JSON.stringify({\n    type: 'playListPart_v2',\n    data: {\n      ...listInfo,\n      list: await getListMusics(listInfo.id),\n    },\n  }))\n  try {\n    await handleSaveFile(`${path}/lx_list_part_${filterFileName(listInfo.name)}.lxmc`, data)\n  } catch (error: any) {\n    log.error(error.stack)\n  }\n}\nexport const handleExport = (listInfo: LX.List.MyListInfo, path: string) => {\n  toast(global.i18n.t('setting_backup_part_export_list_tip_zip'))\n  exportList(listInfo, path).then(() => {\n    toast(global.i18n.t('setting_backup_part_export_list_tip_success'))\n  }).catch((err: any) => {\n    log.error(err.message)\n    toast(global.i18n.t('setting_backup_part_export_list_tip_failed') + ': ' + (err.message as string))\n  })\n}\n\nexport const handleSync = (listInfo: LX.List.UserListInfo) => {\n  void confirmDialog({\n    message: global.i18n.t('list_sync_confirm_tip', { name: listInfo.name }),\n    confirmButtonText: global.i18n.t('list_remove_tip_button'),\n  }).then(isSync => {\n    if (!isSync) return\n    void syncSourceList(listInfo).then(() => {\n      toast(global.i18n.t('list_update_success', { name: listInfo.name }))\n    }).catch(() => {\n      toast(global.i18n.t('list_update_error', { name: listInfo.name }))\n    })\n  })\n}\n\nexport const buildLocalMusicInfoByFilePath = (file: FileType): LX.Music.MusicInfoLocal => {\n  const index = file.name.lastIndexOf('.')\n  return {\n    id: file.path,\n    name: file.name.substring(0, index),\n    singer: '',\n    source: 'local',\n    interval: null,\n    meta: {\n      albumName: '',\n      filePath: file.path,\n      songId: file.path,\n      picUrl: '',\n      ext: file.name.substring(index + 1),\n    },\n  }\n}\nexport const buildLocalMusicInfo = (filePath: string, metadata: MusicMetadataFull): LX.Music.MusicInfoLocal => {\n  return {\n    id: filePath,\n    name: metadata.name,\n    singer: metadata.singer,\n    source: 'local',\n    interval: formatPlayTime2(metadata.interval),\n    meta: {\n      albumName: metadata.albumName,\n      filePath,\n      songId: filePath,\n      picUrl: '',\n      ext: metadata.ext,\n    },\n  }\n}\nconst createLocalMusicInfos = async(filePaths: string[], errorPath: string[]): Promise<LX.Music.MusicInfoLocal[]> => {\n  const list: LX.Music.MusicInfoLocal[] = []\n  filePaths = [...filePaths]\n  while (filePaths.length) {\n    const tasks = [\n      filePaths.shift(),\n      filePaths.shift(),\n      filePaths.shift(),\n      filePaths.shift(),\n      filePaths.shift(),\n    ].filter(Boolean) as string[]\n\n    await Promise.all(tasks.map(async path => readMetadata(path).then(info => ([path, info] as const)))).then((res) => {\n      for (const [path, info] of res) {\n        if (!info) {\n          errorPath.push(path)\n          continue\n        }\n        list.push(buildLocalMusicInfo(path, info))\n      }\n    })\n  }\n  return list\n}\n\nconst createThrottleAddMusics = (add: (listId: string, musicInfos: LX.Music.MusicInfoLocal[]) => Promise<void>, remove: (listId: string, errorPath: string[]) => Promise<void>, listId: string) => {\n  let timer: number | null = null\n  let _musicInfos: LX.Music.MusicInfoLocal[] = []\n  let _errorPath: string[] = []\n  return (musicInfos: LX.Music.MusicInfoLocal[], errorPath?: string[]) => {\n    if (musicInfos.length) _musicInfos = [..._musicInfos, ...musicInfos]\n    if (errorPath) _errorPath = [..._errorPath, ...errorPath]\n    if (timer) return\n    timer = BackgroundTimer.setTimeout(async() => {\n      timer = null\n      let musicInfos = _musicInfos\n      _musicInfos = []\n      let errorPath = _errorPath\n      _errorPath = []\n      if (musicInfos.length) await add(listId, musicInfos)\n      if (errorPath.length) await remove(listId, errorPath)\n    }, 1000)\n  }\n}\n\nconst handleUpdateMusics = async(filePaths: string[],\n  throttleUpdateMusics: (musicInfos: LX.Music.MusicInfoLocal[], errorPath?: string[]) => void, index: number = -1, total: number = 0, errorPath: string[] = []) => {\n  // console.log(index + 1, index + 201)\n  if (!total) total = filePaths.length\n  const paths = filePaths.slice(index + 1, index + 11)\n  const musicInfos = await createLocalMusicInfos(paths, errorPath)\n  if (musicInfos.length) throttleUpdateMusics(musicInfos)\n  index += 10\n  if (filePaths.length - 1 > index) await handleUpdateMusics(filePaths, throttleUpdateMusics, index, total, errorPath)\n  else {\n    if (errorPath.length) {\n      log.warn('Parse metadata failed:\\n' + errorPath.map(p => p.split('/').at(-1)).join('\\n'))\n      toast(global.i18n.t('list_select_local_file_result_failed_tip', { total, success: total - errorPath.length, failed: errorPath.length }), 'long')\n    } else {\n      toast(global.i18n.t('list_select_local_file_result_tip', { total }), 'long')\n    }\n    throttleUpdateMusics([], errorPath)\n  }\n}\nexport const handleImportMediaFile = async(listInfo: LX.List.MyListInfo, path: string) => {\n  setFetchingListStatus(listInfo.id, true)\n  const files = await scanAudioFiles(path)\n  if (files.length) {\n    const throttleUpdateMusics = createThrottleAddMusics(async(listId, musicInfos) => {\n      return updateListMusics(musicInfos.map(info => ({ id: listId, musicInfo: info })))\n    }, async(listId, errorPath) => {\n      return removeListMusics(listId, errorPath)\n    }, listInfo.id)\n    await addListMusics(listInfo.id, files.map(buildLocalMusicInfoByFilePath), settingState.setting['list.addMusicLocationType'])\n    toast(global.i18n.t('list_select_local_file_temp_add_tip', { total: files.length }), 'long')\n    await handleUpdateMusics(files.map(f => f.path), throttleUpdateMusics)\n  } else toast(global.i18n.t('list_select_local_file_empty_tip'), 'long')\n  setFetchingListStatus(listInfo.id, false)\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Mylist/MyList/utils.ts",
    "content": "import { arrShuffle } from '@/utils'\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\n/**\n * 排序歌曲\n * @param list 歌曲列表\n * @param sortType 排序类型\n * @param fieldName 排序字段\n * @param localeId 排序语言\n * @returns\n */\nexport const sortListMusicInfo = (list: LX.Music.MusicInfo[], sortType: 'up' | 'down' | 'random', fieldName: 'name' | 'singer' | 'album' | 'time' | 'source', localeId: string) => {\n  // console.log(sortType, fieldName, localeId)\n  localeId = localeId.replaceAll('_', '-')\n  switch (sortType) {\n    case 'random':\n      arrShuffle(list)\n      break\n    case 'up':\n      if (fieldName == 'time') {\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 'album':\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 == 'time') {\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 'album':\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\n\nconst variantRxp = /(\\(|（).+(\\)|）)/g\nconst variantRxp2 = /\\s|'|\\.|,|，|&|\"|、|\\(|\\)|（|）|`|~|-|<|>|\\||\\/|\\]|\\[/g\nexport interface DuplicateMusicItem {\n  id: string\n  index: number\n  group: string\n  musicInfo: LX.Music.MusicInfo\n}\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  const listMap = new Map<string, DuplicateMusicItem[]>()\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        group: name,\n      })\n      duplicateList.add(name)\n    } else {\n      listMap.set(name, [{\n        id: musicInfo.id,\n        index,\n        musicInfo,\n        group: name,\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"
  },
  {
    "path": "src/screens/Home/Views/Mylist/index.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport settingState from '@/store/setting/state'\nimport MusicList from './MusicList'\nimport MyList from './MyList'\nimport { useTheme } from '@/store/theme/hook'\nimport DrawerLayoutFixed, { type DrawerLayoutFixedType } from '@/components/common/DrawerLayoutFixed'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport type { InitState as CommonState } from '@/store/common/state'\n\nconst MAX_WIDTH = scaleSizeW(400)\n\nexport default () => {\n  const drawer = useRef<DrawerLayoutFixedType>(null)\n  const theme = useTheme()\n  // const [width, setWidth] = useState(0)\n\n  useEffect(() => {\n    const handleFixDrawer = (id: CommonState['navActiveId']) => {\n      if (id == 'nav_love') drawer.current?.fixWidth()\n    }\n    const changeVisible = (visible: boolean) => {\n      if (visible) {\n        requestAnimationFrame(() => {\n          drawer.current?.openDrawer()\n        })\n      } else {\n        drawer.current?.closeDrawer()\n      }\n    }\n\n    // setWidth(getWindowSise().width * 0.82)\n\n    global.state_event.on('navActiveIdUpdated', handleFixDrawer)\n    global.app_event.on('changeLoveListVisible', changeVisible)\n\n    // 就放旋转屏幕后的宽度没有更新的问题\n    // const changeEvent = onDimensionChange(({ window }) => {\n    //   setWidth(window.width * 0.82)\n    //   drawer.current?.setNativeProps({\n    //     width: window.width,\n    //   })\n    // })\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleFixDrawer)\n      global.app_event.off('changeLoveListVisible', changeVisible)\n    // changeEvent.remove()\n    }\n  }, [])\n\n  const navigationView = () => <MyList />\n  // console.log('render drawer content')\n\n  return (\n    <DrawerLayoutFixed\n      ref={drawer}\n      visibleNavNames={[COMPONENT_IDS.home]}\n      // drawerWidth={width}\n      widthPercentage={0.82}\n      widthPercentageMax={MAX_WIDTH}\n      drawerPosition={settingState.setting['common.drawerLayoutPosition']}\n      renderNavigationView={navigationView}\n      drawerBackgroundColor={theme['c-content-background']}\n      style={{ elevation: 1 }}\n    >\n      <MusicList />\n    </DrawerLayoutFixed>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Search/BlankView/HistorySearch.tsx",
    "content": "import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'\nimport { TouchableOpacity, View } from 'react-native'\nimport { type InitState } from '@/store/hotSearch/state'\nimport Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { clearHistoryList, getSearchHistory, removeHistoryWord } from '@/core/search/search'\nimport { Icon } from '@/components/common/Icon'\n\n\nexport type List = NonNullable<InitState['sourceList'][keyof InitState['sourceList']]>\n\nconst ListItem = ({ keyword, onSearch, onRemove }: {\n  keyword: string\n  onSearch: (keyword: string) => void\n  onRemove: (keyword: string) => void\n}) => {\n  const theme = useTheme()\n  return (\n    <Button\n      style={{ ...styles.button, backgroundColor: theme['c-button-background'] }}\n      onPress={() => { onSearch(keyword) }}\n      onLongPress={() => { onRemove(keyword) }}\n    >\n      <Text color={theme['c-button-font']} size={13}>{keyword}</Text>\n    </Button>\n  )\n}\n\n\ninterface HistorySearchProps {\n  onSearch: (keyword: string) => void\n}\nexport interface HistorySearchType {\n  show: () => void\n}\n\nexport default forwardRef<HistorySearchType, HistorySearchProps>((props, ref) => {\n  const [list, setList] = useState<List>([])\n  const isUnmountedRef = useRef(false)\n  const t = useI18n()\n  const theme = useTheme()\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  useImperativeHandle(ref, () => ({\n    show() {\n      void getSearchHistory().then((list) => {\n        if (isUnmountedRef.current) return\n        setList(list)\n      })\n    },\n  }), [])\n\n  const handleClear = () => {\n    clearHistoryList()\n    setList([])\n  }\n\n  const handleRemove = useCallback((keyword: string) => {\n    setList(list => {\n      list = [...list]\n      const index = list.indexOf(keyword)\n      list.splice(index, 1)\n      removeHistoryWord(index)\n      return list\n    })\n  }, [])\n\n  return (\n    list.length\n      ? (\n          <View>\n            <View style={styles.titleContent}>\n              <Text size={16}>{t('search_history_search')}</Text>\n              <TouchableOpacity onPress={handleClear} style={styles.titleBtn}>\n                <Icon name=\"eraser\" color={theme['c-300']} size={14} />\n              </TouchableOpacity>\n            </View>\n            <View style={styles.list}>\n              {\n                list.map(keyword => <ListItem keyword={keyword} key={keyword} onSearch={props.onSearch} onRemove={handleRemove} />)\n              }\n            </View>\n          </View>\n        )\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  titleContent: {\n    paddingTop: 15,\n    flexDirection: 'row',\n    alignItems: 'center',\n  },\n  title: {\n    // paddingLeft: 15,\n    // paddingBottom: 5,\n  },\n  titleBtn: {\n    marginLeft: 10,\n    padding: 5,\n  },\n  list: {\n    // paddingLeft: 15,\n    // paddingRight: 15,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    // paddingBottom: 15,\n  },\n  button: {\n    textAlign: 'center',\n    paddingLeft: 10,\n    paddingRight: 10,\n    paddingTop: 5,\n    paddingBottom: 5,\n    borderRadius: 4,\n    marginRight: 10,\n    marginTop: 8,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/BlankView/HotSearch.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'\nimport { ScrollView, View } from 'react-native'\nimport { type Source, type InitState } from '@/store/hotSearch/state'\nimport Button from '@/components/common/Button'\nimport { getList } from '@/core/hotSearch'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\n\n\ninterface ListProps {\n  onSearch: (keyword: string) => void\n}\nexport interface HotSearchType {\n  show: (source: Source) => void\n}\n\n\nexport type List = NonNullable<InitState['sourceList'][keyof InitState['sourceList']]>\n\nconst ListItem = ({ keyword, onSearch }: {\n  keyword: string\n  onSearch: (keyword: string) => void\n}) => {\n  const theme = useTheme()\n  return (\n    <Button style={{ ...styles.button, backgroundColor: theme['c-button-background'] }} onPress={() => { onSearch(keyword) }}>\n      <Text color={theme['c-button-font']} size={13}>{keyword}</Text>\n    </Button>\n  )\n}\n\nexport default forwardRef<HotSearchType, ListProps>((props, ref) => {\n  // const [listType, setListType] = useState<SearchState['searchType']>('music')\n  // const listRef = useRef<MusicListType>(null)\n  const [list, setList] = useState<List>([])\n  const t = useI18n()\n  // const theme = useTheme()\n\n  const isUnmountedRef = useRef(false)\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  useImperativeHandle(ref, () => ({\n    show(source) {\n      void getList(source).then((list) => {\n        if (isUnmountedRef.current) return\n        setList(list)\n      })\n    },\n  }), [])\n\n  return (\n    list.length\n      ? (\n          <ScrollView>\n            <Text style={styles.title} size={16}>{t('search_hot_search')}</Text>\n            <View style={styles.list}>\n              {\n                list.map(keyword => <ListItem keyword={keyword} key={keyword} onSearch={props.onSearch} />)\n              }\n            </View>\n          </ScrollView>\n        )\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  title: {\n    // paddingLeft: 15,\n    paddingTop: 15,\n    // paddingBottom: 10,\n  },\n  list: {\n    // paddingLeft: 15,\n    // paddingRight: 15,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    // paddingBottom: 15,\n  },\n  button: {\n    textAlign: 'center',\n    paddingLeft: 10,\n    paddingRight: 10,\n    paddingTop: 5,\n    paddingBottom: 5,\n    borderRadius: 4,\n    marginRight: 10,\n    marginTop: 8,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/BlankView/index.tsx",
    "content": "import Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { ScrollView, View } from 'react-native'\nimport HistorySearch, { type HistorySearchType } from './HistorySearch'\nimport HotSearch, { type HotSearchType } from './HotSearch'\n\ninterface BlankViewProps {\n  onSearch: (keyword: string) => void\n}\ntype Source = LX.OnlineSource | 'all'\n\nexport interface BlankViewType {\n  show: (source: Source) => void\n}\n\nexport default forwardRef<BlankViewType, BlankViewProps>(({ onSearch }, ref) => {\n  // const [listType, setListType] = useState<SearchState['searchType']>('music')\n  const [visible, setVisible] = useState(false)\n  const hotSearchRef = useRef<HotSearchType>(null)\n  const historySearchRef = useRef<HistorySearchType>(null)\n  const isShowHotSearch = useSettingValue('search.isShowHotSearch')\n  const isShowHistorySearch = useSettingValue('search.isShowHistorySearch')\n  const t = useI18n()\n  const theme = useTheme()\n\n  const handleShow = (source: Source) => {\n    hotSearchRef.current?.show(source)\n    historySearchRef.current?.show()\n  }\n\n  useImperativeHandle(ref, () => ({\n    show(source) {\n      if (visible) handleShow(source)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow(source)\n        })\n      }\n    },\n  }), [visible])\n\n  return (\n    visible\n      ? isShowHotSearch || isShowHistorySearch\n        ? (\n            <ScrollView>\n              <View style={styles.content}>\n                { isShowHotSearch ? <HotSearch ref={hotSearchRef} onSearch={onSearch} /> : null }\n                { isShowHistorySearch ? <HistorySearch ref={historySearchRef} onSearch={onSearch} /> : null }\n              </View>\n            </ScrollView>\n          )\n        : (\n            <View style={styles.welcome}>\n              <Text size={22} color={theme['c-font-label']}>{t('search__welcome')}</Text>\n            </View>\n          )\n      : null\n\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    // paddingTop: 15,\n    paddingBottom: 15,\n    paddingLeft: 15,\n    paddingRight: 15,\n  },\n  welcome: {\n    flex: 1,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/HeaderBar/SearchInput.tsx",
    "content": "import { useCallback, useRef, forwardRef, useImperativeHandle, useState } from 'react'\n// import { StyleSheet } from 'react-native'\nimport Input, { type InputType, type InputProps } from '@/components/common/Input'\n\nexport interface SearchInputProps {\n  onChangeText: (text: string) => void\n  onSubmit: (text: string) => void\n  onBlur: () => void\n  onTouchStart: () => void\n}\n\nexport interface SearchInputType {\n  setText: (text: string) => void\n  // getText: () => string\n  focus: () => void\n  blur: () => void\n}\n\nexport default forwardRef<SearchInputType, SearchInputProps>(({ onChangeText, onSubmit, onBlur, onTouchStart }, ref) => {\n  // const theme = useTheme()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    // getText() {\n    //   return text.trim()\n    // },\n    setText(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n    blur() {\n      inputRef.current?.blur()\n    },\n  }))\n\n  const handleChangeText = (text: string) => {\n    setText(text)\n    onChangeText(text.trim())\n  }\n\n  const handleClearText = useCallback(() => {\n    setText('')\n    onChangeText('')\n    onSubmit('')\n  }, [onChangeText, onSubmit])\n\n  const handleSubmit = useCallback<NonNullable<InputProps['onSubmitEditing']>>(({ nativeEvent: { text } }) => {\n    onSubmit(text)\n  }, [onSubmit])\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder=\"Search for something...\"\n      value={text}\n      onChangeText={handleChangeText}\n      // style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n      onBlur={onBlur}\n      onSubmitEditing={handleSubmit}\n      onClearText={handleClearText}\n      onTouchStart={onTouchStart}\n      clearBtn\n    />\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/HeaderBar/index.tsx",
    "content": "import { useRef, forwardRef, useImperativeHandle } from 'react'\nimport { View } from 'react-native'\n\n// import music from '@/utils/musicSdk'\nimport { BorderWidths } from '@/theme'\n// import InsetShadow from 'react-native-inset-shadow'\nimport SourceSelector, {\n  type SourceSelectorType as _SourceSelectorType,\n  type SourceSelectorProps as _SourceSelectorProps,\n} from '@/components/SourceSelector'\nimport SearchInput, { type SearchInputType, type SearchInputProps } from './SearchInput'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { type Source as MusicSource } from '@/store/search/music/state'\nimport { type Source as SonglistSource } from '@/store/search/songlist/state'\n\ntype Sources = Readonly<Array<MusicSource | SonglistSource>>\ntype SourceSelectorProps = _SourceSelectorProps<Sources>\ntype SourceSelectorType = _SourceSelectorType<Sources>\n\nexport interface HeaderBarProps {\n  onSourceChange: SourceSelectorProps['onSourceChange']\n  onTipSearch: SearchInputProps['onChangeText']\n  onSearch: SearchInputProps['onSubmit']\n  onHideTipList: SearchInputProps['onBlur']\n  onShowTipList: SearchInputProps['onTouchStart']\n}\n\nexport interface HeaderBarType {\n  setSourceList: SourceSelectorType['setSourceList']\n  setText: SearchInputType['setText']\n  blur: SearchInputType['blur']\n}\n\n\nexport default forwardRef<HeaderBarType, HeaderBarProps>(({ onSourceChange, onTipSearch, onSearch, onHideTipList, onShowTipList }, ref) => {\n  const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  const searchInputRef = useRef<SearchInputType>(null)\n  const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    setSourceList(list, source) {\n      sourceSelectorRef.current?.setSourceList(list, source)\n    },\n    setText(text) {\n      searchInputRef.current?.setText(text)\n    },\n    blur() {\n      searchInputRef.current?.blur()\n    },\n  }), [])\n\n\n  return (\n    <View style={{ ...styles.searchBar, borderBottomColor: theme['c-border-background'] }}>\n      <View style={styles.selector}>\n        <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} center />\n      </View>\n      <SearchInput\n        ref={searchInputRef}\n        onChangeText={onTipSearch}\n        onSubmit={onSearch}\n        onBlur={onHideTipList}\n        onTouchStart={onShowTipList}\n      />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  searchBar: {\n    flexDirection: 'row',\n    height: 38,\n    zIndex: 2,\n    paddingRight: 10,\n    borderBottomWidth: BorderWidths.normal,\n  },\n  selector: {\n    // width: 86,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/List.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport type { InitState as SearchState } from '@/store/search/state'\nimport type { Source as MusicSource } from '@/store/search/music/state'\nimport type { Source as SongListSource } from '@/store/search/songlist/state'\nimport MusicList, { type MusicListType } from './MusicList'\nimport BlankView, { type BlankViewType } from './BlankView'\nimport SonglistList from './SonglistList'\n\ninterface ListProps {\n  onSearch: (keyword: string) => void\n}\nexport interface ListType {\n  loadList: (text: string, source: MusicSource | SongListSource, type: SearchState['searchType']) => void\n}\n\nexport default forwardRef<ListType, ListProps>(({ onSearch }, ref) => {\n  const [listType, setListType] = useState<SearchState['searchType']>('music')\n  const [showBlankView, setShowListView] = useState(true)\n  const listRef = useRef<MusicListType>(null)\n  const blankViewRef = useRef<BlankViewType>(null)\n\n  useImperativeHandle(ref, () => ({\n    loadList(text, source, type) {\n      if (text) {\n        setShowListView(false)\n        setListType(type)\n        // const listDetailInfo = searchMusicState.listDetailInfo\n        requestAnimationFrame(() => {\n          listRef.current?.loadList(text, source)\n        })\n      } else {\n        setShowListView(true)\n        requestAnimationFrame(() => {\n          blankViewRef.current?.show(source)\n        })\n      }\n    },\n  }), [])\n\n  return (\n    showBlankView\n      ? <BlankView ref={blankViewRef} onSearch={onSearch} />\n      : listType == 'songlist'\n        ? <SonglistList ref={listRef} />\n        : <MusicList ref={listRef} />\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/MusicList.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\nimport OnlineList, { type OnlineListType, type OnlineListProps } from '@/components/OnlineList'\nimport { search } from '@/core/search/music'\nimport searchMusicState, { type Source } from '@/store/search/music/state'\n\n// export type MusicListProps = Pick<OnlineListProps,\n// 'onLoadMore'\n// | 'onPlayList'\n// | 'onRefresh'\n// >\n\nexport interface MusicListType {\n  loadList: (text: string, source: Source) => void\n}\n\nexport default forwardRef<MusicListType, {}>((props, ref) => {\n  const listRef = useRef<OnlineListType>(null)\n  const searchInfoRef = useRef<{ text: string, source: Source }>({ text: '', source: 'kw' })\n  const isUnmountedRef = useRef(false)\n  useImperativeHandle(ref, () => ({\n    async loadList(text, source) {\n      // const listDetailInfo = searchMusicState.listDetailInfo\n      listRef.current?.setList([], false, source == 'all')\n      if (searchMusicState.searchText == text && searchMusicState.source == source && searchMusicState.listInfos[searchMusicState.source]!.list.length) {\n        requestAnimationFrame(() => {\n          listRef.current?.setList(searchMusicState.listInfos[searchMusicState.source]!.list, false, source == 'all')\n        })\n      } else {\n        listRef.current?.setStatus('loading')\n        const page = 1\n        searchInfoRef.current.text = text\n        searchInfoRef.current.source = source\n        return search(text, page, source).then((list) => {\n          // const result = setListInfo(listDetail, id, page)\n          if (isUnmountedRef.current) return\n          requestAnimationFrame(() => {\n            listRef.current?.setList(list, false, source == 'all')\n            listRef.current?.setStatus(searchMusicState.listInfos[searchMusicState.source]!.maxPage <= page ? 'end' : 'idle')\n          })\n        }).catch(() => {\n          listRef.current?.setStatus('error')\n        })\n      }\n    },\n  }), [])\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  const handleRefresh: OnlineListProps['onRefresh'] = () => {\n    const page = 1\n    listRef.current?.setStatus('refreshing')\n    search(searchInfoRef.current.text, page, searchInfoRef.current.source).then((list) => {\n      // const result = setListInfo(listDetail, searchMusicState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(list, false, searchInfoRef.current.source == 'all')\n      listRef.current?.setStatus(searchMusicState.listInfos[searchInfoRef.current.source]!.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      listRef.current?.setStatus('error')\n    })\n  }\n  const handleLoadMore: OnlineListProps['onLoadMore'] = () => {\n    listRef.current?.setStatus('loading')\n    const info = searchMusicState.listInfos[searchInfoRef.current.source]!\n    const page = info?.list.length ? info.page + 1 : 1\n    search(searchInfoRef.current.text, page, searchInfoRef.current.source).then((list) => {\n      // const result = setListInfo(listDetail, searchMusicState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(list, true, searchInfoRef.current.source == 'all')\n      listRef.current?.setStatus(info.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      listRef.current?.setStatus('error')\n    })\n  }\n\n  return <OnlineList\n    ref={listRef}\n    onRefresh={handleRefresh}\n    onLoadMore={handleLoadMore}\n    checkHomePagerIdle\n  />\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Search/SearchTypeSelector.tsx",
    "content": "import { useEffect, useMemo, useState } from 'react'\nimport { ScrollView, TouchableOpacity } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport { type SearchType } from '@/store/search/state'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { getSearchSetting } from '@/utils/data'\nimport { BorderWidths } from '@/theme'\n\nconst SEARCH_TYPE_LIST = [\n  'music',\n  'songlist',\n] as const\n\nexport default () => {\n  const t = useI18n()\n  const theme = useTheme()\n  const [type, setType] = useState<SearchType>('music')\n\n  useEffect(() => {\n    void getSearchSetting().then(info => {\n      setType(info.type)\n    })\n  }, [])\n\n  const list = useMemo(() => {\n    return SEARCH_TYPE_LIST.map(type => ({ label: t(`search_type_${type}`), id: type }))\n  }, [t])\n\n  const handleTypeChange = (type: SearchType) => {\n    setType(type)\n    global.app_event.searchTypeChanged(type)\n  }\n\n  return (\n    <ScrollView style={styles.container} keyboardShouldPersistTaps={'always'} horizontal={true}>\n      {\n        list.map(t => (\n          <TouchableOpacity style={styles.button} onPress={() => { handleTypeChange(t.id) }} key={t.id}>\n            <Text style={{ ...styles.buttonText, borderBottomColor: type == t.id ? theme['c-primary-background-active'] : 'transparent' }} color={type == t.id ? theme['c-primary-font-active'] : theme['c-font']}>{t.label}</Text>\n          </TouchableOpacity>\n        ))\n      }\n    </ScrollView>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    height: '100%',\n    flexGrow: 0,\n    flexShrink: 1,\n    // paddingLeft: 5,\n    // paddingRight: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  button: {\n    // height: 38,\n    // lineHeight: 38,\n    justifyContent: 'center',\n    paddingLeft: 8,\n    paddingRight: 8,\n    // width: 80,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  buttonText: {\n    // height: 38,\n    // lineHeight: 38,\n    textAlign: 'center',\n    paddingLeft: 2,\n    paddingRight: 2,\n    // paddingTop: 10,\n    paddingTop: 3,\n    paddingBottom: 3,\n    borderBottomWidth: BorderWidths.normal3,\n    // width: 80,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Search/SonglistList.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\n\nimport { search } from '@/core/search/songlist'\nimport Songlist, { type SonglistProps, type SonglistType } from '@/screens/Home/Views/SongList/components/Songlist'\nimport searchSonglistState, { type Source } from '@/store/search/songlist/state'\n\n// export type MusicListProps = Pick<OnlineListProps,\n// 'onLoadMore'\n// | 'onPlayList'\n// | 'onRefresh'\n// >\n\nexport interface MusicListType {\n  loadList: (text: string, source: Source) => void\n}\n\nexport default forwardRef<MusicListType, {}>((props, ref) => {\n  const listRef = useRef<SonglistType>(null)\n  const searchInfoRef = useRef<{ text: string, source: Source }>({ text: '', source: 'kw' })\n  const isUnmountedRef = useRef(false)\n  useImperativeHandle(ref, () => ({\n    async loadList(text, source) {\n      // const listDetailInfo = searchSonglistState.listDetailInfo\n      listRef.current?.setList([], source == 'all')\n      if (searchSonglistState.searchText == text && searchSonglistState.source == source && searchSonglistState.listInfos[searchSonglistState.source]!.list.length) {\n        requestAnimationFrame(() => {\n          listRef.current?.setList(searchSonglistState.listInfos[searchSonglistState.source]!.list, source == 'all')\n        })\n      } else {\n        listRef.current?.setStatus('loading')\n        const page = 1\n        searchInfoRef.current.text = text\n        searchInfoRef.current.source = source\n        return search(text, page, source).then((list) => {\n          // const result = setListInfo(listDetail, id, page)\n          if (isUnmountedRef.current) return\n          requestAnimationFrame(() => {\n            listRef.current?.setList(list, source == 'all')\n            listRef.current?.setStatus(searchSonglistState.maxPages[searchSonglistState.source] == page ? 'end' : 'idle')\n          })\n        }).catch(() => {\n          listRef.current?.setStatus('error')\n        })\n      }\n    },\n  }), [])\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  const handleRefresh: SonglistProps['onRefresh'] = () => {\n    const page = 1\n    listRef.current?.setStatus('refreshing')\n    search(searchInfoRef.current.text, page, searchInfoRef.current.source).then((list) => {\n      // const result = setListInfo(listDetail, searchSonglistState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(list, searchInfoRef.current.source == 'all')\n      listRef.current?.setStatus(searchSonglistState.maxPages[searchSonglistState.source] == page ? 'end' : 'idle')\n    }).catch(() => {\n      listRef.current?.setStatus('error')\n    })\n  }\n  const handleLoadMore: SonglistProps['onLoadMore'] = () => {\n    listRef.current?.setStatus('loading')\n    const info = searchSonglistState.listInfos[searchInfoRef.current.source]!\n    const page = info.list.length ? info.page + 1 : 1\n    search(searchInfoRef.current.text, page, searchInfoRef.current.source).then((list) => {\n      // const result = setListInfo(listDetail, searchSonglistState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(list, searchInfoRef.current.source == 'all')\n      listRef.current?.setStatus(searchSonglistState.maxPages[searchSonglistState.source] == page ? 'end' : 'idle')\n    }).catch(() => {\n      listRef.current?.setStatus('error')\n    })\n  }\n\n  return <Songlist\n    ref={listRef}\n    onRefresh={handleRefresh}\n    onLoadMore={handleLoadMore}\n  />\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Search/TipList.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useEffect } from 'react'\nimport SearchTipList, { type SearchTipListProps as _SearchTipListProps, type SearchTipListType as _SearchTipListType } from '@/components/SearchTipList'\nimport Button from '@/components/common/Button'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport musicSdk from '@/utils/musicSdk'\nimport searchState, { type InitState as SearchState } from '@/store/search/state'\nimport { setSearchText, setTipList, setTipListInfo } from '@/core/search/search'\nimport { debounce } from '@/utils'\n\nexport const ITEM_HEIGHT = scaleSizeH(36)\n\nexport const debounceTipSearch = debounce((keyword: string, source: SearchState['temp_source'], callback: (list: string[]) => void) => {\n  // console.log(reslutList)\n  void musicSdk[source].tipSearch.search(keyword).then(callback)\n}, 200)\n\n\nexport type SearchTipListProps = _SearchTipListProps<string>\nexport type SearchTipListType = _SearchTipListType<string>\n\ninterface TipListProps {\n  onSearch: (keyword: string) => void\n}\nexport interface TipListType {\n  search: (keyword: string, height: number) => void\n  show: (height: number) => void\n  hide: () => void\n}\n\nexport default forwardRef<TipListType, TipListProps>(({ onSearch }, ref) => {\n  const searchTipListRef = useRef<SearchTipListType>(null)\n  const [visible, setVisible] = useState(false)\n  const visibleListRef = useRef(false)\n  const isUnmountedRef = useRef(false)\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  const handleSearch = (keyword: string, height: number) => {\n    searchTipListRef.current?.setHeight(height)\n    setSearchText(keyword)\n    if (keyword) {\n      setTipListInfo(keyword, searchState.temp_source)\n      debounceTipSearch(keyword, searchState.temp_source, (list) => {\n        if (keyword != searchState.tipListInfo.text) return\n        setTipList(list)\n        if (!visibleListRef.current || isUnmountedRef.current) return\n        searchTipListRef.current?.setList(list)\n      })\n    } else {\n      setTipListInfo(keyword, searchState.temp_source)\n      setTipList([])\n      searchTipListRef.current?.setList([])\n    }\n  }\n\n  const handleShowList = (height: number) => {\n    searchTipListRef.current?.setHeight(height)\n    if (searchState.tipListInfo.list.length) {\n      visibleListRef.current = true\n      searchTipListRef.current?.setList([...searchState.tipListInfo.list])\n    }\n  }\n\n  useImperativeHandle(ref, () => ({\n    search(keyword, height) {\n      if (visible) handleSearch(keyword, height)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleSearch(keyword, height)\n        })\n      }\n    },\n    show(height) {\n      visibleListRef.current = true\n      if (visible) handleShowList(height)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShowList(height)\n        })\n      }\n    },\n    hide() {\n      requestAnimationFrame(() => {\n        visibleListRef.current = false\n        searchTipListRef.current?.setList([])\n      })\n    },\n  }), [visible])\n\n  const renderItem: SearchTipListProps['renderItem'] = ({ item, index }) => {\n    return (\n      <Button style={styles.item} onPress={() => { onSearch(item) }} key={index}>\n        <Text numberOfLines={1}>{item}</Text>\n      </Button>\n    )\n  }\n  const getkey: SearchTipListProps['keyExtractor'] = (item, index) => String(index)\n  const getItemLayout: SearchTipListProps['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n\n  return (\n    visible\n      ? <SearchTipList\n          ref={searchTipListRef}\n          renderItem={renderItem}\n          onPressBg={() => searchTipListRef.current?.setList([])}\n          keyExtractor={getkey}\n          getItemLayout={getItemLayout}\n        />\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  item: {\n    height: ITEM_HEIGHT,\n    flexDirection: 'row',\n    alignItems: 'center',\n    paddingLeft: 15,\n    paddingRight: 15,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Search/index.tsx",
    "content": "import { useRef, useEffect } from 'react'\nimport { type LayoutChangeEvent, View } from 'react-native'\n\n// import music from '@/utils/musicSdk'\n// import InsetShadow from 'react-native-inset-shadow'\n// import TipList from './components/TipList'\n// import MusicList from './components/MusicList'\nimport HeaderBar, { type HeaderBarProps, type HeaderBarType } from './HeaderBar'\nimport searchState, { type SearchType } from '@/store/search/state'\nimport searchMusicState from '@/store/search/music/state'\nimport searchSonglistState from '@/store/search/songlist/state'\nimport { getSearchSetting, saveSearchSetting } from '@/utils/data'\nimport { createStyle } from '@/utils/tools'\nimport TipList, { type TipListType } from './TipList'\nimport List, { type ListType } from './List'\nimport { addHistoryWord } from '@/core/search/search'\n\n\ninterface SearchInfo {\n  temp_source: LX.OnlineSource\n  source: LX.OnlineSource | 'all'\n  searchType: 'music' | 'songlist'\n}\n\nexport default () => {\n  const headerBarRef = useRef<HeaderBarType>(null)\n  const searchTipListRef = useRef<TipListType>(null)\n  const listRef = useRef<ListType>(null)\n  const layoutHeightRef = useRef<number>(0)\n  const searchInfo = useRef<SearchInfo>({ temp_source: 'kw', source: 'kw', searchType: 'music' })\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null)\n\n  useEffect(() => {\n    void getSearchSetting().then(info => {\n      // info.type = 'music'\n      searchInfo.current.temp_source = info.temp_source\n      searchInfo.current.source = info.source\n      searchInfo.current.searchType = info.type\n      switch (info.type) {\n        case 'music':\n          headerBarRef.current?.setSourceList(searchMusicState.sources, info.source)\n          break\n        case 'songlist':\n          headerBarRef.current?.setSourceList(searchSonglistState.sources, info.source)\n          break\n      }\n      headerBarRef.current?.setText(searchState.searchText)\n      listRef.current?.loadList(searchState.searchText, searchInfo.current.source, searchInfo.current.searchType)\n    })\n\n    const handleTypeChange = (type: SearchType) => {\n      searchInfo.current.searchType = type\n      void saveSearchSetting({ type })\n      listRef.current?.loadList(searchState.searchText, searchInfo.current.source, type)\n    }\n    global.app_event.on('searchTypeChanged', handleTypeChange)\n\n    return () => {\n      global.app_event.off('searchTypeChanged', handleTypeChange)\n    }\n  }, [])\n\n\n  const handleLayout = (e: LayoutChangeEvent) => {\n    layoutHeightRef.current = e.nativeEvent.layout.height\n  }\n\n  const handleSourceChange: HeaderBarProps['onSourceChange'] = (source) => {\n    searchInfo.current.source = source\n    void saveSearchSetting({ source })\n    listRef.current?.loadList(searchState.searchText, source, searchInfo.current.searchType)\n  }\n  const handleTipSearch: HeaderBarProps['onTipSearch'] = (text) => {\n    setTimeout(() => {\n      searchTipListRef.current?.search(text, layoutHeightRef.current)\n    }, 500)\n  }\n  const handleHideTipList = () => {\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current)\n      timeoutRef.current = null\n    }\n    searchTipListRef.current?.hide()\n  }\n  const handleSearch: HeaderBarProps['onSearch'] = (text) => {\n    handleHideTipList()\n    searchTipListRef.current?.search(text, layoutHeightRef.current)\n    headerBarRef.current?.setText(text)\n    headerBarRef.current?.blur()\n    void addHistoryWord(text)\n    listRef.current?.loadList(text, searchInfo.current.source, searchInfo.current.searchType)\n  }\n  const handleShowTipList: HeaderBarProps['onShowTipList'] = () => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current)\n    timeoutRef.current = setTimeout(() => {\n      searchTipListRef.current?.show(layoutHeightRef.current)\n    }, 500)\n  }\n\n  return (\n    <View style={styles.container}>\n      <HeaderBar\n        ref={headerBarRef}\n        onSourceChange={handleSourceChange}\n        onTipSearch={handleTipSearch}\n        onSearch={handleSearch}\n        onHideTipList={handleHideTipList}\n        onShowTipList={handleShowTipList}\n      />\n      <View style={styles.content} onLayout={handleLayout}>\n        <TipList ref={searchTipListRef} onSearch={handleSearch} />\n        <List ref={listRef} onSearch={handleSearch} />\n      </View>\n    </View>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    width: '100%',\n    flex: 1,\n  },\n  content: {\n    flex: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Horizontal/NavList.tsx",
    "content": "import { memo, useRef, useState } from 'react'\nimport { View, TouchableOpacity, FlatList, type FlatListProps } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\n\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { SETTING_SCREENS, type SettingScreenIds } from '../Main'\nimport { useI18n } from '@/lang'\n\ntype FlatListType = FlatListProps<SettingScreenIds>\n\nconst ITEM_HEIGHT = scaleSizeH(40)\n\nconst ListItem = memo(({ id, activeId, onPress }: {\n  onPress: (item: SettingScreenIds) => void\n  activeId: string\n  id: SettingScreenIds\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n\n  const active = activeId == id\n\n  const handlePress = () => {\n    onPress(id)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, height: ITEM_HEIGHT }}>\n      {\n        active\n          ? <Icon style={styles.listActiveIcon} name=\"chevron-right\" size={12} color={theme['c-primary-font']} />\n          : null\n      }\n      <TouchableOpacity style={styles.listName} onPress={handlePress}>\n        <Text numberOfLines={1} size={16} color={active ? theme['c-primary-font'] : theme['c-font']}>{t(`setting_${id}`)}</Text>\n      </TouchableOpacity>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return !!(prevProps.id === nextProps.id &&\n    prevProps.activeId != nextProps.id &&\n    nextProps.activeId != nextProps.id\n  )\n})\n\n\nexport default ({ onChangeId }: {\n  onChangeId: (id: SettingScreenIds) => void\n}) => {\n  const flatListRef = useRef<FlatList>(null)\n  const [activeId, setActiveId] = useState(global.lx.settingActiveId)\n\n  const handleChangeId = (id: SettingScreenIds) => {\n    onChangeId(id)\n    setActiveId(id)\n    global.lx.settingActiveId = id\n  }\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => (\n    <ListItem\n      key={item}\n      id={item}\n      activeId={activeId}\n      onPress={handleChangeId}\n    />\n  )\n  const getkey: FlatListType['keyExtractor'] = item => item\n  const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {\n    return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  }\n\n  return (\n    <FlatList\n      ref={flatListRef}\n      style={styles.container}\n      data={SETTING_SCREENS}\n      maxToRenderPerBatch={9}\n      // updateCellsBatchingPeriod={80}\n      windowSize={9}\n      removeClippedSubviews={true}\n      initialNumToRender={18}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      // extraData={activeIndex}\n      getItemLayout={getItemLayout}\n    />\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    flexShrink: 1,\n    flexGrow: 0,\n  },\n  // listContainer: {\n  //   // borderBottomWidth: BorderWidths.normal2,\n  // },\n\n  listItem: {\n    height: 'auto',\n    flexDirection: 'row',\n    alignItems: 'center',\n    paddingRight: 10,\n    paddingLeft: 10,\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  listActiveIcon: {\n    // width: 18,\n    marginLeft: 3,\n    // paddingRight: 5,\n    textAlign: 'center',\n  },\n  listName: {\n    height: '100%',\n    // height: 46,\n    // paddingTop: 12,\n    // paddingBottom: 12,\n    justifyContent: 'center',\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingLeft: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Horizontal/index.tsx",
    "content": "import { useRef } from 'react'\nimport { ScrollView, View } from 'react-native'\nimport NavList from './NavList'\nimport Main, { type MainType } from '../Main'\nimport { createStyle } from '@/utils/tools'\nimport { BorderWidths } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    flexDirection: 'row',\n    borderTopWidth: BorderWidths.normal,\n  },\n  nav: {\n    height: '100%',\n    width: '22%',\n    borderRightWidth: BorderWidths.normal,\n  },\n  main: {\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingTop: 15,\n    paddingBottom: 15,\n    flex: 0,\n  },\n})\n\nexport default () => {\n  const theme = useTheme()\n  const mainRef = useRef<MainType>(null)\n\n  return (\n    <View style={{ ...styles.container, borderTopColor: theme['c-border-background'] }}>\n      <View style={{ ...styles.nav, borderRightColor: theme['c-border-background'] }}>\n        <NavList onChangeId={(id) => mainRef.current?.setActiveId(id)} />\n      </View>\n      <ScrollView keyboardShouldPersistTaps={'always'}>\n        <View style={styles.main}>\n          <Main ref={mainRef} />\n        </View>\n      </ScrollView>\n    </View>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Main.tsx",
    "content": "import { forwardRef, useImperativeHandle, useMemo, useState } from 'react'\n\nimport Basic from './settings/Basic'\nimport Player from './settings/Player'\nimport LyricDesktop from './settings/LyricDesktop'\nimport Search from './settings/Search'\nimport List from './settings/List'\nimport Sync from './settings/Sync'\nimport Backup from './settings/Backup'\nimport Other from './settings/Other'\nimport Version from './settings/Version'\nimport About from './settings/About'\n\nexport const SETTING_SCREENS = [\n  'basic',\n  'player',\n  'lyric_desktop',\n  'search',\n  'list',\n  'sync',\n  'backup',\n  'other',\n  'version',\n  'about',\n] as const\n\nexport type SettingScreenIds = typeof SETTING_SCREENS[number]\n\n// interface MainProps {\n//   onUpdateActiveId: (id: string) => void\n// }\nexport interface MainType {\n  setActiveId: (id: SettingScreenIds) => void\n}\n\nconst Main = forwardRef<MainType, {}>((props, ref) => {\n  const [id, setId] = useState(global.lx.settingActiveId)\n\n  useImperativeHandle(ref, () => ({\n    setActiveId(id) {\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          setId(id)\n        })\n      })\n    },\n  }))\n\n  const component = useMemo(() => {\n    switch (id) {\n      case 'player': return <Player />\n      case 'lyric_desktop': return <LyricDesktop />\n      case 'search': return <Search />\n      case 'list': return <List />\n      case 'sync': return <Sync />\n      case 'backup': return <Backup />\n      case 'other': return <Other />\n      case 'version': return <Version />\n      case 'about': return <About />\n      case 'basic':\n      default: return <Basic />\n    }\n  }, [id])\n\n  return component\n})\n\n\nexport default Main\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Vertical/Header.tsx",
    "content": "import { forwardRef, useImperativeHandle, useState } from 'react'\nimport { TouchableOpacity } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\nimport { BorderWidths } from '@/theme'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { type SettingScreenIds } from '../Main'\n\nexport interface HeaderProps {\n  onShowNavBar: () => void\n}\nexport interface HeaderType {\n  setActiveId: (id: SettingScreenIds) => void\n}\n\nexport default forwardRef<HeaderType, HeaderProps>(({ onShowNavBar }, ref) => {\n  const [activeId, setActiveId] = useState(global.lx.settingActiveId)\n  const theme = useTheme()\n  const t = useI18n()\n\n  useImperativeHandle(ref, () => ({\n    setActiveId(id) {\n      setActiveId(id)\n    },\n  }))\n\n  return (\n    <TouchableOpacity onPress={onShowNavBar} style={{ ...styles.currentList, borderBottomColor: theme['c-border-background'] }}>\n      <Icon style={styles.currentListIcon} color={theme['c-button-font']} name=\"chevron-right\" size={12} />\n      <Text numberOfLines={1} size={16} style={styles.currentListText} color={theme['c-button-font']}>{t(`setting_${activeId}`)}</Text>\n    </TouchableOpacity>\n  )\n})\n\n\nconst styles = createStyle({\n  currentList: {\n    flexDirection: 'row',\n    paddingRight: 2,\n    height: 40,\n    alignItems: 'center',\n    borderBottomWidth: BorderWidths.normal,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  currentListIcon: {\n    paddingLeft: 15,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 0,\n  },\n  currentListText: {\n    flex: 1,\n    // minWidth: 70,\n    // paddingLeft: 10,\n    paddingRight: 10,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Vertical/Main.tsx",
    "content": "import { memo } from 'react'\nimport { FlatList, type FlatListProps } from 'react-native'\n\nimport Basic from '../settings/Basic'\nimport Player from '../settings/Player'\nimport LyricDesktop from '../settings/LyricDesktop'\nimport Search from '../settings/Search'\nimport List from '../settings/List'\nimport Sync from '../settings/Sync'\nimport Backup from '../settings/Backup'\nimport Other from '../settings/Other'\nimport Version from '../settings/Version'\nimport About from '../settings/About'\nimport { createStyle } from '@/utils/tools'\nimport { SETTING_SCREENS, type SettingScreenIds } from '../Main'\n\ntype FlatListType = FlatListProps<SettingScreenIds>\n\n\nconst styles = createStyle({\n  content: {\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingTop: 15,\n    paddingBottom: 15,\n    flex: 0,\n  },\n})\n\nconst ListItem = memo(({\n  id,\n}: { id: SettingScreenIds }) => {\n  switch (id) {\n    case 'player': return <Player />\n    case 'lyric_desktop': return <LyricDesktop />\n    case 'search': return <Search />\n    case 'list': return <List />\n    case 'sync': return <Sync />\n    case 'backup': return <Backup />\n    case 'other': return <Other />\n    case 'version': return <Version />\n    case 'about': return <About />\n    case 'basic': return <Basic />\n  }\n}, () => true)\n\nexport default () => {\n  const renderItem: FlatListType['renderItem'] = ({ item }) => <ListItem id={item} />\n  const getkey: FlatListType['keyExtractor'] = item => item\n\n  return (\n    <FlatList\n      data={SETTING_SCREENS}\n      keyboardShouldPersistTaps={'always'}\n      renderItem={renderItem}\n      keyExtractor={getkey}\n      contentContainerStyle={styles.content}\n      maxToRenderPerBatch={2}\n      // updateCellsBatchingPeriod={80}\n      windowSize={2}\n      // removeClippedSubviews={true}\n      initialNumToRender={1}\n    />\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Vertical/NavList.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View, TouchableOpacity, ScrollView } from 'react-native'\n\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { SETTING_SCREENS, type SettingScreenIds } from '../Main'\nimport { useI18n } from '@/lang'\nimport { BorderRadius, BorderWidths } from '@/theme'\n\n\nconst ListItem = memo(({ id, activeId, onPress }: {\n  onPress: (item: SettingScreenIds) => void\n  activeId: string\n  id: SettingScreenIds\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n\n  const active = activeId == id\n\n  const handlePress = () => {\n    onPress(id)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, backgroundColor: active ? theme['c-primary-background-active'] : 'transparent' }}>\n      <TouchableOpacity style={styles.listName} onPress={handlePress}>\n        <Text numberOfLines={1} color={active ? theme['c-primary-font'] : theme['c-font']}>{t(`setting_${id}`)}</Text>\n      </TouchableOpacity>\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return !!(prevProps.id === nextProps.id &&\n    prevProps.activeId != nextProps.id &&\n    nextProps.activeId != nextProps.id\n  )\n})\n\n\nexport default ({ onChangeId }: {\n  onChangeId: (id: SettingScreenIds) => void\n}) => {\n  const [activeId, setActiveId] = useState(global.lx.settingActiveId)\n  const theme = useTheme()\n\n  const handleChangeId = useCallback((id: SettingScreenIds) => {\n    onChangeId(id)\n    setActiveId(id)\n    global.lx.settingActiveId = id\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <ScrollView horizontal style={{ ...styles.container, borderBottomColor: theme['c-border-background'] }} contentContainerStyle={styles.contentContainer} keyboardShouldPersistTaps={'always'}>\n      {\n        SETTING_SCREENS.map(id => <ListItem key={id} id={id} activeId={activeId} onPress={handleChangeId} />)\n      }\n    </ScrollView>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    height: 50,\n    flexGrow: 0,\n    flexShrink: 0,\n    borderBottomWidth: BorderWidths.normal,\n    opacity: 0.7,\n  },\n  contentContainer: {\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    padding: 5,\n    // backgroundColor: 'rgba(0, 0, 0, 0.1)',\n  },\n  // listContainer: {\n  //   // borderBottomWidth: BorderWidths.normal2,\n  // },\n\n  listItem: {\n    // width: '33.33%',\n    height: 40,\n    paddingLeft: 15,\n    paddingRight: 15,\n    // height: 'auto',\n    // flexDirection: 'row',\n    // alignItems: 'center',\n    paddingHorizontal: 5,\n    // paddingVertical: 10,\n    borderRadius: BorderRadius.normal,\n    marginBottom: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  listName: {\n    justifyContent: 'center',\n    alignItems: 'center',\n    flex: 1,\n    // paddingLeft: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/Vertical/index.tsx",
    "content": "export { default } from './Main'\n// // import { View } from 'react-native'\n// import Main from './Main'\n// import { createStyle } from '@/utils/tools'\n\n\n// const Content = () => {\n//   return (\n//     <View style={styles.container}>\n//       <Main />\n//     </View>\n//   )\n// }\n\n// const styles = createStyle({\n//   container: {\n//     flex: 1,\n//     flexDirection: 'column',\n//   },\n//   // main: {\n//   //   paddingLeft: 15,\n//   //   paddingRight: 15,\n//   //   paddingTop: 15,\n//   //   paddingBottom: 15,\n//   // },\n// })\n\n// export default Content\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/Button.tsx",
    "content": "import { memo } from 'react'\n\nimport Button, { type BtnProps } from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\n\ntype ButtonProps = BtnProps\n\nexport default memo(({ disabled, onPress, children }: ButtonProps) => {\n  const theme = useTheme()\n\n  return (\n    <Button style={{ ...styles.button, backgroundColor: theme['c-button-background'] }} onPress={onPress} disabled={disabled}>\n      <Text size={14} color={theme['c-button-font']}>{children}</Text>\n    </Button>\n  )\n})\n\nconst styles = createStyle({\n  button: {\n    paddingLeft: 10,\n    paddingRight: 10,\n    paddingTop: 5,\n    paddingBottom: 5,\n    borderRadius: 4,\n    marginRight: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/CheckBoxItem.tsx",
    "content": "import { memo } from 'react'\n\nimport { View } from 'react-native'\n\nimport CheckBox, { type CheckBoxProps } from '@/components/common/CheckBox'\nimport { createStyle } from '@/utils/tools'\n\n\nexport default memo((props: CheckBoxProps) => {\n  return (\n    <View style={styles.container}>\n      <CheckBox {...props} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    paddingLeft: 25,\n    // marginTop: -10,\n    // marginBottom: 0,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/InputItem.tsx",
    "content": "import { memo, useState, useEffect, useRef } from 'react'\n\nimport { StyleSheet, View, Keyboard } from 'react-native'\nimport type { InputType, InputProps } from '@/components/common/Input'\nimport Input from '@/components/common/Input'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\n\n\nexport interface InputItemProps extends InputProps {\n  value: string\n  label: string\n  onChanged: (text: string, callback: (vlaue: string) => void) => void\n}\n\nexport default memo(({ value, label, onChanged, ...props }: InputItemProps) => {\n  const [text, setText] = useState(value)\n  const textRef = useRef(value)\n  const isMountRef = useRef(false)\n  const inputRef = useRef<InputType>(null)\n  const theme = useTheme()\n  const saveValue = () => {\n    onChanged?.(text, (value: string) => {\n      if (!isMountRef.current) return\n      const newValue = String(value)\n      setText(newValue)\n      textRef.current = newValue\n    })\n  }\n  useEffect(() => {\n    isMountRef.current = true\n    return () => {\n      isMountRef.current = false\n    }\n  }, [])\n  useEffect(() => {\n    const handleKeyboardDidHide = () => {\n      if (!inputRef.current?.isFocused()) return\n      onChanged?.(textRef.current, value => {\n        if (!isMountRef.current) return\n        const newValue = String(value)\n        setText(newValue)\n        textRef.current = newValue\n      })\n    }\n    const keyboardDidHide = Keyboard.addListener('keyboardDidHide', handleKeyboardDidHide)\n\n    return () => {\n      keyboardDidHide.remove()\n    }\n  }, [onChanged])\n  useEffect(() => {\n    if (value != text) {\n      const newValue = String(value)\n      setText(newValue)\n      textRef.current = newValue\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [value])\n  const handleSetSelectMode = (text: string) => {\n    setText(text)\n    textRef.current = text\n  }\n  return (\n    <View style={styles.container}>\n      <Text style={styles.label} size={14}>{label}</Text>\n      <Input\n        value={text}\n        ref={inputRef}\n        onChangeText={handleSetSelectMode}\n        style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n        {...props}\n        onBlur={saveValue}\n       />\n    </View>\n  )\n})\n\nconst styles = StyleSheet.create({\n  container: {\n    paddingLeft: 25,\n    marginBottom: 15,\n  },\n  label: {\n    marginBottom: 2,\n  },\n  input: {\n    backgroundColor: 'rgba(0,0,0,0.2)',\n    flexGrow: 1,\n    flexShrink: 1,\n    borderRadius: 4,\n    // paddingTop: 3,\n    // paddingBottom: 3,\n    maxWidth: 300,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/Section.tsx",
    "content": "import { View } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\n\n\ninterface Props {\n  title: string\n  children: React.ReactNode | React.ReactNode[]\n}\n\nexport default ({ title, children }: Props) => {\n  const theme = useTheme()\n\n  return (\n    <View style={styles.container}>\n      <Text style={{ ...styles.title, borderLeftColor: theme['c-primary'] }} size={16} >{title}</Text>\n      <View>\n        {children}\n      </View>\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    // paddingLeft: 10,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  title: {\n    borderLeftWidth: 5,\n    paddingLeft: 12,\n    marginBottom: 10,\n    // lineHeight: 16,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/Slider.tsx",
    "content": "\nimport { memo } from 'react'\n\nimport Slider, { type SliderProps } from '@react-native-community/slider'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\n\nexport type {\n  SliderProps,\n}\n\nexport default memo(({ value, minimumValue, maximumValue, onSlidingStart, onSlidingComplete, onValueChange, step }: SliderProps) => {\n  const theme = useTheme()\n\n  return (\n    <Slider\n      value={value}\n      style={styles.slider}\n      minimumValue={minimumValue}\n      maximumValue={maximumValue}\n      minimumTrackTintColor={theme['c-button-background-active']}\n      maximumTrackTintColor={theme['c-button-background']}\n      thumbTintColor={theme['c-primary-light-100']}\n      onSlidingStart={onSlidingStart}\n      onSlidingComplete={onSlidingComplete}\n      onValueChange={onValueChange}\n      step={step}\n    />\n  )\n})\n\n\nconst styles = createStyle({\n  slider: {\n    flexShrink: 0,\n    flexGrow: 1,\n    // width: '100%',\n    maxWidth: 300,\n    height: 40,\n    marginTop: -6,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/components/SubTitle.tsx",
    "content": "import { memo } from 'react'\n\nimport { View } from 'react-native'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\nexport default memo(({ title, children }: {\n  title: string\n  children: React.ReactNode | React.ReactNode[]\n}) => {\n  return (\n    <View style={styles.container}>\n      <Text style={styles.title}>{title}</Text>\n      {children}\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    paddingLeft: 25,\n    marginBottom: 18,\n  },\n  title: {\n    marginLeft: -10,\n    marginBottom: 10,\n    // lineHeight: 16,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/index.tsx",
    "content": "import { useHorizontalMode } from '@/utils/hooks'\nimport Vertical from './Vertical'\nimport Horizontal from './Horizontal'\nimport { useBackHandler } from '@/utils/hooks/useBackHandler'\nimport { useCallback } from 'react'\n// import { AppColors } from '@/theme'\nimport commonState from '@/store/common/state'\nimport { setNavActiveId } from '@/core/common'\n\nexport type { SettingScreenIds } from './Main'\n\nexport default () => {\n  const isHorizontalMode = useHorizontalMode()\n  useBackHandler(useCallback(() => {\n    if (Object.keys(commonState.componentIds).length == 1 && commonState.navActiveId == 'nav_setting') {\n      setNavActiveId(commonState.lastNavActiveId)\n      return true\n    }\n    return false\n  }, []))\n\n  return isHorizontalMode\n    ? <Horizontal />\n    : <Vertical />\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/About.tsx",
    "content": "import { memo } from 'react'\nimport { View, TouchableOpacity } from 'react-native'\n\nimport Section from '../components/Section'\n// import Button from './components/Button'\n\nimport { createStyle, openUrl } from '@/utils/tools'\n// import { showPactModal } from '@/navigation'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { showPactModal } from '@/core/common'\n\n// const qqGroupUrl = 'mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26jump_from%3Dwebapi%26k%3Du1zyxek8roQAwic44nOkBXtG9CfbAxFw'\n// const qqGroupUrl2 = 'mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26jump_from%3Dwebapi%26k%3D-l4kNZ2bPQAuvfCQFFhl1UoibvF5wcrQ'\n// const qqGroupWebUrl = 'https://qm.qq.com/cgi-bin/qm/qr?k=jRZkyFSZ4FmUuTHA3P_RAXbbUO_Rrn5e&jump_from=webapi'\n// const qqGroupWebUrl2 = 'https://qm.qq.com/cgi-bin/qm/qr?k=HPNJEfrZpBZ9T8szYWbe2d5JrAAeOt_l&jump_from=webapi'\n\nexport default memo(() => {\n  const theme = useTheme()\n  const t = useI18n()\n  const openHomePage = () => {\n    void openUrl('https://github.com/lyswhut/lx-music-mobile#readme')\n  }\n  const openIssuePage = () => {\n    void openUrl('https://github.com/lyswhut/lx-music-mobile/issues?q=is%3Aissue+')\n  }\n  const openGHReleasePage = () => {\n    void openUrl('https://github.com/lyswhut/lx-music-mobile/releases')\n  }\n  const openFAQPage = () => {\n    void openUrl('https://lyswhut.github.io/lx-music-doc/mobile/faq')\n  }\n  // const openIssuesPage = () => {\n  //   openUrl('https://github.com/lyswhut/lx-music-mobile/issues')\n  // }\n  const openPactModal = () => {\n    showPactModal()\n  }\n  const openPartPage = () => {\n    void openUrl('https://github.com/lyswhut/lx-music-mobile#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')\n  }\n\n  // const goToQQGroup = () => {\n  //   openUrl(qqGroupUrl).catch(() => {\n  //     void openUrl(qqGroupWebUrl)\n  //   })\n  // }\n  // const goToQQGroup2 = () => {\n  //   openUrl(qqGroupUrl2).catch(() => {\n  //     void openUrl(qqGroupWebUrl2)\n  //   })\n  // }\n\n  const textLinkStyle = {\n    ...styles.text,\n    textDecorationLine: 'underline',\n    color: theme['c-primary-font'],\n    // fontSize: 14,\n  } as const\n\n\n  return (\n    <Section title={t('setting_about')}>\n      <View style={styles.part}>\n        <Text style={styles.text} >本软件完全免费，代码已开源。开源地址：</Text>\n        <TouchableOpacity onPress={openHomePage}>\n          <Text style={textLinkStyle}>https://github.com/lyswhut/lx-music-mobile</Text>\n        </TouchableOpacity>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>最新版下载地址：</Text>\n        <TouchableOpacity onPress={openGHReleasePage}>\n          <Text style={textLinkStyle}>GitHub Releases</Text>\n        </TouchableOpacity>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text} >软件的常见问题可转至：</Text>\n        <TouchableOpacity onPress={openFAQPage}>\n          <Text style={textLinkStyle}>移动版常见问题</Text>\n        </TouchableOpacity>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}><Text style={styles.boldText}>本软件没有客服</Text>，但我们整理了一些常见的使用问题。<Text style={styles.boldText} >仔细、仔细、仔细</Text>地阅读常见问题后，</Text>\n        <Text style={styles.text}>仍有问题可到 GitHub </Text>\n        <TouchableOpacity onPress={openIssuePage}>\n          <Text style={textLinkStyle}>提交 Issue</Text>\n        </TouchableOpacity>\n        <Text style={styles.text}>。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>由于软件开发的初衷仅是为了对新技术的学习与研究，因此软件直至停止维护都将会一直保持纯净。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>目前本项目的原始发布地址<Text style={styles.boldText}>只有 GitHub</Text>，其他渠道均为第三方转载发布，可信度请自行鉴别。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}><Text style={styles.boldText}>本项目没有微信公众号之类的所谓「官方账号」，也未在小米、华为、vivo 等应用商店发布同名应用，谨防被骗！</Text></Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>若你使用过程中遇到<Text style={styles.boldText}>广告</Text>或者<Text style={styles.boldText}>引流</Text>（如需要加群、关注公众号之类才能使用或者升级）的信息，则表明你当前运行的软件是「第三方修改版」。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>若在升级新版本时提示「<Text style={styles.boldText}>签名不一致</Text>」，则表明你手机上的旧版本或者将要安装的新版本中<Text style={styles.boldText}>有一方</Text>是「<Text style={styles.boldText}>第三方修改版</Text>」。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>你已签署本软件的</Text>\n        <TouchableOpacity onPress={openPactModal}><Text style={styles.text} color={theme['c-primary-font']}>许可协议</Text></TouchableOpacity>\n        <Text style={styles.text}>，协议的在线版本在</Text>\n        <TouchableOpacity onPress={openPartPage}><Text style={textLinkStyle}>这里</Text></TouchableOpacity>\n        <Text style={styles.text}>。</Text>\n      </View>\n      <View style={styles.part}>\n        <Text style={styles.text}>By: </Text>\n        <Text style={styles.text}>落雪无痕</Text>\n      </View>\n    </Section>\n  )\n})\n\nconst styles = createStyle({\n  part: {\n    marginLeft: 15,\n    marginRight: 15,\n    marginBottom: 10,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n  text: {\n    fontSize: 14,\n    textAlignVertical: 'bottom',\n  },\n  boldText: {\n    fontSize: 14,\n    fontWeight: 'bold',\n    textAlignVertical: 'bottom',\n  },\n  throughText: {\n    fontSize: 14,\n    textDecorationLine: 'line-through',\n    textAlignVertical: 'bottom',\n  },\n  btn: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Backup/All.js",
    "content": ""
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Backup/ListImportExport.tsx",
    "content": "import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'\nimport { LXM_FILE_EXT_RXP } from '@/config/constant'\nimport { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { handleExportList, handleImportList } from './actions'\n\nexport interface SelectInfo {\n  // listInfo: LX.List.MyListInfo\n  // selectedList: LX.Music.MusicInfo[]\n  // index: number\n  // listId: string\n  // single: boolean\n  action: 'import' | 'export'\n}\nconst initSelectInfo = {}\n\n// export interface ListImportExportProps {\n//   // onRename: (listInfo: LX.List.UserListInfo) => void\n//   // onImport: (index: number) => void\n//   // onExport: (listInfo: LX.List.MyListInfo) => void\n//   // onSync: (listInfo: LX.List.UserListInfo) => void\n//   // onRemove: (listInfo: LX.List.MyListInfo) => void\n// }\nexport interface ListImportExportType {\n  import: () => void\n  export: () => void\n}\n\nexport default forwardRef<ListImportExportType, {}>((props, ref) => {\n  const [visible, setVisible] = useState(false)\n  const choosePathRef = useRef<ChoosePathType>(null)\n  const selectInfoRef = useRef<SelectInfo>((initSelectInfo as SelectInfo))\n  console.log('render import export')\n\n  useImperativeHandle(ref, () => ({\n    import() {\n      selectInfoRef.current.action = 'import'\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('list_import_part_desc'),\n          dirOnly: false,\n          filter: LXM_FILE_EXT_RXP,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('list_import_part_desc'),\n            dirOnly: false,\n            filter: LXM_FILE_EXT_RXP,\n          })\n        })\n      }\n    },\n    export() {\n      selectInfoRef.current.action = 'export'\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('list_export_part_desc'),\n          dirOnly: true,\n          filter: LXM_FILE_EXT_RXP,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('list_export_part_desc'),\n            dirOnly: true,\n            filter: LXM_FILE_EXT_RXP,\n          })\n        })\n      }\n    },\n  }))\n\n\n  const onConfirmPath = (path: string) => {\n    switch (selectInfoRef.current.action) {\n      case 'import':\n        handleImportList(path)\n        break\n      case 'export':\n        handleExportList(path)\n        break\n    }\n  }\n\n  return (\n    visible\n      ? <ChoosePath ref={choosePathRef} onConfirm={onConfirmPath} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Backup/Part.tsx",
    "content": "import { memo, useRef } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\n// import { gzip, ungzip } from 'pako'\n\nimport SubTitle from '../../components/SubTitle'\nimport Button from '../../components/Button'\nimport { useI18n } from '@/lang'\nimport ListImportExport, { type ListImportExportType } from './ListImportExport'\n\n\nexport default memo(() => {\n  const t = useI18n()\n  const listImportExportRef = useRef<ListImportExportType>(null)\n\n  return (\n    <>\n      <SubTitle title={t('setting_backup_part')}>\n        <View style={styles.list}>\n          <Button onPress={() => listImportExportRef.current?.import()}>{t('setting_backup_part_import_list')}</Button>\n          <Button onPress={() => listImportExportRef.current?.export()}>{t('setting_backup_part_export_list')}</Button>\n          {/* <Button onPress={() => importAndExportData('import', 'setting')}>{t('setting_backup_part_import_setting')}</Button>\n          <Button onPress={() => importAndExportData('export', 'setting')}>{t('setting_backup_part_export_setting')}</Button> */}\n        </View>\n      </SubTitle>\n      {/* <SubTitle title={t('setting_backup_all')}>\n        <View style={styles.list}>\n          <Button onPress={() => importAndExportData('import', 'all')}>{t('setting_backup_all_import')}</Button>\n          <Button onPress={() => importAndExportData('export', 'all')}>{t('setting_backup_all_export')}</Button>\n        </View>\n      </SubTitle> */}\n      <ListImportExport ref={listImportExportRef} />\n    </>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Backup/actions.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\nimport { createList, getListMusics, overwriteList, overwriteListFull, overwriteListMusics } from '@/core/list'\nimport { filterMusicList, fixNewMusicInfoQuality, toNewMusicInfo } from '@/utils'\nimport { log } from '@/utils/log'\nimport { confirmDialog, handleReadFile, handleSaveFile, showImportTip, toast } from '@/utils/tools'\nimport listState from '@/store/list/state'\n\n\nconst getAllLists = async() => {\n  const lists = []\n  lists.push(await getListMusics(listState.defaultList.id).then(musics => ({ ...listState.defaultList, list: musics })))\n  lists.push(await getListMusics(listState.loveList.id).then(musics => ({ ...listState.loveList, list: musics })))\n\n  for await (const list of listState.userList) {\n    lists.push(await getListMusics(list.id).then(musics => ({ ...list, list: musics })))\n  }\n\n  return lists\n}\nconst importOldListData = async(lists: any[]) => {\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 as any[]).map(m => toNewMusicInfo(m)))\n      } else {\n        const listInfo = {\n          name: list.name,\n          id: list.id,\n          list: filterMusicList((list.list as any[]).map(m => toNewMusicInfo(m))),\n          source: list.source,\n          sourceListId: list.sourceListId,\n          locationUpdateTime: list.locationUpdateTime ?? null,\n        }\n        allLists.push(listInfo as LX.List.UserListInfoFull)\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 as LX.List.UserListInfoFull[] })\n}\nconst importNewListData = async(lists: Array<LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull | LX.List.UserListInfoFull>) => {\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        const data = {\n          name: list.name,\n          id: list.id,\n          list: filterMusicList(list.list).map(m => fixNewMusicInfoQuality(m)),\n          source: (list as LX.List.UserListInfoFull).source,\n          sourceListId: (list as LX.List.UserListInfoFull).sourceListId,\n          locationUpdateTime: (list as LX.List.UserListInfoFull).locationUpdateTime ?? null,\n        }\n        allLists.push(data as LX.List.UserListInfoFull)\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 as LX.List.UserListInfoFull[] })\n}\n\n/**\n * 导入单个列表\n * @param listData\n * @param position\n * @returns\n */\nexport const handleImportListPart = async(listData: LX.ConfigFile.MyListInfoPart['data'], position: number = listState.userList.length) => {\n  const targetList = listState.allList.find(l => l.id === listData.id)\n  if (targetList) {\n    const confirm = await confirmDialog({\n      message: global.i18n.t('list_import_part_confirm', { importName: listData.name, localName: targetList.name }),\n      cancelButtonText: global.i18n.t('list_import_part_button_cancel'),\n      confirmButtonText: global.i18n.t('list_import_part_button_confirm'),\n      bgClose: false,\n    })\n    if (confirm) {\n      listData.name = targetList.name\n      void overwriteList(listData).then(() => {\n        toast(global.i18n.t('setting_backup_part_import_list_tip_success'))\n      }).catch((err) => {\n        log.error(err)\n        toast(global.i18n.t('setting_backup_part_import_list_tip_error'))\n      })\n      return\n    }\n    listData.id += `__${Date.now()}`\n  }\n  const userList = listData as LX.List.UserListInfoFull\n  void createList({\n    name: userList.name,\n    id: userList.id,\n    list: userList.list,\n    source: userList.source,\n    sourceListId: userList.sourceListId,\n    position: Math.max(position, -1),\n  }).then(() => {\n    toast(global.i18n.t('setting_backup_part_import_list_tip_success'))\n  }).catch((err) => {\n    log.error(err)\n    toast(global.i18n.t('setting_backup_part_import_list_tip_error'))\n  })\n}\n\nconst showConfirm = async() => {\n  return confirmDialog({\n    message: global.i18n.t('list_import_part_confirm_tip'),\n    cancelButtonText: global.i18n.t('dialog_cancel'),\n    confirmButtonText: global.i18n.t('confirm_button_text'),\n    bgClose: false,\n  })\n}\nconst importPlayList = async(path: string) => {\n  let configData: any\n  try {\n    configData = await handleReadFile(path)\n  } catch (error: any) {\n    log.error(error.stack)\n    throw error\n  }\n\n  switch (configData.type) {\n    case 'defautlList': // 兼容0.6.2及以前版本的列表数据\n      if (!await showConfirm()) return true\n      await overwriteListMusics(LIST_IDS.DEFAULT, filterMusicList((configData.data as LX.List.MyDefaultListInfoFull).list.map(m => toNewMusicInfo(m))))\n      break\n    case 'playList':\n      if (!await showConfirm()) return true\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      await importOldListData(configData.data)\n      break\n    case 'playList_v2':\n      if (!await showConfirm()) return true\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      await importNewListData(configData.data)\n      break\n    case 'allData':\n      if (!await showConfirm()) return true\n      // 兼容0.6.2及以前版本的列表数据\n      if (configData.defaultList) await overwriteListMusics(LIST_IDS.DEFAULT, filterMusicList((configData.defaultList as LX.List.MyDefaultListInfoFull).list.map(m => toNewMusicInfo(m))))\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      else await importOldListData(configData.playList)\n      break\n    case 'allData_v2':\n      if (!await showConfirm()) return true\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      await importNewListData(configData.playList)\n      break\n    case 'playListPart':\n      configData.data.list = filterMusicList((configData.data as LX.ConfigFile.MyListInfoPart['data']).list.map(m => toNewMusicInfo(m)))\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      void handleImportListPart(configData.data)\n      return true\n    case 'playListPart_v2':\n      configData.data.list = filterMusicList((configData.data as LX.ConfigFile.MyListInfoPart['data']).list).map(m => fixNewMusicInfoQuality(m))\n      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n      void handleImportListPart(configData.data)\n      return true\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n    default: showImportTip(configData.type)\n  }\n}\n\nexport const handleImportList = (path: string) => {\n  console.log(path)\n  toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))\n  void importPlayList(path).then((skipTip) => {\n    if (skipTip) return\n    toast(global.i18n.t('setting_backup_part_import_list_tip_success'))\n  }).catch((err) => {\n    log.error(err)\n    toast(global.i18n.t('setting_backup_part_import_list_tip_error'))\n  })\n}\n\n\nconst exportAllList = async(path: string) => {\n  const data = JSON.parse(JSON.stringify({\n    type: 'playList_v2',\n    data: await getAllLists(),\n  }))\n\n  try {\n    await handleSaveFile(path + '/lx_list.lxmc', data)\n  } catch (error: any) {\n    log.error(error.stack)\n  }\n}\nexport const handleExportList = (path: string) => {\n  toast(global.i18n.t('setting_backup_part_export_list_tip_zip'))\n  void exportAllList(path).then(() => {\n    toast(global.i18n.t('setting_backup_part_export_list_tip_success'))\n  }).catch((err: any) => {\n    log.error(err.message)\n    toast(global.i18n.t('setting_backup_part_export_list_tip_failed') + ': ' + (err.message as string))\n  })\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Backup/index.tsx",
    "content": "import { useI18n } from '@/lang'\nimport { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport Part from './Part'\n// import MaxCache from './MaxCache'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_backup')}>\n      <Part />\n      {/* <MaxCache /> */}\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/DrawerLayoutPosition.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\n\nconst LIST = [\n  {\n    position: 'left',\n    name: 'setting_basic_drawer_layout_position_left',\n  },\n  {\n    position: 'right',\n    name: 'setting_basic_drawer_layout_position_right',\n  },\n] as const\n\nconst useActive = (id: LX.AppSetting['common.drawerLayoutPosition']) => {\n  const drawerLayoutPosition = useSettingValue('common.drawerLayoutPosition')\n  const isActive = useMemo(() => drawerLayoutPosition == id, [drawerLayoutPosition, id])\n  return isActive\n}\n\nconst Item = ({ position, label }: {\n  position: LX.AppSetting['common.drawerLayoutPosition']\n  label: string\n}) => {\n  const isActive = useActive(position)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginRight={8} check={isActive} label={label} onChange={() => { updateSetting({ 'common.drawerLayoutPosition': position }) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n\n  const list = useMemo(() => {\n    return LIST.map((item) => ({ position: item.position, name: t(item.name) }))\n  }, [t])\n\n  return (\n    <SubTitle title={t('setting_basic_drawer_layout_position')}>\n      <View style={styles.list}>\n        {\n          list.map(({ position, name }) => <Item key={position} position={position} label={name} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/FontSize.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useI18n } from '@/lang'\nimport { setFontSize } from '@/core/common'\nimport { useFontSize } from '@/store/common/hook'\nimport Text from '@/components/common/Text'\nimport { getTextSize } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\n\nconst LIST = [\n  {\n    size: 0.8,\n    name: 'setting_basic_font_size_80',\n  },\n  {\n    size: 0.9,\n    name: 'setting_basic_font_size_90',\n  },\n  {\n    size: 1,\n    name: 'setting_basic_font_size_100',\n  },\n  {\n    size: 1.1,\n    name: 'setting_basic_font_size_110',\n  },\n  {\n    size: 1.2,\n    name: 'setting_basic_font_size_120',\n  },\n  {\n    size: 1.3,\n    name: 'setting_basic_font_size_130',\n  },\n] as const\n\ntype SIZE_TYPE = typeof LIST[number]['size']\n\nconst useActive = (size: SIZE_TYPE) => {\n  const _size = useFontSize()\n  const isActive = useMemo(() => _size == size, [_size, size])\n  return isActive\n}\n\nconst SizeText = () => {\n  const size = getTextSize(14) * useFontSize()\n  const t = useI18n()\n  const theme = useTheme()\n\n  return <Text style={{ fontSize: size }} color={theme['c-primary']}>{t('setting_basic_font_size_preview')}</Text>\n}\n\nconst Item = ({ size, label }: {\n  size: SIZE_TYPE\n  label: string\n}) => {\n  const isActive = useActive(size)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginRight={8} check={isActive} label={label} onChange={() => { setFontSize(size) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n\n  const list = useMemo(() => {\n    return LIST.map((item) => ({ size: item.size, name: t(item.name) }))\n  }, [t])\n\n  return (\n    <SubTitle title={t('setting_basic_font_size')}>\n      <View style={styles.preview}>\n        <SizeText />\n      </View>\n      <View style={styles.list}>\n        {\n          list.map(({ size, name }) => <Item key={size} size={size} label={name} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  preview: {\n    justifyContent: 'center',\n    // paddingTop: 3,\n    paddingBottom: 10,\n    height: 45,\n  },\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsAllowProgressBarSeek.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const allowProgressBarSeek = useSettingValue('common.allowProgressBarSeek')\n  const setAllowProgressBarSeek = (allowProgressBarSeek: boolean) => {\n    updateSetting({ 'common.allowProgressBarSeek': allowProgressBarSeek })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={allowProgressBarSeek} label={t('setting_basic_allow_progress_bar_seek')} onChange={setAllowProgressBarSeek} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsAlwaysKeepStatusbarHeight.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const val = useSettingValue('common.alwaysKeepStatusbarHeight')\n  const update = (alwaysKeepStatusbarHeight: boolean) => {\n    updateSetting({ 'common.alwaysKeepStatusbarHeight': alwaysKeepStatusbarHeight })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem\n        check={val}\n        label={t('setting_basic_always_keep_statusbar_height')}\n        helpDesc={t('setting_basic_always_keep_statusbar_height_tip')}\n        onChange={update} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsAutoHidePlayBar.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const autoHidePlayBar = useSettingValue('common.autoHidePlayBar')\n  const setAutoHidePlayBar = (autoHidePlayBar: boolean) => {\n    updateSetting({ 'common.autoHidePlayBar': autoHidePlayBar })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={autoHidePlayBar} label={t('setting_basic_auto_hide_play_bar')} onChange={setAutoHidePlayBar} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsHomePageScroll.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const homePageScroll = useSettingValue('common.homePageScroll')\n  const setHomePageScroll = (homePageScroll: boolean) => {\n    updateSetting({ 'common.homePageScroll': homePageScroll })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={homePageScroll} label={t('setting_basic_home_page_scroll')} onChange={setHomePageScroll} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsShowBackBtn.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const showBackBtn = useSettingValue('common.showBackBtn')\n  const setShowBackBtn = (showBackBtn: boolean) => {\n    updateSetting({ 'common.showBackBtn': showBackBtn })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={showBackBtn} label={t('setting_basic_show_back_btn')} onChange={setShowBackBtn} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsShowExitBtn.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const showExitBtn = useSettingValue('common.showExitBtn')\n  const setShowExitBtn = (showExitBtn: boolean) => {\n    updateSetting({ 'common.showExitBtn': showExitBtn })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={showExitBtn} label={t('setting_basic_show_exit_btn')} onChange={setShowExitBtn} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsStartupAutoPlay.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const startupAutoPlay = useSettingValue('player.startupAutoPlay')\n  const setStartupAutoPlay = (startupAutoPlay: boolean) => {\n    updateSetting({ 'player.startupAutoPlay': startupAutoPlay })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={startupAutoPlay} label={t('setting_basic_startup_auto_play')} onChange={setStartupAutoPlay} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsStartupPushPlayDetailScreen.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const startupPushPlayDetailScreen = useSettingValue('player.startupPushPlayDetailScreen')\n  const setStartupPushPlayDetailScreen = (startupPushPlayDetailScreen: boolean) => {\n    updateSetting({ 'player.startupPushPlayDetailScreen': startupPushPlayDetailScreen })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={startupPushPlayDetailScreen} label={t('setting_basic_startup_push_play_detail_screen')} onChange={setStartupPushPlayDetailScreen} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/IsUseSystemFileSelector.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const val = useSettingValue('common.useSystemFileSelector')\n  const update = (useSystemFileSelector: boolean) => {\n    updateSetting({ 'common.useSystemFileSelector': useSystemFileSelector })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem\n      check={val}\n      label={t('setting_basic_use_system_file_selector')}\n      helpDesc={t('setting_basic_use_system_file_selector_tip')}\n      onChange={update} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/Language.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport type { I18n } from '@/lang'\nimport { useI18n, langList } from '@/lang'\nimport { setLanguage } from '@/core/common'\nimport { useSettingValue } from '@/store/setting/hook'\n\nconst useActive = (id: I18n['locale']) => {\n  const activeLangId = useSettingValue('common.langId')\n  const isActive = useMemo(() => activeLangId == id, [activeLangId, id])\n  return isActive\n}\n\nconst Item = ({ id, name }: {\n  id: I18n['locale']\n  name: string\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginRight={8} check={isActive} label={name} onChange={() => { setLanguage(id) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <SubTitle title={t('setting_basic_lang')}>\n      <View style={styles.list}>\n        {\n          langList.map(({ locale, name }) => <Item name={name} id={locale} key={locale} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/ShareType.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\n\ntype ShareType = LX.AppSetting['common.shareType']\n\nconst setShareType = (type: ShareType) => {\n  updateSetting({ 'common.shareType': type })\n}\n\n\nconst useActive = (type: ShareType) => {\n  const shareType = useSettingValue('common.shareType')\n  const isActive = useMemo(() => shareType == type, [shareType, type])\n  return isActive\n}\n\nconst Item = ({ id, name }: {\n  id: ShareType\n  name: string\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginBottom={3} check={isActive} label={name} onChange={() => { setShareType(id) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const list = useMemo(() => {\n    return [\n      {\n        id: 'system',\n        name: t('setting_basic_share_type_system'),\n      },\n      {\n        id: 'clipboard',\n        name: t('setting_basic_share_type_clipboard'),\n      },\n    ] as const\n  }, [t])\n\n  return (\n    <SubTitle title={t('setting_basic_share_type')}>\n      <View style={styles.list}>\n        {\n          list.map(({ id, name }) => <Item name={name} id={id} key={id} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/Source.tsx",
    "content": "import { memo, useCallback, useMemo, useRef } from 'react'\n\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { createStyle } from '@/utils/tools'\nimport { setApiSource } from '@/core/apiSource'\nimport { useI18n } from '@/lang'\nimport apiSourceInfo from '@/utils/musicSdk/api-source-info'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useStatus, useUserApiList } from '@/store/userApi'\nimport Button from '../../components/Button'\nimport UserApiEditModal, { type UserApiEditModalType } from './UserApiEditModal'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\n// import { importUserApi, removeUserApi } from '@/core/userApi'\n\nconst apiSourceList = apiSourceInfo.map(api => ({\n  id: api.id,\n  name: api.name,\n  disabled: api.disabled,\n}))\n\nconst useActive = (id: string) => {\n  const activeLangId = useSettingValue('common.apiSource')\n  const isActive = useMemo(() => activeLangId == id, [activeLangId, id])\n  return isActive\n}\n\nconst Item = ({ id, name, desc, statusLabel, change }: {\n  id: string\n  name: string\n  desc?: string\n  statusLabel?: string\n  change: (id: string) => void\n}) => {\n  const isActive = useActive(id)\n  const theme = useTheme()\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return (\n    <CheckBox marginBottom={5} check={isActive} onChange={() => { change(id) }} need>\n      <Text style={styles.sourceLabel}>\n        {name}\n        {\n          desc ? <Text style={styles.sourceDesc} color={theme['c-500']} size={13}>  {desc}</Text> : null\n        }\n        {\n          statusLabel ? <Text style={styles.sourceStatus} size={13}>  {statusLabel}</Text> : null\n        }\n      </Text>\n    </CheckBox>\n  )\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const list = useMemo(() => apiSourceList.map(s => ({\n    // @ts-expect-error\n    name: t(`setting_basic_source_${s.id}`) || s.name,\n    id: s.id,\n  })), [t])\n  const setApiSourceId = useCallback((id: string) => {\n    setApiSource(id)\n  }, [])\n  const userApiListRaw = useUserApiList()\n  const apiStatus = useStatus()\n  const apiSourceSetting = useSettingValue('common.apiSource')\n  const userApiList = useMemo(() => {\n    const getApiStatus = () => {\n      let status\n      if (apiStatus.status) status = t('setting_basic_source_status_success')\n      else if (apiStatus.message == 'initing') status = t('setting_basic_source_status_initing')\n      else status = t('setting_basic_source_status_failed')\n\n      return status\n    }\n    return userApiListRaw.map(api => {\n      const statusLabel = api.id == apiSourceSetting ? `[${getApiStatus()}]` : ''\n      return {\n        id: api.id,\n        name: api.name,\n        label: `${api.name}${statusLabel}`,\n        desc: [/^\\d/.test(api.version) ? `v${api.version}` : api.version].filter(Boolean).join(', '),\n        statusLabel,\n        // status: apiStatus.status,\n        // message: apiStatus.message,\n        // disabled: false,\n      }\n    })\n  }, [userApiListRaw, apiStatus, apiSourceSetting, t])\n\n  const modalRef = useRef<UserApiEditModalType>(null)\n  const handleShow = () => {\n    modalRef.current?.show()\n  }\n\n  return (\n    <SubTitle title={t('setting_basic_source')}>\n      <View style={styles.list}>\n        {\n          list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setApiSourceId} />)\n        }\n        {\n          userApiList.map(({ id, name, desc, statusLabel }) => <Item name={name} desc={desc} statusLabel={statusLabel} id={id} key={id} change={setApiSourceId} />)\n        }\n      </View>\n      <View style={styles.btn}>\n        <Button onPress={handleShow}>{t('setting_basic_source_user_api_btn')}</Button>\n      </View>\n      <UserApiEditModal ref={modalRef} />\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  list: {\n    flexGrow: 0,\n    flexShrink: 1,\n    // flexDirection: 'row',\n    // flexWrap: 'wrap',\n  },\n  btn: {\n    marginTop: 10,\n    flexDirection: 'row',\n  },\n  sourceLabel: {\n\n  },\n  sourceDesc: {\n\n  },\n  sourceStatus: {\n\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/SourceName.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\n\ntype SourceNameType = LX.AppSetting['common.sourceNameType']\n\nconst setSourceNameType = (type: SourceNameType) => {\n  updateSetting({ 'common.sourceNameType': type })\n}\n\n\nconst useActive = (type: SourceNameType) => {\n  const sourceNameType = useSettingValue('common.sourceNameType')\n  const isActive = useMemo(() => sourceNameType == type, [sourceNameType, type])\n  return isActive\n}\n\nconst Item = ({ id, name }: {\n  id: SourceNameType\n  name: string\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginBottom={3} check={isActive} label={name} onChange={() => { setSourceNameType(id) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const list = useMemo(() => {\n    return [\n      {\n        id: 'real',\n        name: t('setting_basic_sourcename_real'),\n      },\n      {\n        id: 'alias',\n        name: t('setting_basic_sourcename_alias'),\n      },\n    ] as const\n  }, [t])\n\n  return (\n    <SubTitle title={t('setting_basic_sourcename')}>\n      <View style={styles.list}>\n        {\n          list.map(({ id, name }) => <Item name={name} id={id} key={id} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/ImportBtn.tsx",
    "content": "import { useMemo, useRef } from 'react'\n\nimport DorpDownMenu, { type DorpDownMenuProps as _DorpDownMenuProps } from '@/components/common/DorpDownMenu'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport ScriptImportExport, { type ScriptImportExportType } from './ScriptImportExport'\nimport ScriptImportOnline, { type ScriptImportOnlineType } from './ScriptImportOnline'\nimport { state } from '@/store/userApi'\nimport { tipDialog } from '@/utils/tools'\n\nimport { useTheme } from '@/store/theme/hook'\n\ninterface BtnProps {\n  btnStyle?: _DorpDownMenuProps<any[]>['btnStyle']\n}\n\n\nexport default ({ btnStyle }: BtnProps) => {\n  const t = useI18n()\n  const theme = useTheme()\n  const scriptImportExportRef = useRef<ScriptImportExportType>(null)\n  const scriptImportOnlineRef = useRef<ScriptImportOnlineType>(null)\n\n  const importTypes = useMemo(() => {\n    return [\n      { action: 'local', label: t('user_api_btn_import_local') },\n      { action: 'online', label: t('user_api_btn_import_online') },\n    ] as const\n  }, [t])\n\n  type DorpDownMenuProps = _DorpDownMenuProps<typeof importTypes>\n\n  const handleAction: DorpDownMenuProps['onPress'] = ({ action }) => {\n    if (state.list.length > 20) {\n      void tipDialog({\n        message: t('user_api_max_tip'),\n        btnText: t('ok'),\n      })\n      return\n    }\n\n    if (action == 'local') {\n      scriptImportExportRef.current?.import()\n    } else {\n      scriptImportOnlineRef.current?.show()\n    }\n  }\n\n\n  return (\n    <DorpDownMenu\n      btnStyle={btnStyle}\n      menus={importTypes}\n      center\n      onPress={handleAction}\n    >\n      <Text size={14} color={theme['c-button-font']}>{t('user_api_btn_import')}</Text>\n      <ScriptImportExport ref={scriptImportExportRef} />\n      <ScriptImportOnline ref={scriptImportOnlineRef} />\n    </DorpDownMenu>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/List.tsx",
    "content": "import { useCallback } from 'react'\nimport Text from '@/components/common/Text'\nimport { View, TouchableOpacity, ScrollView } from 'react-native'\nimport { confirmDialog, createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { useUserApiList, state as userApiState } from '@/store/userApi'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { removeUserApi, setUserApiAllowShowUpdateAlert } from '@/core/userApi'\nimport { BorderRadius } from '@/theme'\nimport CheckBox from '@/components/common/CheckBox'\nimport { Icon } from '@/components/common/Icon'\nimport settingState from '@/store/setting/state'\nimport apiSourceInfo from '@/utils/musicSdk/api-source-info'\nimport { setApiSource } from '@/core/apiSource'\n\nconst formatVersionName = (version: string) => {\n  return /^\\d/.test(version) ? `v${version}` : version\n}\nconst ListItem = ({ item, activeId, onRemove, onChangeAllowShowUpdateAlert }: {\n  item: LX.UserApi.UserApiInfo\n  activeId: string\n  onRemove: (id: string, name: string) => void\n  onChangeAllowShowUpdateAlert: (id: string, enabled: boolean) => void\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const changeAllowShowUpdateAlert = (check: boolean) => {\n    onChangeAllowShowUpdateAlert(item.id, check)\n  }\n  const handleRemove = () => {\n    onRemove(item.id, item.name)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, backgroundColor: activeId == item.id ? theme['c-primary-background-active'] : 'transparent' }}>\n      <View style={styles.listItemLeft}>\n        <Text size={14}>\n          {item.name}\n          {\n            item.version ? (\n              <Text size={12} color={theme['c-font-label']}>{ '   ' + formatVersionName(item.version) }</Text>\n            ) : null\n          }\n          {\n            item.author ? (\n              <Text size={12} color={theme['c-font-label']}>{ '   ' + item.author }</Text>\n            ) : null\n          }\n        </Text>\n        {\n          item.description ? (\n            <Text size={12} color={theme['c-font-label']}>{item.description}</Text>\n          ) : null\n        }\n        <CheckBox check={item.allowShowUpdateAlert} label={t('user_api_allow_show_update_alert')} onChange={changeAllowShowUpdateAlert} size={0.86} />\n      </View>\n      <View style={styles.listItemRight}>\n        <TouchableOpacity style={styles.btn} onPress={handleRemove}>\n          <Icon name=\"close\" color={theme['c-button-font']} />\n        </TouchableOpacity>\n      </View>\n    </View>\n  )\n}\n\nexport interface UserApiEditModalProps {\n  onSave: (rules: string) => void\n  // onSourceChange: SourceSelectorProps['onSourceChange']\n}\nexport interface UserApiEditModalType {\n  show: (rules: string) => void\n}\n\n\nexport default () => {\n  const userApiList = useUserApiList()\n  const apiSource = useSettingValue('common.apiSource')\n  const theme = useTheme()\n  const t = useI18n()\n\n  const handleRemove = useCallback(async(id: string, name: string) => {\n    const confirm = await confirmDialog({\n      message: global.i18n.t('user_api_remove_tip', { name }),\n      cancelButtonText: global.i18n.t('cancel_button_text_2'),\n      confirmButtonText: global.i18n.t('confirm_button_text'),\n      bgClose: false,\n    })\n    if (!confirm) return\n    void removeUserApi([id]).finally(() => {\n      if (settingState.setting['common.apiSource'] == id) {\n        let backApiId = apiSourceInfo.find(api => !api.disabled)?.id\n        if (!backApiId) backApiId = userApiState.list[0]?.id\n        setApiSource(backApiId ?? '')\n      }\n    })\n  }, [])\n  const handleChangeAllowShowUpdateAlert = useCallback((id: string, enabled: boolean) => {\n    void setUserApiAllowShowUpdateAlert(id, enabled)\n  }, [])\n\n  return (\n    <ScrollView style={styles.scrollView} keyboardShouldPersistTaps={'always'}>\n      <View onStartShouldSetResponder={() => true}>\n        {\n          userApiList.length\n            ? userApiList.map((item) => {\n              return (\n              <ListItem\n                key={item.id}\n                item={item}\n                activeId={apiSource}\n                onRemove={handleRemove}\n                onChangeAllowShowUpdateAlert={handleChangeAllowShowUpdateAlert}\n              />\n              )\n            })\n            : <Text style={styles.tipText} color={theme['c-font-label']}>{t('user_api_empty')}</Text>\n        }\n      </View>\n    </ScrollView>\n  )\n}\n\n\nconst styles = createStyle({\n  scrollView: {\n    paddingHorizontal: 7,\n    flexGrow: 0,\n  },\n  list: {\n    paddingBottom: 15,\n    flexDirection: 'column',\n  },\n  listItem: {\n    padding: 10,\n    borderRadius: BorderRadius.normal,\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-between',\n  },\n  listItemLeft: {\n    paddingRight: 10,\n    flex: 1,\n    gap: 2,\n  },\n  listItemRight: {\n    flex: 0,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  // btns: {\n  //   padding: 5,\n  // },\n  btn: {\n    padding: 10,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  tipText: {\n    textAlign: 'center',\n    marginTop: 25,\n    marginBottom: 15,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/ScriptImportExport.tsx",
    "content": "import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'\nimport { USER_API_SOURCE_FILE_EXT_RXP } from '@/config/constant'\nimport { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { handleImportLocalFile } from './action'\n\nexport interface SelectInfo {\n  // listInfo: LX.List.MyListInfo\n  // selectedList: LX.Music.MusicInfo[]\n  // index: number\n  // listId: string\n  // single: boolean\n  action: 'import'\n}\nconst initSelectInfo = {}\n\n// export interface ScriptImportExportProps {\n//   // onRename: (listInfo: LX.List.UserListInfo) => void\n//   // onImport: (index: number) => void\n//   // onExport: (listInfo: LX.List.MyListInfo) => void\n//   // onSync: (listInfo: LX.List.UserListInfo) => void\n//   // onRemove: (listInfo: LX.List.MyListInfo) => void\n// }\nexport interface ScriptImportExportType {\n  import: () => void\n  // export: (listInfo: LX.List.MyListInfo, index: number) => void\n}\n\nexport default forwardRef<ScriptImportExportType, {}>((props, ref) => {\n  const [visible, setVisible] = useState(false)\n  const choosePathRef = useRef<ChoosePathType>(null)\n  const selectInfoRef = useRef<SelectInfo>((initSelectInfo as SelectInfo))\n  // console.log('render import export')\n\n  useImperativeHandle(ref, () => ({\n    import() {\n      selectInfoRef.current = {\n        action: 'import',\n      }\n      if (visible) {\n        choosePathRef.current?.show({\n          title: global.i18n.t('user_api_import_desc'),\n          dirOnly: false,\n          filter: USER_API_SOURCE_FILE_EXT_RXP,\n        })\n      } else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          choosePathRef.current?.show({\n            title: global.i18n.t('user_api_import_desc'),\n            dirOnly: false,\n            filter: USER_API_SOURCE_FILE_EXT_RXP,\n          })\n        })\n      }\n    },\n    // export(listInfo, index) {\n    //   selectInfoRef.current = {\n    //     action: 'export',\n    //     listInfo,\n    //     index,\n    //   }\n    //   if (visible) {\n    //     choosePathRef.current?.show({\n    //       title: global.i18n.t('list_export_part_desc'),\n    //       dirOnly: true,\n    //       filter: LXM_FILE_EXT_RXP,\n    //     })\n    //   } else {\n    //     setVisible(true)\n    //     requestAnimationFrame(() => {\n    //       choosePathRef.current?.show({\n    //         title: global.i18n.t('list_export_part_desc'),\n    //         dirOnly: true,\n    //         filter: LXM_FILE_EXT_RXP,\n    //       })\n    //     })\n    //   }\n    // },\n  }))\n\n\n  const onConfirmPath = (path: string) => {\n    switch (selectInfoRef.current.action) {\n      case 'import':\n        handleImportLocalFile(path)\n        break\n      // case 'export':\n      //   handleExport(selectInfoRef.current.listInfo, path)\n      //   break\n    }\n  }\n\n  return (\n    visible\n      ? <ChoosePath ref={choosePathRef} onConfirm={onConfirmPath} />\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/ScriptImportOnline.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { httpFetch } from '@/utils/request'\nimport { handleImportScript } from './action'\n\ninterface UrlInputType {\n  setText: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst UrlInput = forwardRef<UrlInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const [text, setText] = useState('')\n  const [placeholder, setPlaceholder] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setText(text) {\n      setText(text)\n      setPlaceholder(global.i18n.t('user_api_btn_import_online_input_tip'))\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={placeholder}\n      value={text}\n      onChangeText={setText}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\n\nexport interface ScriptImportOnlineType {\n  show: () => void\n}\n\n\nexport default forwardRef<ScriptImportOnlineType, {}>((props, ref) => {\n  const t = useI18n()\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const urlInputRef = useRef<UrlInputType>(null)\n  const [visible, setVisible] = useState(false)\n  const [btn, setBtn] = useState({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })\n\n  const handleShow = () => {\n    alertRef.current?.setVisible(true)\n    setBtn({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })\n    requestAnimationFrame(() => {\n      urlInputRef.current?.setText('')\n      setTimeout(() => {\n        urlInputRef.current?.focus()\n      }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show() {\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleImport = async() => {\n    let url = urlInputRef.current?.getText() ?? ''\n    if (!/^https?:\\/\\//.test(url)) {\n      url = ''\n      urlInputRef.current?.setText('')\n    }\n    if (!url.length) return\n    setBtn({ disabled: true, text: t('user_api_btn_import_online_input_loading') })\n    let script: string\n    try {\n      script = await httpFetch(url).promise.then(resp => resp.body) as string\n    } catch (err: any) {\n      toast(t('user_api_import_failed_tip', { message: err.message }), 'long')\n      return\n    } finally {\n      setBtn({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })\n    }\n    if (script.length > 9_000_000) {\n      toast(t('user_api_import_failed_tip', { message: 'Too large script' }), 'long')\n      return\n    }\n    void handleImportScript(script)\n\n    alertRef.current?.setVisible(false)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleImport}\n          disabledConfirm={btn.disabled}\n          confirmText={btn.text}\n        >\n          <View style={styles.reurlContent}>\n            <Text style={{ marginBottom: 5 }}>{ t('user_api_btn_import_online')}</Text>\n            <UrlInput ref={urlInputRef} />\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  reurlContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 290,\n    borderRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/action.ts",
    "content": "import { importUserApi } from '@/core/userApi'\nimport { readFile } from '@/utils/fs'\nimport { log } from '@/utils/log'\nimport { toast } from '@/utils/tools'\n\n\nexport const handleImportScript = async(script: string) => {\n  await importUserApi(script).then(() => {\n    toast(global.i18n.t('user_api_import_success_tip'))\n  }).catch((error: any) => {\n    log.error(error.stack)\n    toast(global.i18n.t('user_api_import_failed_tip', { message: error.message }), 'long')\n  })\n}\n\nexport const handleImportLocalFile = (path: string) => {\n  // toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))\n  void readFile(path).then(async script => {\n    if (script == null) throw new Error('Read file failed')\n    void handleImportScript(script)\n  }).catch((error: any) => {\n    toast(global.i18n.t('user_api_import_failed_tip', { message: error.message }), 'long')\n  })\n}\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/UserApiEditModal/index.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport Text from '@/components/common/Text'\nimport { View, TouchableOpacity } from 'react-native'\nimport { createStyle, openUrl } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport Button from '@/components/common/Button'\nimport List from './List'\nimport ImportBtn from './ImportBtn'\n\n// interface UrlInputType {\n//   setText: (text: string) => void\n//   getText: () => string\n//   focus: () => void\n// }\n// const UrlInput = forwardRef<UrlInputType, {}>((props, ref) => {\n//   const theme = useTheme()\n//   const t = useI18n()\n//   const [text, setText] = useState('')\n//   const inputRef = useRef<InputType>(null)\n//   const [height, setHeight] = useState(100)\n\n//   useImperativeHandle(ref, () => ({\n//     getText() {\n//       return text.trim()\n//     },\n//     setText(text) {\n//       setText(text)\n//     },\n//     focus() {\n//       inputRef.current?.focus()\n//     },\n//   }))\n\n//   const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {\n//     setHeight(nativeEvent.layout.height)\n//   }, [])\n\n//   return (\n//     <View style={styles.inputContent} onLayout={handleLayout}>\n//       <Input\n//         ref={inputRef}\n//         value={text}\n//         onChangeText={setText}\n//         textAlignVertical=\"top\"\n//         placeholder={t('setting_dislike_list_input_tip')}\n//         size={12}\n//         style={{ ...styles.input, height, backgroundColor: theme['c-primary-input-background'] }}\n//       />\n//     </View>\n//   )\n// })\n\n\n// export interface UserApiEditModalProps {\n//   onSave: (rules: string) => void\n//   // onSourceChange: SourceSelectorProps['onSourceChange']\n// }\nexport interface UserApiEditModalType {\n  show: () => void\n}\n\nexport default forwardRef<UserApiEditModalType, {}>((props, ref) => {\n  const dialogRef = useRef<DialogType>(null)\n  // const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  // const inputRef = useRef<UrlInputType>(null)\n  const [visible, setVisible] = useState(false)\n  const theme = useTheme()\n  const t = useI18n()\n\n  const handleShow = () => {\n    dialogRef.current?.setVisible(true)\n    // requestAnimationFrame(() => {\n    // inputRef.current?.setText('')\n    // sourceSelectorRef.current?.setSource(source)\n    // setTimeout(() => {\n    //   inputRef.current?.focus()\n    // }, 300)\n    // })\n  }\n  useImperativeHandle(ref, () => ({\n    show() {\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleCancel = () => {\n    dialogRef.current?.setVisible(false)\n  }\n\n  const openFAQPage = () => {\n    void openUrl('https://lyswhut.github.io/lx-music-doc/mobile/custom-source')\n  }\n\n  return (\n    visible\n      ? (\n          <Dialog ref={dialogRef} bgHide={false}>\n            <View style={styles.content}>\n              {/* <UrlInput ref={inputRef} /> */}\n              <Text size={16} style={styles.title}>{t('user_api_title')}</Text>\n              <List />\n              <View style={styles.tips}>\n                <Text style={styles.tipsText} size={12}>\n                  {t('user_api_readme')}\n                </Text>\n                <TouchableOpacity onPress={openFAQPage}>\n                  <Text style={{ ...styles.tipsText, textDecorationLine: 'underline' }} size={12} color={theme['c-primary-font']}>FAQ</Text>\n                </TouchableOpacity>\n                <View>\n                  <Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>\n                </View>\n              </View>\n            </View>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>\n                <Text size={14} color={theme['c-button-font']}>{t('close')}</Text>\n              </Button>\n              <ImportBtn btnStyle={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} />\n            </View>\n          </Dialog>\n        ) : null\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    // flexGrow: 1,\n    flexShrink: 1,\n    paddingHorizontal: 8,\n    paddingTop: 15,\n    paddingBottom: 10,\n    flexDirection: 'column',\n  },\n  title: {\n    marginBottom: 15,\n    textAlign: 'center',\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  tips: {\n    paddingHorizontal: 7,\n    marginTop: 15,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n  tipsText: {\n    marginTop: 8,\n    textAlignVertical: 'bottom',\n    // lineHeight: 18,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  btns: {\n    flexDirection: 'row',\n    justifyContent: 'center',\n    paddingBottom: 15,\n    paddingLeft: 15,\n    // paddingRight: 15,\n  },\n  btn: {\n    flex: 1,\n    padding: 10,\n    alignItems: 'center',\n    borderRadius: 4,\n    marginRight: 15,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Basic/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Theme from '../Theme'\nimport Section from '../../components/Section'\nimport Source from './Source'\nimport SourceName from './SourceName'\nimport Language from './Language'\nimport FontSize from './FontSize'\nimport ShareType from './ShareType'\nimport IsStartupAutoPlay from './IsStartupAutoPlay'\nimport IsStartupPushPlayDetailScreen from './IsStartupPushPlayDetailScreen'\nimport IsAutoHidePlayBar from './IsAutoHidePlayBar'\nimport IsHomePageScroll from './IsHomePageScroll'\nimport IsAllowProgressBarSeek from './IsAllowProgressBarSeek'\nimport IsUseSystemFileSelector from './IsUseSystemFileSelector'\nimport IsAlwaysKeepStatusbarHeight from './IsAlwaysKeepStatusbarHeight'\nimport IsShowBackBtn from './IsShowBackBtn'\nimport IsShowExitBtn from './IsShowExitBtn'\nimport DrawerLayoutPosition from './DrawerLayoutPosition'\nimport { useI18n } from '@/lang/i18n'\n\nexport default memo(() => {\n  const t = useI18n()\n\n\n  return (\n    <Section title={t('setting_basic')}>\n      <IsStartupAutoPlay />\n      <IsStartupPushPlayDetailScreen />\n      <IsShowBackBtn />\n      <IsShowExitBtn />\n      <IsAutoHidePlayBar />\n      <IsHomePageScroll />\n      <IsAllowProgressBarSeek />\n      <IsUseSystemFileSelector />\n      <IsAlwaysKeepStatusbarHeight />\n      <Theme />\n      <DrawerLayoutPosition />\n      <Language />\n      <FontSize />\n      <ShareType />\n      <Source />\n      <SourceName />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/List/AddMusicLocationType.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\n\nconst setAddMusicLocationType = (type: LX.AddMusicLocationType) => {\n  updateSetting({ 'list.addMusicLocationType': type })\n}\n\nconst useActive = (id: LX.AddMusicLocationType) => {\n  const addMusicLocationType = useSettingValue('list.addMusicLocationType')\n  const isActive = useMemo(() => addMusicLocationType == id, [addMusicLocationType, id])\n  return isActive\n}\n\nconst Item = ({ id, name }: {\n  id: LX.AddMusicLocationType\n  name: string\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginRight={8} check={isActive} label={name} onChange={() => { setAddMusicLocationType(id) }} need />\n}\n\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <SubTitle title={t('setting_list_add_music_location_type')}>\n      <View style={styles.list}>\n        <Item id=\"top\" name={t('setting_list_add_music_location_type_top')} />\n        <Item id=\"bottom\" name={t('setting_list_add_music_location_type_bottom')} />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/List/IsClickPlayList.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isClickPlayList = useSettingValue('list.isClickPlayList')\n  const setClickPlayList = (isClickPlayList: boolean) => {\n    updateSetting({ 'list.isClickPlayList': isClickPlayList })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isClickPlayList} onChange={setClickPlayList} label={t('setting_list_click_action')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/List/IsShowAlbumName.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowAlbumName = useSettingValue('list.isShowAlbumName')\n  const setShowAlbumName = (isShowAlbumName: boolean) => {\n    requestAnimationFrame(() => {\n      updateSetting({ 'list.isShowAlbumName': isShowAlbumName })\n    })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowAlbumName} onChange={setShowAlbumName} label={t('setting_list_show_album_name')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/List/IsShowInterval.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowInterval = useSettingValue('list.isShowInterval')\n  const setShowInterval = (isShowInterval: boolean) => {\n    requestAnimationFrame(() => {\n      updateSetting({ 'list.isShowInterval': isShowInterval })\n    })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowInterval} onChange={setShowInterval} label={t('setting_list_show interval')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    marginBottom: 15,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/List/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport AddMusicLocationType from './AddMusicLocationType'\nimport IsClickPlayList from './IsClickPlayList'\nimport IsShowAlbumName from './IsShowAlbumName'\nimport IsShowInterval from './IsShowInterval'\n\nimport { useI18n } from '@/lang'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_list')}>\n      <IsClickPlayList />\n      <IsShowAlbumName />\n      <IsShowInterval />\n      <AddMusicLocationType />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/IsLockLyric.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { toggleDesktopLyricLock } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isLock = useSettingValue('desktopLyric.isLock')\n  const setLock = (isLock: boolean) => {\n    void toggleDesktopLyricLock(isLock).then(() => {\n      updateSetting({ 'desktopLyric.isLock': isLock })\n    })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isLock} onChange={setLock} label={t('setting_lyric_desktop_lock')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/IsShowLyric.tsx",
    "content": "import { memo, useRef } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nimport { createStyle } from '@/utils/tools'\n\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport DesktopLyricEnable, { type DesktopLyricEnableType } from '@/components/DesktopLyricEnable'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isEnable = useSettingValue('desktopLyric.enable')\n  // const setIsShowDesktopLyric = useDispatch('common', 'setIsShowDesktopLyric')\n  const desktopLyricEnableRef = useRef<DesktopLyricEnableType>(null)\n\n  const handleChangeEnableDesktopLyric = async(isEnable: boolean) => {\n    desktopLyricEnableRef.current?.setEnabled(isEnable)\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isEnable} onChange={(enable) => { void handleChangeEnableDesktopLyric(enable) }} label={t('setting_lyric_desktop_enable')} />\n      <DesktopLyricEnable ref={desktopLyricEnableRef} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/IsShowToggleAnima.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { setShowDesktopLyricToggleAnima } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\nexport default memo(() => {\n  const t = useI18n()\n  const showToggleAnima = useSettingValue('desktopLyric.showToggleAnima')\n  const update = (showToggleAnima: boolean) => {\n    void setShowDesktopLyricToggleAnima(showToggleAnima).then(() => {\n      updateSetting({ 'desktopLyric.showToggleAnima': showToggleAnima })\n    })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={showToggleAnima} onChange={update} label={t('setting_lyric_desktop_toggle_anima')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/IsSingleLine.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { setDesktopLyricSingleLine } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isSingleLine = useSettingValue('desktopLyric.isSingleLine')\n  const update = (isSingleLine: boolean) => {\n    void setDesktopLyricSingleLine(isSingleLine).then(() => {\n      updateSetting({ 'desktopLyric.isSingleLine': isSingleLine })\n    })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isSingleLine} onChange={update} label={t('setting_lyric_desktop_single_line')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/MaxLineNum.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport Slider, { type SliderProps } from '../../components/Slider'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { setDesktopLyricMaxLineNum } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\n\nexport default memo(() => {\n  const t = useI18n()\n  const maxLineNum = useSettingValue('desktopLyric.maxLineNum')\n  const theme = useTheme()\n  const [sliderSize, setSliderSize] = useState(maxLineNum)\n  const [isSliding, setSliding] = useState(false)\n  const handleSlidingStart = useCallback<NonNullable<SliderProps['onSlidingStart']>>(() => {\n    setSliding(true)\n  }, [])\n  const handleValueChange = useCallback<NonNullable<SliderProps['onValueChange']>>(value => {\n    setSliderSize(value)\n  }, [])\n  const handleSlidingComplete = useCallback<NonNullable<SliderProps['onSlidingComplete']>>(value => {\n    if (maxLineNum == value) return\n    void setDesktopLyricMaxLineNum(value).then(() => {\n      updateSetting({ 'desktopLyric.maxLineNum': value })\n    }).finally(() => {\n      setSliding(false)\n    })\n  }, [maxLineNum])\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_maxlineNum')}>\n      <View style={styles.content}>\n        <Text style={{ color: theme['c-primary-font'] }}>{isSliding ? sliderSize : maxLineNum}</Text>\n        <Slider\n          minimumValue={1}\n          maximumValue={8}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={1}\n          value={maxLineNum}\n        />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    alignItems: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/TextOpacity.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport Slider, { type SliderProps } from '../../components/Slider'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { setDesktopLyricAlpha } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\n\nexport default memo(() => {\n  const t = useI18n()\n  const opacity = useSettingValue('desktopLyric.style.opacity')\n  const theme = useTheme()\n  const [sliderSize, setSliderSize] = useState(opacity)\n  const [isSliding, setSliding] = useState(false)\n  const handleSlidingStart = useCallback<NonNullable<SliderProps['onSlidingStart']>>(() => {\n    setSliding(true)\n  }, [])\n  const handleValueChange = useCallback<NonNullable<SliderProps['onValueChange']>>(value => {\n    setSliderSize(value)\n  }, [])\n  const handleSlidingComplete = useCallback<NonNullable<SliderProps['onSlidingComplete']>>(value => {\n    if (opacity == value) return\n    void setDesktopLyricAlpha(value).then(() => {\n      updateSetting({ 'desktopLyric.style.opacity': value })\n    }).finally(() => {\n      setSliding(false)\n    })\n  }, [opacity])\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_text_opacity')}>\n      <View style={styles.content}>\n        <Text style={{ color: theme['c-primary-font'] }}>{isSliding ? sliderSize : opacity}</Text>\n        <Slider\n          minimumValue={10}\n          maximumValue={100}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={2}\n          value={opacity}\n        />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    alignItems: 'center',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/TextPositionX.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { setDesktopLyricTextPosition } from '@/core/desktopLyric'\nimport { createStyle } from '@/utils/tools'\nimport { updateSetting } from '@/core/common'\n\ntype X_TYPE = LX.AppSetting['desktopLyric.textPosition.x']\n\nconst X_LIST = [\n  'left',\n  'center',\n  'right',\n] as const\n\nconst useActive = (id: X_TYPE) => {\n  const x = useSettingValue('desktopLyric.textPosition.x')\n  const isActive = useMemo(() => x == id, [x, id])\n  return isActive\n}\n\nconst Item = ({ id, name, change }: {\n  id: X_TYPE\n  name: string\n  change: (id: X_TYPE) => void\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginBottom={3} check={isActive} label={name} onChange={() => { change(id) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const list = useMemo(() => {\n    return X_LIST.map(id => ({ id, name: t(`setting_lyric_desktop_text_x_${id}`) }))\n  }, [t])\n\n  const setPosition = (id: X_TYPE) => {\n    void setDesktopLyricTextPosition(id, null).then(() => {\n      updateSetting({ 'desktopLyric.textPosition.x': id })\n    })\n  }\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_text_x')}>\n      <View style={styles.list}>\n        {\n          list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setPosition} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  list: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/TextPositionY.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { setDesktopLyricTextPosition } from '@/core/desktopLyric'\nimport { createStyle } from '@/utils/tools'\nimport { updateSetting } from '@/core/common'\n\ntype Y_TYPE = LX.AppSetting['desktopLyric.textPosition.y']\n\nconst Y_LIST = [\n  'top',\n  'center',\n  'bottom',\n] as const\n\nconst useActive = (id: Y_TYPE) => {\n  const y = useSettingValue('desktopLyric.textPosition.y')\n  const isActive = useMemo(() => y == id, [y, id])\n  return isActive\n}\n\nconst Item = ({ id, name, change }: {\n  id: Y_TYPE\n  name: string\n  change: (id: Y_TYPE) => void\n}) => {\n  const isActive = useActive(id)\n  return <CheckBox marginBottom={3} check={isActive} label={name} onChange={() => { change(id) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const list = useMemo(() => {\n    return Y_LIST.map(id => ({ id, name: t(`setting_lyric_desktop_text_y_${id}`) }))\n  }, [t])\n\n  const setPosition = (id: Y_TYPE) => {\n    void setDesktopLyricTextPosition(null, id).then(() => {\n      updateSetting({ 'desktopLyric.textPosition.y': id })\n    })\n  }\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_text_y')}>\n      <View style={styles.list}>\n        {\n          list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setPosition} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  list: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/TextSize.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport Slider, { type SliderProps } from '../../components/Slider'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { setDesktopLyricTextSize } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\n\nexport default memo(() => {\n  const t = useI18n()\n  const fontSize = useSettingValue('desktopLyric.style.fontSize')\n  const theme = useTheme()\n  const [sliderSize, setSliderSize] = useState(fontSize)\n  const [isSliding, setSliding] = useState(false)\n  const handleSlidingStart = useCallback<NonNullable<SliderProps['onSlidingStart']>>(() => {\n    setSliding(true)\n  }, [])\n  const handleValueChange = useCallback<NonNullable<SliderProps['onValueChange']>>(value => {\n    setSliderSize(value)\n  }, [])\n  const handleSlidingComplete = useCallback<NonNullable<SliderProps['onSlidingComplete']>>(value => {\n    if (fontSize == value) return\n    void setDesktopLyricTextSize(value).then(() => {\n      updateSetting({ 'desktopLyric.style.fontSize': value })\n    }).finally(() => {\n      setSliding(false)\n    })\n  }, [fontSize])\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_text_size')}>\n      <View style={styles.content}>\n        <Text style={{ color: theme['c-primary-font'] }}>{isSliding ? sliderSize : fontSize}</Text>\n        <Slider\n          minimumValue={100}\n          maximumValue={500}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={2}\n          value={fontSize}\n        />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    alignItems: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/Theme.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { setDesktopLyricColor } from '@/core/desktopLyric'\nimport { useI18n } from '@/lang'\nimport { memo } from 'react'\nimport { StyleSheet, View, TouchableOpacity } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\n\nconst themes = [\n  ['#08e664', 'rgba(0,0,0,0.6)'],\n  ['#fffa12', 'rgba(0,0,0,0.6)'],\n  ['#019ce4', 'rgba(0,0,0,0.6)'],\n  ['#ff1222', 'rgba(0,0,0,0.6)'],\n  ['#ef6976', 'rgba(0,0,0,0.6)'],\n  ['#c851d4', 'rgba(0,0,0,0.6)'],\n  ['#ffa600', 'rgba(0,0,0,0.6)'],\n  ['#000000', '#ffffff'],\n  ['#ffffff', 'rgba(0,0,0,0.6)'],\n] as const\ntype Theme = typeof themes[number]\n\nconst ThemeItem = ({ color, change }: {\n  color: Theme\n  change: (color: Theme) => void\n}) => {\n  return (\n    <TouchableOpacity style={styles.item} activeOpacity={0.5} onPress={() => { change(color) }}>\n      <View style={styles.colorContent}>\n        <View style={{ ...styles.image, backgroundColor: color[0] }}></View>\n      </View>\n    </TouchableOpacity>\n  )\n}\n\nexport default memo(() => {\n  const t = useI18n()\n\n  const setThemeDesktopLyric = (color: Theme) => {\n    // const shadowColor = 'rgba(0,0,0,0.6)'\n    void setDesktopLyricColor(null, color[0], color[1]).then(() => {\n      updateSetting({ 'desktopLyric.style.lyricPlayedColor': color[0], 'desktopLyric.style.lyricShadowColor': color[1] })\n    })\n  }\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_theme')}>\n      <View style={styles.list}>\n        {\n          themes.map((c, i) => <ThemeItem key={i.toString()} color={c} change={setThemeDesktopLyric} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n  item: {\n    marginRight: 15,\n    marginTop: 5,\n    alignItems: 'center',\n    width: 26,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  colorContent: {\n    width: 26,\n    height: 26,\n    borderRadius: 4,\n    // borderWidth: 1.6,\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n  image: {\n    width: 20,\n    height: 20,\n    borderRadius: 4,\n    elevation: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/ViewWidth.tsx",
    "content": "import { memo, useCallback, useState } from 'react'\nimport { View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport Slider, { type SliderProps } from '../../components/Slider'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { setDesktopLyricWidth } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\n\n\nexport default memo(() => {\n  const t = useI18n()\n  const width = useSettingValue('desktopLyric.width')\n  const theme = useTheme()\n  const [sliderSize, setSliderSize] = useState(width)\n  const [isSliding, setSliding] = useState(false)\n  const handleSlidingStart = useCallback<NonNullable<SliderProps['onSlidingStart']>>(() => {\n    setSliding(true)\n  }, [])\n  const handleValueChange = useCallback<NonNullable<SliderProps['onValueChange']>>(value => {\n    setSliderSize(value)\n  }, [])\n  const handleSlidingComplete = useCallback<NonNullable<SliderProps['onSlidingComplete']>>(value => {\n    if (width == value) return\n    void setDesktopLyricWidth(value).then(() => {\n      updateSetting({ 'desktopLyric.width': value })\n    }).finally(() => {\n      setSliding(false)\n    })\n  }, [width])\n\n  return (\n    <SubTitle title={t('setting_lyric_desktop_view_width')}>\n      <View style={styles.content}>\n        <Text style={{ color: theme['c-primary-font'] }}>{isSliding ? sliderSize : width}</Text>\n        <Slider\n          minimumValue={10}\n          maximumValue={100}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={1}\n          value={width}\n        />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    alignItems: 'center',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/LyricDesktop/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport IsShowLyric from './IsShowLyric'\nimport IsLockLyric from './IsLockLyric'\nimport IsShowToggleAnima from './IsShowToggleAnima'\nimport IsSingleLine from './IsSingleLine'\nimport TextSize from './TextSize'\nimport ViewWidth from './ViewWidth'\nimport MaxLineNum from './MaxLineNum'\nimport TextOpacity from './TextOpacity'\nimport TextPositionX from './TextPositionX'\nimport TextPositionY from './TextPositionY'\nimport { useI18n } from '@/lang'\nimport Theme from './Theme'\n// import { useTranslation } from '@/plugins/i18n'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_lyric_desktop')}>\n      <IsShowLyric />\n      <IsLockLyric />\n      <IsShowToggleAnima />\n      <IsSingleLine />\n      <Theme />\n      <TextSize />\n      <ViewWidth />\n      <MaxLineNum />\n      <TextOpacity />\n      <TextPositionX />\n      <TextPositionY />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/DislikeEditModal.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState, useCallback } from 'react'\nimport Text from '@/components/common/Text'\nimport { type LayoutChangeEvent, View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport Dialog, { type DialogType } from '@/components/common/Dialog'\nimport Button from '@/components/common/Button'\n\ninterface RuleInputType {\n  setText: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst RuleInput = forwardRef<RuleInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n  const [height, setHeight] = useState(100)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setText(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  const handleLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {\n    setHeight(nativeEvent.layout.height)\n  }, [])\n\n  return (\n    <View style={styles.inputContent} onLayout={handleLayout}>\n      <Input\n        ref={inputRef}\n        value={text}\n        onChangeText={setText}\n        multiline\n        textAlignVertical=\"top\"\n        placeholder={t('setting_dislike_list_input_tip')}\n        size={13}\n        style={{ ...styles.input, height, backgroundColor: theme['c-primary-input-background'] }}\n      />\n    </View>\n  )\n})\n\n\nexport interface DislikeEditModalProps {\n  onSave: (rules: string) => void\n  // onSourceChange: SourceSelectorProps['onSourceChange']\n}\nexport interface DislikeEditModalType {\n  show: (rules: string) => void\n}\n\nexport default forwardRef<DislikeEditModalType, DislikeEditModalProps>(({ onSave }, ref) => {\n  const dialogRef = useRef<DialogType>(null)\n  // const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  const inputRef = useRef<RuleInputType>(null)\n  const [visible, setVisible] = useState(false)\n  const theme = useTheme()\n  const t = useI18n()\n\n  const handleShow = (rules: string) => {\n    dialogRef.current?.setVisible(true)\n    requestAnimationFrame(() => {\n      inputRef.current?.setText(rules.length ? rules + '\\n' : rules)\n      // sourceSelectorRef.current?.setSource(source)\n      // setTimeout(() => {\n      //   inputRef.current?.focus()\n      // }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show(rules) {\n      if (visible) handleShow(rules)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow(rules)\n        })\n      }\n    },\n  }))\n\n  const handleCancel = () => {\n    dialogRef.current?.setVisible(false)\n  }\n  const handleConfirm = () => {\n    let rules = inputRef.current?.getText() ?? ''\n    handleCancel()\n    onSave(rules)\n  }\n\n  return (\n    visible\n      ? (\n          <Dialog height='80%' ref={dialogRef} bgHide={false}>\n            <View style={styles.content}>\n              <RuleInput ref={inputRef} />\n              <Text style={styles.inputTipText} size={13} color={theme['c-600']}>{t('setting_dislike_list_tips')}</Text>\n            </View>\n            <View style={styles.btns}>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>\n                <Text size={14} color={theme['c-button-font']}>{t('cancel')}</Text>\n              </Button>\n              <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleConfirm}>\n                <Text size={14} color={theme['c-button-font']}>{t('confirm')}</Text>\n              </Button>\n            </View>\n          </Dialog>\n        ) : null\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingHorizontal: 15,\n    paddingTop: 15,\n    paddingBottom: 10,\n    flexDirection: 'column',\n  },\n  col: {\n    flexDirection: 'row',\n    height: 38,\n  },\n  // selector: {\n  //   borderTopLeftRadius: 4,\n  //   borderBottomLeftRadius: 4,\n  // },\n  inputContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n  input: {\n    minWidth: 290,\n    // borderRadius: 4,\n    // borderTopRightRadius: 4,\n    // borderBottomRightRadius: 4,\n    paddingTop: 5,\n    paddingBottom: 5,\n  },\n  inputTipText: {\n    marginTop: 8,\n    // lineHeight: 18,\n    // backgroundColor: 'rgba(0, 0, 0, 0.2)',\n  },\n\n  btns: {\n    flexDirection: 'row',\n    justifyContent: 'center',\n    paddingBottom: 15,\n    paddingLeft: 15,\n    // paddingRight: 15,\n  },\n  btn: {\n    flex: 1,\n    padding: 10,\n    alignItems: 'center',\n    borderRadius: 4,\n    marginRight: 15,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/DislikeList.tsx",
    "content": "import { memo, useRef } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\n// import { gzip, ungzip } from 'pako'\n\nimport SubTitle from '../../components/SubTitle'\nimport Button from '../../components/Button'\nimport { toast } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { state, useRuleNum } from '@/store/dislikeList'\nimport DislikeEditModal, { type DislikeEditModalType } from './DislikeEditModal'\nimport { overwirteDislikeInfo } from '@/core/dislikeList'\n\nexport default memo(() => {\n  const t = useI18n()\n  const modalRef = useRef<DislikeEditModalType>(null)\n  const ruleNum = useRuleNum()\n\n  const handleShow = () => {\n    modalRef.current?.show(state.dislikeInfo.rules)\n  }\n\n  const handleSave = async(rules: string) => {\n    if (state.dislikeInfo.rules.trim() == rules.trim()) return\n    await overwirteDislikeInfo(rules)\n    toast(t('setting__other_dislike_list_saved_tip'))\n  }\n\n  return (\n    <SubTitle title={t('setting__other_dislike_list')}>\n      <View style={styles.ruleNum}>\n        <Text>{t('setting__other_dislike_list_label', { num: ruleNum })}</Text>\n      </View>\n      <View style={styles.btn}>\n        <Button onPress={handleShow}>{t('setting_other_dislike_list_show_btn')}</Button>\n      </View>\n      <DislikeEditModal ref={modalRef} onSave={handleSave} />\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  ruleNum: {\n    marginBottom: 5,\n  },\n  btn: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/Log.tsx",
    "content": "import { memo, useRef, useState, useEffect } from 'react'\nimport { View } from 'react-native'\nimport { getLogs, clearLogs } from '@/utils/log'\n// import { gzip, ungzip } from 'pako'\n\nimport SubTitle from '../../components/SubTitle'\nimport Button from '../../components/Button'\nimport { createStyle, toast } from '@/utils/tools'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\n\nexport default memo(() => {\n  const t = useI18n()\n  const alertRef = useRef<ConfirmAlertType>(null)\n  const [logText, setLogText] = useState('')\n  const isUnmountedRef = useRef(true)\n  const [isEnableSyncErrorLog, setIsEnableSyncErrorLog] = useState(global.lx.isEnableSyncLog)\n  const [isEnableUserApiLog, setIsEnableUserApiLog] = useState(global.lx.isEnableUserApiLog)\n\n  const getErrorLog = () => {\n    void getLogs().then(log => {\n      if (isUnmountedRef.current) return\n      const logArr = log.split(/^----lx log----\\n|\\n----lx log----\\n|\\n----lx log----$/)\n      // console.log(logArr)\n      logArr.reverse()\n      setLogText(logArr.join('\\n\\n').replace(/^\\n+|\\n+$/, ''))\n    })\n  }\n\n  const openLogModal = () => {\n    getErrorLog()\n    alertRef.current?.setVisible(true)\n  }\n\n  const handleCleanLog = () => {\n    void clearLogs().then(() => {\n      toast(t('setting_other_log_tip_clean_success'))\n      getErrorLog()\n    })\n  }\n\n  const handleSetEnableSyncErrorLog = (enable: boolean) => {\n    setIsEnableSyncErrorLog(enable)\n    global.lx.isEnableSyncLog = enable\n  }\n\n  const handleSetEnableUserApiLog = (enable: boolean) => {\n    setIsEnableUserApiLog(enable)\n    global.lx.isEnableUserApiLog = enable\n  }\n\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  return (\n    <>\n      <SubTitle title={t('setting_other_log')}>\n        <View style={styles.checkBox}>\n          <CheckBoxItem check={isEnableSyncErrorLog} label={t('setting_other_log_sync_log')} onChange={handleSetEnableSyncErrorLog} />\n          <CheckBoxItem check={isEnableUserApiLog} label={t('setting_other_log_user_api_log')} onChange={handleSetEnableUserApiLog} />\n        </View>\n        <View style={styles.btn}>\n          <Button onPress={openLogModal}>{t('setting_other_log_btn_show')}</Button>\n        </View>\n      </SubTitle>\n      <ConfirmAlert\n        ref={alertRef}\n        cancelText={t('setting_other_log_btn_hide')}\n        confirmText={t('setting_other_log_btn_clean')}\n        onConfirm={handleCleanLog}\n        showConfirm={!!logText}\n        reverseBtn={true}\n        >\n        <View onStartShouldSetResponder={() => true}>\n          {\n            logText\n              ? <Text selectable size={13}>{ logText }</Text>\n              : <Text size={13}>{t('setting_other_log_tip_null')}</Text>\n          }\n        </View>\n      </ConfirmAlert>\n    </>\n  )\n})\n\nconst styles = createStyle({\n  checkBox: {\n    // paddingTop: 10,\n    paddingBottom: 15,\n    marginLeft: -25,\n  },\n  btn: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/MetaCache.tsx",
    "content": "import { memo, useState, useEffect } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\n// import { gzip, ungzip } from 'pako'\n\nimport SubTitle from '../../components/SubTitle'\nimport Button from '../../components/Button'\nimport { toast } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { clearLyric, clearOtherSource, getMetaCache } from '@/utils/data'\n\nexport default memo(() => {\n  const t = useI18n()\n  const [otherSourceCleaning, setOtherSourceCleaning] = useState(false)\n  const [lyricCleaning, setLyricCleaning] = useState(false)\n  const [cacheInfo, setCacheInfo] = useState<{\n    otherSourceKeys: null | string[]\n    // musicUrlKeys: null | string[]\n    lyricKeys: null | string[]\n  }>({ otherSourceKeys: null, lyricKeys: null })\n\n  const handleGetMetaCache = () => {\n    void getMetaCache().then((info) => {\n      setCacheInfo(info)\n    })\n  }\n\n  const handleCleanOtherSourceCache = () => {\n    if (!cacheInfo.otherSourceKeys?.length) return\n    setOtherSourceCleaning(true)\n    void clearOtherSource(cacheInfo.otherSourceKeys).then(() => {\n      toast(t('setting_other_cache_clear_success_tip'))\n    }).finally(() => {\n      handleGetMetaCache()\n      setOtherSourceCleaning(false)\n    })\n  }\n\n  const handleCleanLyricKeysCache = () => {\n    if (!cacheInfo.lyricKeys?.length) return\n    setLyricCleaning(true)\n    void clearLyric(cacheInfo.lyricKeys).then(() => {\n      toast(t('setting_other_cache_clear_success_tip'))\n    }).finally(() => {\n      handleGetMetaCache()\n      setLyricCleaning(false)\n    })\n  }\n\n\n  useEffect(() => {\n    handleGetMetaCache()\n  }, [])\n\n  return (\n    <>\n      <SubTitle title={t('setting__other_meta_cache')}>\n        <View style={styles.cacheSize}>\n          <Text>{cacheInfo.otherSourceKeys == null ? t('setting_other_cache_getting') : `${t('setting__other_other_source_label')}${cacheInfo.otherSourceKeys.length}`}</Text>\n          {/* <Text>{cacheInfo.musicUrlKeys == null ? t('setting_other_cache_getting') : `${t('setting__other_music_url_label')}${cacheInfo.musicUrlKeys.length}`}</Text> */}\n          <Text>{cacheInfo.lyricKeys == null ? t('setting_other_cache_getting') : `${t('setting__other_lyric_raw_label')}${cacheInfo.lyricKeys.length}`}</Text>\n        </View>\n        <View style={styles.clearBtn}>\n          <Button disabled={otherSourceCleaning} onPress={handleCleanOtherSourceCache}>{t('setting__other_other_source_clear_btn')}</Button>\n          {/* <Button disabled={cleaning} onPress={handleCleanMusicUrlCache}>{t('setting__other_music_url_clear_btn')}</Button> */}\n          <Button disabled={lyricCleaning} onPress={handleCleanLyricKeysCache}>{t('setting__other_lyric_raw_clear_btn')}</Button>\n        </View>\n      </SubTitle>\n    </>\n  )\n})\n\nconst styles = StyleSheet.create({\n  cacheSize: {\n    marginBottom: 5,\n  },\n  clearBtn: {\n    gap: 5,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/ResourceCache.tsx",
    "content": "import { memo, useState, useEffect } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\n// import { gzip, ungzip } from 'pako'\n\nimport SubTitle from '../../components/SubTitle'\nimport Button from '../../components/Button'\nimport { toast, resetNotificationPermissionCheck, confirmDialog, resetIgnoringBatteryOptimizationCheck } from '@/utils/tools'\nimport { getAppCacheSize, clearAppCache } from '@/utils/nativeModules/cache'\nimport { getCacheSize, clearCache } from '@/plugins/player/utils'\nimport { sizeFormate } from '@/utils'\nimport { useI18n } from '@/lang'\nimport Text from '@/components/common/Text'\nimport { clearMusicUrl } from '@/utils/data'\n\nexport default memo(() => {\n  const t = useI18n()\n  const [cleaning, setCleaning] = useState(false)\n  const [cacheSize, setCacheSize] = useState<string | null>(null)\n  // const setting = useGetter('common', 'setting')\n  // TODO clear list cache\n  // const clearCache = useDispatch('list', 'clearCache')\n\n  const handleGetAppCacheSize = () => {\n    void Promise.all([getAppCacheSize(), getCacheSize()]).then(([size, size2]) => {\n      const count = size + size2\n      setCacheSize(sizeFormate(count))\n    })\n  }\n\n  const handleCleanCache = () => {\n    if (cacheSize == null) return\n    void confirmDialog({\n      message: t('confirm_tip'),\n      confirmButtonText: t('list_remove_tip_button'),\n    }).then(confirm => {\n      if (!confirm) return\n      setCleaning(true)\n      void Promise.all([\n        clearAppCache(),\n        clearCache(),\n        clearMusicUrl(),\n        resetNotificationPermissionCheck(),\n        resetIgnoringBatteryOptimizationCheck(),\n      ]).then(() => {\n        toast(t('setting_other_cache_clear_success_tip'))\n      }).finally(() => {\n        handleGetAppCacheSize()\n        setCleaning(false)\n      })\n    })\n  }\n\n\n  useEffect(() => {\n    handleGetAppCacheSize()\n  }, [])\n\n  return (\n    <>\n      <SubTitle title={t('setting__other_resource_cache')}>\n        <View style={styles.cacheSize}>\n          <Text>{cacheSize == null ? t('setting_other_cache_getting') : t('setting_other_cache_size') + cacheSize}</Text>\n        </View>\n        <View style={styles.clearBtn}>\n          <Button disabled={cleaning} onPress={handleCleanCache}>{t('setting_other_cache_clear_btn')}</Button>\n        </View>\n      </SubTitle>\n    </>\n  )\n})\n\nconst styles = StyleSheet.create({\n  cacheSize: {\n    marginBottom: 5,\n  },\n  clearBtn: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Other/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport ResourceCache from './ResourceCache'\nimport MetaCache from './MetaCache'\nimport DislikeList from './DislikeList'\nimport Log from './Log'\n// import MaxCache from './MaxCache'\nimport { useI18n } from '@/lang'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_other')}>\n      <ResourceCache />\n      <MetaCache />\n      <DislikeList />\n      <Log />\n      {/* <MaxCache /> */}\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsAutoCleanPlayedList.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isAutoCleanPlayedList = useSettingValue('player.isAutoCleanPlayedList')\n  const setAutoCleanPlayedList = (isAutoCleanPlayedList: boolean) => {\n    updateSetting({ 'player.isAutoCleanPlayedList': isAutoCleanPlayedList })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem\n        check={isAutoCleanPlayedList}\n        onChange={setAutoCleanPlayedList}\n        helpDesc={t('setting_play_auto_clean_played_list_tip')}\n        label={t('setting_play_auto_clean_played_list')}\n      />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsEnableAudioOffload.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle, toast } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isEnableAudioOffload = useSettingValue('player.isEnableAudioOffload')\n  const setHandleAudioFocus = (isEnableAudioOffload: boolean) => {\n    updateSetting({ 'player.isEnableAudioOffload': isEnableAudioOffload })\n    toast(t('setting_play_handle_audio_focus_tip'))\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem\n        check={isEnableAudioOffload}\n        onChange={setHandleAudioFocus}\n        helpDesc={t('setting_play_audio_offload_tip')}\n        label={t('setting_play_audio_offload')}\n      />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsHandleAudioFocus.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle, toast } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isHandleAudioFocus = useSettingValue('player.isHandleAudioFocus')\n  const setHandleAudioFocus = (isHandleAudioFocus: boolean) => {\n    updateSetting({ 'player.isHandleAudioFocus': isHandleAudioFocus })\n    toast(t('setting_play_handle_audio_focus_tip'))\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isHandleAudioFocus} onChange={setHandleAudioFocus} label={t('setting_play_handle_audio_focus')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsS2T.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isS2t = useSettingValue('player.isS2t')\n  const setS2T = (isS2t: boolean) => {\n    updateSetting({ 'player.isS2t': isS2t })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isS2t} onChange={setS2T} label={t('setting_play_s2t')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsSavePlayTime.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isSavePlayTime = useSettingValue('player.isSavePlayTime')\n  const setSavePlayTime = (isSavePlayTime: boolean) => {\n    updateSetting({ 'player.isSavePlayTime': isSavePlayTime })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isSavePlayTime} label={t('setting_player_save_play_time')} onChange={setSavePlayTime} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsShowBluetoothLyric.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle, remoteLyricTip } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { showRemoteLyric } from '@/core/desktopLyric'\nimport { setLastLyric } from '@/core/player/playInfo'\nimport { updateNowPlayingTitles } from '@/plugins/player/utils'\nimport playerState from '@/store/player/state'\nimport { state } from '@/plugins/player/playList'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowBluetoothLyric = useSettingValue('player.isShowBluetoothLyric')\n  const setShowBluetoothLyric = async(isShowBluetoothLyric: boolean) => {\n    if (isShowBluetoothLyric) {\n      await remoteLyricTip()\n    }\n    updateSetting({ 'player.isShowBluetoothLyric': isShowBluetoothLyric })\n    void showRemoteLyric(isShowBluetoothLyric)\n    if (!isShowBluetoothLyric) {\n      setLastLyric()\n      void updateNowPlayingTitles((state.prevDuration || 0) * 1000, playerState.musicInfo.name, playerState.musicInfo.singer ?? '', playerState.musicInfo.album ?? '')\n    }\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowBluetoothLyric} onChange={setShowBluetoothLyric} label={t('setting_play_show_bluetooth_lyric')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsShowLyricRoma.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { toggleRoma } from '@/core/lyric'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowLyricRoma = useSettingValue('player.isShowLyricRoma')\n  const setShowLyricRoma = (isShowLyricRoma: boolean) => {\n    updateSetting({ 'player.isShowLyricRoma': isShowLyricRoma })\n    void toggleRoma(isShowLyricRoma)\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowLyricRoma} onChange={setShowLyricRoma} label={t('setting_play_show_roma')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsShowLyricTranslation.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { toggleTranslation } from '@/core/lyric'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowLyricTranslation = useSettingValue('player.isShowLyricTranslation')\n  const setShowLyricTranslation = (isShowLyricTranslation: boolean) => {\n    updateSetting({ 'player.isShowLyricTranslation': isShowLyricTranslation })\n    void toggleTranslation(isShowLyricTranslation)\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowLyricTranslation} onChange={setShowLyricTranslation} label={t('setting_play_show_translation')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/IsShowNotificationImage.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowNotificationImage = useSettingValue('player.isShowNotificationImage')\n  const setShowNotificationImage = (isShowNotificationImage: boolean) => {\n    updateSetting({ 'player.isShowNotificationImage': isShowNotificationImage })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowNotificationImage} onChange={setShowNotificationImage} label={t('setting_play_show_notification_image')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/MaxCache.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { View } from 'react-native'\n\nimport InputItem, { type InputItemProps } from '../../components/InputItem'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\n\nconst MAX_SIZE = 1024 * 1024 * 1024\nexport default memo(() => {\n  const t = useI18n()\n  const cacheSize = useSettingValue('player.cacheSize')\n  const setCacheSize = (size: string) => {\n    updateSetting({ 'player.cacheSize': size })\n  }\n\n  const size = useMemo(() => {\n    let size: number | string = parseInt(cacheSize)\n    if (size == 0 || Number.isNaN(size)) size = ''\n    return size.toString()\n  }, [cacheSize])\n\n  const setSize: InputItemProps['onChanged'] = (value, callback) => {\n    let size: number | string = parseInt(value)\n    if (Number.isNaN(size) || size < 0) size = ''\n    else if (size > MAX_SIZE) size = MAX_SIZE\n    size = size.toString()\n    callback(size)\n    if (cacheSize == size) return\n    setCacheSize(size)\n    toast(t('setting_play_cache_size_save_tip'))\n  }\n\n  return (\n    <View style={styles.content} >\n      <InputItem\n        value={size}\n        label={t('setting_play_cache_size')}\n        onChanged={setSize}\n        keyboardType=\"number-pad\"\n        placeholder={t('setting_play_cache_size_no_cache')} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 10,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/PlayHighQuality.tsx",
    "content": "import { memo, useMemo } from 'react'\n\nimport { StyleSheet, View } from 'react-native'\n\nimport SubTitle from '../../components/SubTitle'\nimport CheckBox from '@/components/common/CheckBox'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { TRY_QUALITYS_LIST } from '@/core/music/utils'\n\nconst useActive = (id: LX.Quality) => {\n  const q = useSettingValue('player.playQuality')\n  const isActive = useMemo(() => q == id, [q, id])\n  return isActive\n}\n\nconst Item = ({ id, name }: {\n  id: LX.Quality\n  name: string\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginRight={8} check={isActive} label={name} onChange={() => { updateSetting({ 'player.playQuality': id }) }} need />\n}\n\nexport default memo(() => {\n  const t = useI18n()\n  const playQualityList = useMemo(() => {\n    return [...TRY_QUALITYS_LIST, '128k'].reverse() as LX.Quality[]\n  }, [])\n\n  return (\n    <SubTitle title={t('setting_play_play_quality')}>\n      <View style={styles.list}>\n        {\n          playQualityList.map((q) => <Item name={q} id={q} key={q} />)\n        }\n      </View>\n    </SubTitle>\n  )\n})\n\nconst styles = StyleSheet.create({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n})\n\n\n// export default memo(() => {\n//   const t = useI18n()\n//   const isPlayHighQuality = useSettingValue('player.isPlayHighQuality')\n//   const setPlayHighQuality = (isPlayHighQuality: boolean) => {\n//     updateSetting({ 'player.isPlayHighQuality': isPlayHighQuality })\n//   }\n\n//   return (\n//     <View style={styles.content}>\n//       <CheckBoxItem check={isPlayHighQuality} onChange={setPlayHighQuality} label={t('setting_play_quality')} />\n//     </View>\n//   )\n// })\n\n\n// const styles = createStyle({\n//   content: {\n//     marginTop: 5,\n//   },\n// })\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Player/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport IsSavePlayTime from './IsSavePlayTime'\nimport PlayHighQuality from './PlayHighQuality'\nimport IsHandleAudioFocus from './IsHandleAudioFocus'\nimport IsEnableAudioOffload from './IsEnableAudioOffload'\nimport IsAutoCleanPlayedList from './IsAutoCleanPlayedList'\nimport IsShowBluetoothLyric from './IsShowBluetoothLyric'\nimport IsShowNotificationImage from './IsShowNotificationImage'\nimport IsShowLyricTranslation from './IsShowLyricTranslation'\nimport IsShowLyricRoma from './IsShowLyricRoma'\nimport IsS2T from './IsS2T'\nimport MaxCache from './MaxCache'\nimport { useI18n } from '@/lang'\n\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_player')}>\n      <IsSavePlayTime />\n      <IsAutoCleanPlayedList />\n      <IsHandleAudioFocus />\n      <IsEnableAudioOffload />\n      <IsShowBluetoothLyric />\n      <IsShowNotificationImage />\n      <IsShowLyricTranslation />\n      <IsShowLyricRoma />\n      <IsS2T />\n      <MaxCache />\n      <PlayHighQuality />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Search/IsShowHistorySearch.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowHistorySearch = useSettingValue('search.isShowHistorySearch')\n  const handleUpdate = (isShowHistorySearch: boolean) => {\n    updateSetting({ 'search.isShowHistorySearch': isShowHistorySearch })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowHistorySearch} onChange={handleUpdate} label={t('setting_search_show_history_search')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    marginBottom: 15,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Search/IsShowHotSearch.tsx",
    "content": "import { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { memo } from 'react'\nimport { View } from 'react-native'\nimport { useSettingValue } from '@/store/setting/hook'\n\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isShowHotSearch = useSettingValue('search.isShowHotSearch')\n  const handleUpdate = (isShowHotSearch: boolean) => {\n    updateSetting({ 'search.isShowHotSearch': isShowHotSearch })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isShowHotSearch} onChange={handleUpdate} label={t('setting_search_show_hot_search')} />\n    </View>\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Search/index.tsx",
    "content": "import { memo } from 'react'\n\nimport Section from '../../components/Section'\nimport IsShowHotSearch from './IsShowHotSearch'\nimport IsShowHistorySearch from './IsShowHistorySearch'\n\nimport { useI18n } from '@/lang'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  return (\n    <Section title={t('setting_search')}>\n      <IsShowHotSearch />\n      <IsShowHistorySearch />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Sync/History.tsx",
    "content": "import { memo, useRef, useState, useCallback, useImperativeHandle, forwardRef } from 'react'\nimport { View, TouchableOpacity, ScrollView } from 'react-native'\n// import { gzip, ungzip } from 'pako'\nimport { Icon } from '@/components/common/Icon'\n\nimport Button from '../../components/Button'\nimport { getSyncHostHistory, removeSyncHostHistory, setSyncHost } from '@/plugins/sync/data'\nimport Popup, { type PopupType } from '@/components/common/Popup'\nimport { BorderWidths } from '@/theme'\nimport Text from '@/components/common/Text'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { createStyle } from '@/utils/tools'\n\ntype SyncHistoryItem = Awaited<ReturnType<typeof getSyncHostHistory>>[number]\n\nconst HistoryListItem = ({ item, index, onRemove, onSelect }: {\n  item: SyncHistoryItem\n  index: number\n  onRemove: (index: number) => void\n  onSelect: (index: number) => void\n}) => {\n  const theme = useTheme()\n  const handleSetHost = () => {\n    onSelect(index)\n    // setHost({\n    //   host: item.host,\n    //   port: item.port,\n    // })\n    // setSyncHost({\n    //   host: item.host,\n    //   port: item.port,\n    // })\n  }\n  const handleRemove = () => {\n    onRemove(index)\n  }\n\n  return (\n    <View style={{ ...styles.listItem, borderBottomColor: theme['c-border-background'] }}>\n      <TouchableOpacity style={styles.listName} onPress={handleSetHost}>\n        <Text numberOfLines={1}>{item}</Text>\n      </TouchableOpacity>\n      <TouchableOpacity onPress={handleRemove} style={styles.listBtn}>\n        <Icon name=\"remove\" color={theme['c-font-label']} size={12} />\n      </TouchableOpacity>\n    </View>\n  )\n}\n\ninterface HistoryListProps {\n  onSelect: (item: SyncHistoryItem) => void\n}\ninterface HistoryListType {\n  show: () => void\n}\nconst HistoryList = forwardRef<HistoryListType, HistoryListProps>(({ onSelect }, ref) => {\n  const popupRef = useRef<PopupType>(null)\n  const [visible, setVisible] = useState(false)\n  const [list, setList] = useState<SyncHistoryItem[]>([])\n  // const isUnmountedRef = useRef(true)\n  const theme = useTheme()\n  const t = useI18n()\n\n  const handleShow = () => {\n    popupRef.current?.setVisible(true)\n    requestAnimationFrame(() => {\n      void getSyncHostHistory().then(historyList => {\n        setList([...historyList])\n      })\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show() {\n      if (visible) handleShow()\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow()\n        })\n      }\n    },\n  }))\n\n  const handleSelect = useCallback((index: number) => {\n    popupRef.current?.setVisible(false)\n    onSelect(list[index])\n  }, [list, onSelect])\n\n  const handleRemove = useCallback((index: number) => {\n    void removeSyncHostHistory(index)\n    const newList = [...list]\n    newList.splice(index, 1)\n    setList(newList)\n  }, [list])\n\n\n  return (\n    visible\n      ? (\n          <Popup ref={popupRef} title={t('setting_sync_history_title')}>\n            <ScrollView style={styles.list}>\n              {\n                list.length\n                  ? list.map((item, index) => (\n                      <HistoryListItem\n                        item={item}\n                        index={index}\n                        onRemove={handleRemove}\n                        key={item}\n                        onSelect={handleSelect}\n                      />\n                  ))\n                  : <Text style={styles.tipText} color={theme['c-font-label']}>{t('setting_sync_history_empty')}</Text>\n              }\n            </ScrollView>\n          </Popup>\n        )\n      : null\n  )\n})\n\nexport default memo(({ setHost }: {\n  setHost: (host: string) => void\n}) => {\n  const t = useI18n()\n  const isEnableSync = useSettingValue('sync.enable')\n  const listRef = useRef<HistoryListType>(null)\n\n  const showPopup = () => {\n    listRef.current?.show()\n  }\n\n  const handleSelect = (item: SyncHistoryItem) => {\n    setHost(item)\n    void setSyncHost(item)\n  }\n\n  return (\n    <>\n      <View style={styles.btn}>\n        <Button disabled={isEnableSync} onPress={showPopup}>{t('setting_sync_history')}</Button>\n      </View>\n      <HistoryList ref={listRef} onSelect={handleSelect} />\n    </>\n  )\n})\n\nconst styles = createStyle({\n  btn: {\n    flexDirection: 'row',\n    marginLeft: 25,\n    marginBottom: 15,\n  },\n  tipText: {\n    textAlign: 'center',\n    marginTop: 15,\n  },\n  list: {\n    flexShrink: 1,\n    flexGrow: 0,\n    paddingLeft: 15,\n    paddingRight: 15,\n  },\n  listItem: {\n    flexDirection: 'row',\n    alignItems: 'center',\n    paddingTop: 8,\n    paddingBottom: 8,\n    borderBottomWidth: BorderWidths.normal,\n  },\n  listName: {\n    flex: 1,\n  },\n  listBtn: {\n    padding: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Sync/IsEnable.tsx",
    "content": "import { memo, useCallback, useState, useEffect, useRef, useMemo } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Input from '@/components/common/Input'\nimport { connectServer, disconnectServer } from '@/plugins/sync'\nimport InputItem from '../../components/InputItem'\nimport { getWIFIIPV4Address } from '@/utils/nativeModules/utils'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { addSyncHostHistory, getSyncHost, setSyncHost } from '@/utils/data'\nimport { setSyncMessage } from '@/core/sync'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { useStatus } from '@/store/sync/hook'\nimport Text from '@/components/common/Text'\nimport { SYNC_CODE } from '@/plugins/sync/constants'\n\nconst addressRxp = /^https?:\\/\\/\\S+/i\n\nconst HostInput = memo(({ setHost, host, disabled }: {\n  setHost: (host: string) => void\n  host: string\n  disabled?: boolean\n}) => {\n  const t = useI18n()\n\n  const hostAddress = useMemo(() => {\n    return addressRxp.test(host) ? host : ''\n  }, [host])\n\n  const setHostAddress = useCallback((value: string, callback: (host: string) => void) => {\n    let hostAddress: string\n    if (addressRxp.test(value)) hostAddress = value.trim()\n    else {\n      hostAddress = ''\n      if (value) toast(t('setting_sync_host_value_error_tip'), 'long')\n    }\n    callback(hostAddress)\n    if (host == hostAddress) return\n    setHost(hostAddress)\n  }, [host, setHost, t])\n\n  return (\n    <InputItem\n      editable={!disabled}\n      value={hostAddress}\n      label={t('setting_sync_host_label')}\n      onChanged={setHostAddress}\n      inputMode=\"url\"\n      // keyboardType=\"url\"\n      placeholder={t('setting_sync_host_value_tip')} />\n  )\n})\n\n\nexport default memo(({ host, setHost }: {\n  host: string\n  setHost: (host: string) => void\n}) => {\n  const t = useI18n()\n  const setIsEnableSync = useCallback((enable: boolean) => {\n    updateSetting({ 'sync.enable': enable })\n  }, [])\n  const syncStatus = useStatus()\n  const isEnableSync = useSettingValue('sync.enable')\n  const isUnmountedRef = useRef(true)\n  const theme = useTheme()\n  const [address, setAddress] = useState('')\n  const [authCode, setAuthCode] = useState('')\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    void getSyncHost().then(host => {\n      if (isUnmountedRef.current) return\n      setHost(host)\n    })\n    void getWIFIIPV4Address().then(address => {\n      if (isUnmountedRef.current) return\n      setAddress(address)\n    })\n\n    return () => {\n      isUnmountedRef.current = true\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  useEffect(() => {\n    switch (syncStatus.message) {\n      case SYNC_CODE.authFailed:\n        toast(t('setting_sync_code_fail'))\n      case SYNC_CODE.missingAuthCode:\n        confirmAlertRef.current?.setVisible(true)\n        break\n      case SYNC_CODE.msgBlockedIp:\n        toast(t('setting_sync_code_blocked_ip'))\n        break\n      default:\n        break\n    }\n  }, [syncStatus.message, t])\n\n  const handleSetEnableSync = useCallback((enable: boolean) => {\n    setIsEnableSync(enable)\n\n    if (enable) void addSyncHostHistory(host)\n\n    void (enable ? connectServer(host) : disconnectServer())\n  }, [host, setIsEnableSync])\n\n\n  const handleUpdateHost = useCallback((h: string) => {\n    if (h == host) return\n    void setSyncHost(h)\n    setHost(h)\n  }, [host, setHost])\n\n\n  const status = useMemo(() => {\n    let status\n    switch (syncStatus.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 = syncStatus.message\n          ? syncStatus.message\n          : syncStatus.status\n            ? t('setting_sync_status_enabled')\n            : t('sync_status_disabled')\n        break\n    }\n    return status\n  }, [syncStatus.message, syncStatus.status, t])\n\n  const handleCancelSetCode = useCallback(() => {\n    setSyncMessage('')\n    confirmAlertRef.current?.setVisible(false)\n  }, [])\n  const handleSetCode = useCallback(() => {\n    // const code = authCode.trim()\n    // if (code.length != 6) return\n    void connectServer(host, authCode)\n    setAuthCode('')\n    confirmAlertRef.current?.setVisible(false)\n  }, [host, authCode])\n\n  return (\n    <>\n      <View style={styles.infoContent}>\n        <CheckBoxItem disabled={!host} check={isEnableSync} label={t('setting_sync_enable')} onChange={handleSetEnableSync} />\n        <Text style={styles.textAddr} size={13}>{t('setting_sync_address', { address })}</Text>\n        <Text style={styles.text} size={13}>{t('setting_sync_status', { status })}</Text>\n      </View>\n      <View style={styles.inputContent} >\n        <HostInput setHost={handleUpdateHost} host={host} disabled={isEnableSync} />\n      </View>\n      <ConfirmAlert\n        onCancel={handleCancelSetCode}\n        onConfirm={handleSetCode}\n        ref={confirmAlertRef}\n        >\n        <View style={styles.authCodeContent}>\n          <Text style={styles.authCodeLabel}>{t('setting_sync_code_label')}</Text>\n          <Input\n            placeholder={t('setting_sync_code_input_tip')}\n            value={authCode}\n            onChangeText={setAuthCode}\n            style={{ ...styles.authCodeInput, backgroundColor: theme['c-primary-background'] }}\n          />\n        </View>\n      </ConfirmAlert>\n    </>\n  )\n})\n\n\nconst styles = createStyle({\n  infoContent: {\n    marginTop: 5,\n  },\n  textAddr: {\n    marginLeft: 25,\n    marginTop: 5,\n  },\n  text: {\n    marginLeft: 25,\n  },\n  inputContent: {\n    marginTop: 8,\n  },\n  authCodeContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  authCodeLabel: {\n    marginBottom: 5,\n  },\n  authCodeInput: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 260,\n    borderRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n    // fontSize: 14,\n  },\n\n  // tagTypeList: {\n  //   flexDirection: 'row',\n  //   flexWrap: 'wrap',\n  // },\n  // tagButton: {\n  //   // marginRight: 10,\n  //   borderRadius: 4,\n  //   marginRight: 10,\n  //   marginBottom: 10,\n  // },\n  // tagButtonText: {\n  //   paddingLeft: 12,\n  //   paddingRight: 12,\n  //   paddingTop: 8,\n  //   paddingBottom: 8,\n  // },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Sync/index.tsx",
    "content": "import { memo, useState } from 'react'\n\nimport Section from '../../components/Section'\nimport IsEnable from './IsEnable'\nimport History from './History'\nimport { useI18n } from '@/lang'\n// import SyncHost from './SyncHost'\n\nexport default memo(() => {\n  const t = useI18n()\n\n  const [host, setHost] = useState('')\n\n  return (\n    <Section title={t('setting_sync')}>\n      <IsEnable host={host} setHost={setHost} />\n      <History setHost={setHost} />\n    </Section>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Sync/isEnable.tsx.bak",
    "content": "import { memo, useState, useEffect, useRef } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport ConfirmAlert, { ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Input from '@/components/common/Input'\nimport { connect, disconnect, SYNC_CODE } from '@/plugins/sync'\nimport InputItem from '../../components/InputItem'\nimport { getWIFIIPV4Address } from '@/utils/nativeModules/utils'\nimport { createStyle, toast } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { addSyncHostHistory, getSyncHost, setSyncHost } from '@/utils/data'\nimport { setSpText } from '@/utils/pixelRatio'\nimport { setSyncMessage } from '@/core/sync'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { useStatus } from '@/store/sync/hook'\nimport Text from '@/components/common/Text'\n\nconst addressRxp = /(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})/\nconst portRxp = /(\\d+)/\n\nconst HostInput = ({ setHost, host, disabled }: {\n  setHost: (host: string) => void\n  host: string\n  disabled?: boolean\n}) => {\n  const t = useI18n()\n\n  const hostAddress = useMemo(() => {\n    return addressRxp.test(host) ? RegExp.$1 : ''\n  }, [host])\n\n  const setHostAddress = useCallback((value: string, callback: (host: string) => void) => {\n    let hostAddress = addressRxp.test(value) ? RegExp.$1 : ''\n    callback(hostAddress)\n    if (host == hostAddress) return\n    setHost(hostAddress)\n  }, [host, setHost])\n\n  return (\n    <InputItem\n      editable={!disabled}\n      value={hostAddress}\n      label={t('setting_sync_host_label')}\n      onChanged={setHostAddress}\n      keyboardType=\"number-pad\"\n      placeholder={t('setting_sync_host_value_tip')} />\n  )\n}\n\nconst PortInput = ({ setPort, port, disabled }: {\n  setPort: (port: string) => void\n  port: string\n  disabled?: boolean\n}) => {\n  const t = useI18n()\n\n  const portNum = useMemo(() => {\n    return portRxp.test(port) ? RegExp.$1 : ''\n  }, [port])\n\n  const setPortAddress = useCallback((value: string, callback: (port: string) => void) => {\n    let portNum = portRxp.test(value) ? RegExp.$1 : ''\n    callback(portNum)\n    if (port == portNum) return\n    setPort(portNum)\n  }, [port, setPort])\n\n  return (\n    <InputItem\n      editable={!disabled}\n      value={portNum}\n      label={t('setting_sync_port_label')}\n      onChanged={setPortAddress}\n      keyboardType=\"number-pad\"\n      placeholder={t('setting_sync_port_tip')} />\n  )\n}\n\nconst Status = () => {\n  const t = useI18n()\n  const syncStatus = useStatus()\n  const status = `${syncStatus.message ? syncStatus.message : syncStatus.status ? t('setting_sync_status_enabled') : t('sync_status_disabled')}`\n\n  return <Text style={styles.text} size={13}>{t('setting_sync_status', { status })}</Text>\n}\n\nexport default memo(({ isWaiting, setIsWaiting }: {\n  hostInfo: { host: string, port: string }\n  isWaiting: boolean\n  setHostInfo: (hostInfo: { host: string, port: string }) => void\n  setIsWaiting: (isWaiting: boolean) => void\n}) => {\n  const t = useI18n()\n  const setIsEnableSync = (enable: boolean) => {\n    updateSetting({ 'sync.enable': enable })\n  }\n  const isEnableSync = useSettingValue('sync.enable')\n  const isUnmountedRef = useRef(true)\n  const theme = useTheme()\n  const [address, setAddress] = useState('')\n  const [authCode, setAuthCode] = useState('')\n  const confirmAlertRef = useRef<ConfirmAlertType>(null)\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    void getSyncHost().then(hostInfo => {\n      if (isUnmountedRef.current) return\n      setHostInfo(hostInfo)\n    })\n    void getWIFIIPV4Address().then(address => {\n      if (isUnmountedRef.current) return\n      setAddress(address)\n    })\n\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  useEffect(() => {\n    switch (syncStatus.message) {\n      case SYNC_CODE.authFailed:\n        toast(t('setting_sync_code_fail'))\n      case SYNC_CODE.missingAuthCode:\n        confirmAlertRef.current?.setVisible(true)\n        break\n      default:\n        break\n    }\n  }, [syncStatus.message, t])\n\n  const handleSetEnableSync = (enable: boolean) => {\n    setIsEnableSync(enable)\n\n    if (enable) void addSyncHostHistory(hostInfo.host, hostInfo.port)\n\n    global.lx.isSyncEnableing = true\n    setIsWaiting(true)\n    ;(enable ? connect() : disconnect()).finally(() => {\n      global.lx.isSyncEnableing = false\n      setIsWaiting(false)\n    })\n  }\n\n\n  const setHost = (host: string) => {\n    if (host == hostInfo.host) return\n    const newHostInfo = { ...hostInfo, host }\n    void setSyncHost(newHostInfo)\n    setHostInfo(newHostInfo)\n  }\n  const setPort = (port: string) => {\n    if (port == hostInfo.host) return\n    const newHostInfo = { ...hostInfo, port }\n    void setSyncHost(newHostInfo)\n    setHostInfo(newHostInfo)\n  }\n\n  const handleCancelSetCode = () => {\n    setSyncMessage('')\n    confirmAlertRef.current?.setVisible(false)\n  }\n  const handleSetCode = () => {\n    const code = authCode.trim()\n    if (code.length != 6) return\n    void connect(code)\n    setAuthCode('')\n    confirmAlertRef.current?.setVisible(false)\n  }\n\n  return (\n    <>\n      <View style={{ marginTop: 5 }}>\n        <CheckBoxItem disabled={isWaiting || !port || !host} check={isEnableSync} label={t('setting_sync_enable')} onChange={handleSetEnableSync} />\n        <Text style={{ ...styles.text, marginTop: setSpText(5) }} size={13}>{t('setting_sync_address', { address })}</Text>\n        <Status />\n      </View>\n      <View style={{ marginTop: setSpText(10) }} >\n        <HostInput setHost={setHost} host={hostInfo.host} disabled={isWaiting || isEnableSync} />\n        <PortInput setPort={setPort} port={hostInfo.port} disabled={isWaiting || isEnableSync} />\n      </View>\n      <ConfirmAlert\n        onCancel={handleCancelSetCode}\n        onConfirm={handleSetCode}\n        ref={confirmAlertRef}\n        >\n        <View style={styles.authCodeContent}>\n          <Text style={{ marginBottom: setSpText(5) }}>{t('setting_sync_code_label')}</Text>\n          <Input\n            placeholder={t('setting_sync_code_input_tip')}\n            value={authCode}\n            onChangeText={setAuthCode}\n            style={{ ...styles.authCodeInput, backgroundColor: theme['c-primary-background'] }}\n          />\n        </View>\n      </ConfirmAlert>\n    </>\n  )\n})\n\n\nconst styles = createStyle({\n  authCodeContent: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  authCodeInput: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 240,\n    borderRadius: 4,\n    paddingTop: 2,\n    paddingBottom: 2,\n    fontSize: 14,\n  },\n  text: {\n    marginLeft: 25,\n  },\n\n  // tagTypeList: {\n  //   flexDirection: 'row',\n  //   flexWrap: 'wrap',\n  // },\n  // tagButton: {\n  //   // marginRight: 10,\n  //   borderRadius: 4,\n  //   marginRight: 10,\n  //   marginBottom: 10,\n  // },\n  // tagButtonText: {\n  //   paddingLeft: 12,\n  //   paddingRight: 12,\n  //   paddingTop: 8,\n  //   paddingBottom: 8,\n  // },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/IsAutoTheme.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { createStyle, getIsSupportedAutoTheme } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { getTheme } from '@/theme/themes'\nimport { applyTheme } from '@/core/theme'\nimport themeState from '@/store/theme/state'\n\nconst isSupportedAutoTheme = getIsSupportedAutoTheme()\n\nexport default memo(() => {\n  const t = useI18n()\n  const isAutoTheme = useSettingValue('common.isAutoTheme')\n  const setIsAutoTheme = (isAutoTheme: boolean) => {\n    updateSetting({ 'common.isAutoTheme': isAutoTheme })\n    void getTheme().then(theme => {\n      if (theme.id == themeState.theme.id) return\n      applyTheme(theme)\n    })\n  }\n\n\n  return (\n    isSupportedAutoTheme\n      ? (\n          <View style={styles.content}>\n            <CheckBoxItem check={isAutoTheme} label={t('setting_basic_theme_auto_theme')} onChange={setIsAutoTheme} />\n          </View>\n        )\n      : null\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/IsDynamicBg.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { useSettingValue } from '@/store/setting/hook'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isDynamicBg = useSettingValue('theme.dynamicBg')\n  const setIsDynamicBg = (isDynamicBg: boolean) => {\n    updateSetting({ 'theme.dynamicBg': isDynamicBg })\n  }\n\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isDynamicBg} label={t('setting_basic_theme_dynamic_bg')} onChange={setIsDynamicBg} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/IsFontShadow.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { useSettingValue } from '@/store/setting/hook'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isFontShadow = useSettingValue('theme.fontShadow')\n  const setIsFontShadow = (isFontShadow: boolean) => {\n    updateSetting({ 'theme.fontShadow': isFontShadow })\n  }\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isFontShadow} label={t('setting_basic_theme_font_shadow')} onChange={setIsFontShadow} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/IsHideBgDark.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\nimport CheckBoxItem from '../../components/CheckBoxItem'\nimport { createStyle } from '@/utils/tools'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { getTheme } from '@/theme/themes'\nimport { applyTheme } from '@/core/theme'\nimport settingState from '@/store/setting/state'\n\nexport default memo(() => {\n  const t = useI18n()\n  const isHideBgDark = useSettingValue('theme.hideBgDark')\n  const setIsAutoTheme = (isHideBgDark: boolean) => {\n    updateSetting({ 'theme.hideBgDark': isHideBgDark })\n    void getTheme().then(theme => {\n      if (!theme.isDark && !settingState.setting['common.isAutoTheme']) return\n      applyTheme(theme)\n    })\n  }\n\n\n  return (\n    <View style={styles.content}>\n      <CheckBoxItem check={isHideBgDark} label={t('setting_basic_theme_hide_bg_dark')} onChange={setIsAutoTheme} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  content: {\n    marginTop: 5,\n    // marginBottom: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/Theme.tsx",
    "content": "import { memo, useCallback, useEffect, useMemo, useState } from 'react'\nimport { View, TouchableOpacity, type ImageSourcePropType } from 'react-native'\nimport { setTheme } from '@/core/theme'\nimport { useI18n } from '@/lang'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useTheme } from '@/store/theme/hook'\n\nimport SubTitle from '../../components/SubTitle'\nimport { BG_IMAGES, getAllThemes, type LocalTheme } from '@/theme/themes'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { Icon } from '@/components/common/Icon'\nimport ImageBackground from '@/components/common/ImageBackground'\n\nconst useActive = (id: string) => {\n  const activeThemeId = useSettingValue('theme.id')\n  const isActive = useMemo(() => activeThemeId == id, [activeThemeId, id])\n  return isActive\n}\n\nconst ThemeItem = ({ id, name, color, image, setTheme, showAll }: {\n  id: string\n  name: string\n  color: string\n  showAll: boolean\n  image?: ImageSourcePropType\n  setTheme: (id: string) => void\n}) => {\n  const theme = useTheme()\n  const isActive = useActive(id)\n\n  return (\n    showAll || isActive ? (\n      <TouchableOpacity style={{ ...styles.item, width: scaleSizeH(ITEM_HEIGHT) }} activeOpacity={0.5} onPress={() => { setTheme(id) }}>\n        <View style={{ ...styles.colorContent, width: scaleSizeH(COLOR_ITEM_HEIGHT), borderColor: isActive ? color : 'transparent' }}>\n          {\n            image\n              ? <ImageBackground style={{ ...styles.imageContent, width: scaleSizeH(IMAGE_HEIGHT), backgroundColor: color }}\n                  imageStyle={{ borderRadius: 4 }}\n                  source={image} />\n              : <View style={{ ...styles.imageContent, width: scaleSizeH(IMAGE_HEIGHT), backgroundColor: color }}></View>\n            }\n        </View>\n        <Text style={styles.name} size={12} color={isActive ? color : theme['c-font']} numberOfLines={1}>{name}</Text>\n      </TouchableOpacity>\n    ) : null\n  )\n}\n\nconst MoreBtn = ({ showAll, setShowAll }: {\n  showAll: boolean\n  setShowAll: (showAll: boolean) => void\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n\n  return (\n    showAll ? null\n      : (\n          <TouchableOpacity style={styles.moreBtn} activeOpacity={0.5} onPress={() => { setShowAll(!showAll) }}>\n            <Text size={14} color={theme['c-primary-font']} numberOfLines={1}>{t('setting_basic_theme_more_btn_show')}</Text>\n            <Icon name=\"chevron-right\" size={12} color={theme['c-primary-font']} />\n          </TouchableOpacity>\n        )\n\n  )\n}\n\ninterface ThemeInfo {\n  themes: Readonly<LocalTheme[]>\n  userThemes: LX.Theme[]\n  dataPath: string\n}\nconst initInfo: ThemeInfo = { themes: [], userThemes: [], dataPath: '' }\nexport default memo(() => {\n  const [showAll, setShowAll] = useState(false)\n  const t = useI18n()\n  const [themeInfo, setThemeInfo] = useState(initInfo)\n  const setThemeId = useCallback((id: string) => {\n    requestAnimationFrame(() => {\n      setTheme(id)\n    })\n  }, [])\n\n  useEffect(() => {\n    void getAllThemes().then(setThemeInfo)\n  }, [])\n\n  return (\n    <SubTitle title={t('setting_basic_theme')}>\n      <View style={styles.list}>\n        {\n          themeInfo.themes.map(({ id, config }) => {\n            return <ThemeItem\n              key={id}\n              color={config.themeColors['c-theme']}\n              image={config.extInfo['bg-image'] ? BG_IMAGES[config.extInfo['bg-image']] : undefined}\n              showAll={showAll}\n              id={id}\n              name={t(`theme_${id}`)}\n              setTheme={setThemeId} />\n          })\n        }\n        {\n          themeInfo.userThemes.map(({ id, name, config }) => {\n            return <ThemeItem\n              key={id}\n              color={config.themeColors['c-theme']}\n              // image={undefined}\n              showAll={showAll}\n              id={id}\n              name={name}\n              setTheme={setThemeId} />\n          })\n        }\n        <MoreBtn showAll={showAll} setShowAll={setShowAll} />\n      </View>\n    </SubTitle>\n  )\n})\n\nconst ITEM_HEIGHT = 62\nconst COLOR_ITEM_HEIGHT = 36\nconst IMAGE_HEIGHT = 29\nconst styles = createStyle({\n  list: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    gap: 10,\n    marginTop: 5,\n  },\n  item: {\n    // marginRight: 15,\n    alignItems: 'center',\n    // marginTop: 5,\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  colorContent: {\n    height: COLOR_ITEM_HEIGHT,\n    borderRadius: 4,\n    borderWidth: 1.6,\n    alignItems: 'center',\n    justifyContent: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.2)',\n  },\n  imageContent: {\n    height: IMAGE_HEIGHT,\n    borderRadius: 4,\n    // elevation: 1,\n  },\n  name: {\n    marginTop: 2,\n  },\n  moreBtn: {\n    marginLeft: 10,\n    flexDirection: 'row',\n    alignItems: 'center',\n    // justifyContent: 'center',\n    gap: 8,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Theme/index.tsx",
    "content": "import { memo } from 'react'\n\n// import Section from '../../components/Section'\nimport Theme from './Theme'\nimport IsAutoTheme from './IsAutoTheme'\nimport IsHideBgDark from './IsHideBgDark'\nimport IsDynamicBg from './IsDynamicBg'\nimport IsFontShadow from './IsFontShadow'\n// import { useI18n } from '@/lang/i18n'\n\nexport default memo(() => {\n  return (\n    <>\n      <Theme />\n      <IsAutoTheme />\n      <IsHideBgDark />\n      <IsDynamicBg />\n      <IsFontShadow />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/Setting/settings/Version.tsx",
    "content": "import { memo, useState, useEffect } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\nimport Section from '../components/Section'\nimport SubTitle from '../components/SubTitle'\nimport Button from '../components/Button'\nimport { sizeFormate } from '@/utils'\n\nimport { useI18n } from '@/lang'\nimport { useVersionDownloadProgressUpdated, useVersionInfo } from '@/store/version/hook'\nimport Text from '@/components/common/Text'\nimport { showModal } from '@/core/version'\n\nconst currentVer = process.versions.app\nexport default memo(() => {\n  const t = useI18n()\n  const versionInfo = useVersionInfo()\n  // const versionStatus = useVrsionUpdateStatus()\n  const [title, setTitle] = useState('')\n  const [tip, setTip] = useState('')\n  const progress = useVersionDownloadProgressUpdated()\n  const handleOpenVersionModal = () => {\n    // setVersionInfo({ showModal: true })\n    showModal()\n  }\n\n  useEffect(() => {\n    if (versionInfo.isLatest) {\n      setTitle(t('version_tip_latest'))\n      setTip('')\n    } else if (versionInfo.isUnknown) {\n      setTitle(t('version_title_unknown'))\n      setTip(t('version_tip_unknown'))\n    } else {\n      switch (versionInfo.status) {\n        case 'downloading':\n          setTitle(t('version_title_new'))\n          setTip(t('version_btn_downloading', {\n            total: sizeFormate(progress.total),\n            current: sizeFormate(progress.current),\n            progress: progress.total ? (progress.current / progress.total * 100).toFixed(2) : '0',\n          }))\n          break\n        case 'downloaded':\n          setTitle(t('version_title_update'))\n          setTip('')\n          break\n        case 'checking':\n          setTitle(t('version_title_checking'))\n          setTip('')\n          break\n        case 'error':\n          setTitle(t('version_title_failed'))\n          setTip(t('version_tip_failed'))\n          break\n        // case 'idle':\n        //   break\n        default:\n          setTitle(t('version_title_new'))\n          setTip('')\n          break\n      }\n    }\n  }, [t, versionInfo, progress])\n\n  return (\n    <Section title={t('setting_version')}>\n      <SubTitle title={title}>\n        <View style={styles.desc}>\n          <Text size={14}>{t('version_label_latest_ver')}{versionInfo.newVersion?.version}</Text>\n          <Text size={14}>{t('version_label_current_ver')}{currentVer}</Text>\n          {\n            tip ? <Text size={14}>{tip}</Text> : null\n          }\n        </View>\n        <View style={styles.btn}>\n          <Button onPress={handleOpenVersionModal}>{t('setting_version_show_ver_modal')}</Button>\n        </View>\n      </SubTitle>\n    </Section>\n  )\n})\n\nconst styles = StyleSheet.create({\n  desc: {\n    marginBottom: 8,\n  },\n  btn: {\n    flexDirection: 'row',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/Content.tsx",
    "content": "import { getSongListSetting, saveSongListSetting } from '@/utils/data'\nimport { useEffect, useRef } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\n// import List from './List/List'\nimport HeaderBar, { type HeaderBarProps, type HeaderBarType } from './HeaderBar'\nimport songlistState, { type InitState, type SortInfo } from '@/store/songlist/state'\nimport List, { type ListType } from './List'\n\n\ninterface SonglistInfo {\n  source: InitState['sources'][number]\n  sortId: SortInfo['id']\n  tagId: string\n}\n\nexport default () => {\n  const headerBarRef = useRef<HeaderBarType>(null)\n  const listRef = useRef<ListType>(null)\n  const songlistInfo = useRef<SonglistInfo>({ source: 'kw', sortId: '5', tagId: '' })\n\n  useEffect(() => {\n    void getSongListSetting().then(info => {\n      songlistInfo.current.source = info.source\n      songlistInfo.current.sortId = info.sortId\n      songlistInfo.current.tagId = info.tagId\n      headerBarRef.current?.setSource(info.source, info.sortId, info.tagName, info.tagId)\n      listRef.current?.loadList(info.source, info.sortId, info.tagId)\n    })\n  }, [])\n\n  const handleSortChange: HeaderBarProps['onSortChange'] = (id) => {\n    songlistInfo.current.sortId = id\n    void saveSongListSetting({ sortId: id })\n    listRef.current?.loadList(songlistInfo.current.source, id, songlistInfo.current.tagId)\n  }\n\n  const handleTagChange: HeaderBarProps['onTagChange'] = (name, id) => {\n    songlistInfo.current.tagId = id\n    void saveSongListSetting({ tagName: name, tagId: id })\n    listRef.current?.loadList(songlistInfo.current.source, songlistInfo.current.sortId, id)\n  }\n\n  const handleSourceChange: HeaderBarProps['onSourceChange'] = (source) => {\n    songlistInfo.current.source = source\n    songlistInfo.current.tagId = ''\n    songlistInfo.current.sortId = songlistState.sortList[source]![0].id\n    void saveSongListSetting({ sortId: songlistInfo.current.sortId, source, tagId: '', tagName: '' })\n    headerBarRef.current?.setSource(source, songlistInfo.current.sortId, '', songlistInfo.current.tagId)\n    listRef.current?.loadList(source, songlistInfo.current.sortId, songlistInfo.current.tagId)\n  }\n\n  return (\n    <View style={styles.container}>\n      <HeaderBar\n        ref={headerBarRef}\n        onSortChange={handleSortChange}\n        onTagChange={handleTagChange}\n        onSourceChange={handleSourceChange}\n      />\n      <List ref={listRef} />\n    </View>\n  )\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    position: 'relative',\n    flex: 1,\n  },\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/OpenList/Modal.tsx",
    "content": "import { useRef, useImperativeHandle, forwardRef, useState } from 'react'\nimport ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'\nimport Text from '@/components/common/Text'\nimport { View } from 'react-native'\nimport Input, { type InputType } from '@/components/common/Input'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\n// import SourceSelector, { type SourceSelectorProps, type SourceSelectorType } from '../SourceSelector'\nimport { type Source } from '@/store/songlist/state'\n\ninterface IdInputType {\n  setText: (text: string) => void\n  getText: () => string\n  focus: () => void\n}\nconst IdInput = forwardRef<IdInputType, {}>((props, ref) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const [text, setText] = useState('')\n  const inputRef = useRef<InputType>(null)\n\n  useImperativeHandle(ref, () => ({\n    getText() {\n      return text.trim()\n    },\n    setText(text) {\n      setText(text)\n    },\n    focus() {\n      inputRef.current?.focus()\n    },\n  }))\n\n  return (\n    <Input\n      ref={inputRef}\n      placeholder={t('songlist_open_input_placeholder')}\n      value={text}\n      onChangeText={setText}\n      style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}\n    />\n  )\n})\n\n\nexport interface ModalProps {\n  onOpenId: (id: string) => void\n  // onSourceChange: SourceSelectorProps['onSourceChange']\n}\nexport interface ModalType {\n  show: (source: Source) => void\n}\n\nexport default forwardRef<ModalType, ModalProps>(({ onOpenId }, ref) => {\n  const alertRef = useRef<ConfirmAlertType>(null)\n  // const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  const inputRef = useRef<IdInputType>(null)\n  const [visible, setVisible] = useState(false)\n  const theme = useTheme()\n  const t = useI18n()\n\n  const handleShow = (source: Source) => {\n    alertRef.current?.setVisible(true)\n    requestAnimationFrame(() => {\n      inputRef.current?.setText('')\n      // sourceSelectorRef.current?.setSource(source)\n      setTimeout(() => {\n        inputRef.current?.focus()\n      }, 300)\n    })\n  }\n  useImperativeHandle(ref, () => ({\n    show(source) {\n      if (visible) handleShow(source)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          handleShow(source)\n        })\n      }\n    },\n  }))\n\n  const handleConfirm = () => {\n    let id = inputRef.current?.getText() ?? ''\n    if (!id.length) return\n    if (id.length > 500) id = id.substring(0, 500)\n    alertRef.current?.setVisible(false)\n    onOpenId(id)\n  }\n\n  return (\n    visible\n      ? <ConfirmAlert\n          ref={alertRef}\n          onConfirm={handleConfirm}\n        >\n          <View style={styles.content}>\n            <View style={styles.col}>\n              {/* <SourceSelector style={{ ...styles.selector, backgroundColor: theme['c-primary-input-background'] }} ref={sourceSelectorRef} onSourceChange={onSourceChange} /> */}\n              <IdInput ref={inputRef} />\n            </View>\n            <Text style={styles.inputTipText} size={13} color={theme['c-600']}>{t('songlist_open_input_tip')}</Text>\n          </View>\n        </ConfirmAlert>\n      : null\n  )\n})\n\n\nconst styles = createStyle({\n  content: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'column',\n  },\n  col: {\n    flexDirection: 'row',\n    height: 38,\n  },\n  // selector: {\n  //   borderTopLeftRadius: 4,\n  //   borderBottomLeftRadius: 4,\n  // },\n  input: {\n    flexGrow: 1,\n    flexShrink: 1,\n    minWidth: 290,\n    // borderRadius: 4,\n    // borderTopRightRadius: 4,\n    // borderBottomRightRadius: 4,\n    // paddingTop: 2,\n    // paddingBottom: 2,\n    height: '100%',\n  },\n  inputTipText: {\n    marginTop: 15,\n    // lineHeight: 18,\n  },\n})\n\n\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/OpenList/index.tsx",
    "content": "import { useRef, forwardRef, useImperativeHandle } from 'react'\n// import { Icon } from '@/components/common/Icon'\nimport Button from '@/components/common/Button'\n// import { navigations } from '@/navigation'\nimport Modal, { type ModalType } from './Modal'\nimport { type Source } from '@/store/songlist/state'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { navigations } from '@/navigation'\nimport commonState from '@/store/common/state'\n\n// export interface OpenListProps {\n//   onTagChange: (name: string, id: string) => void\n// }\n\nexport interface OpenListType {\n  setInfo: (source: Source) => void\n}\n\nexport default forwardRef<OpenListType, {}>((props, ref) => {\n  const t = useI18n()\n  const modalRef = useRef<ModalType>(null)\n  const songlistInfoRef = useRef<{ source: Source }>({ source: 'kw' })\n\n  useImperativeHandle(ref, () => ({\n    setInfo(source) {\n      songlistInfoRef.current.source = source\n    },\n  }))\n\n  const handleOpenSonglist = (id: string) => {\n    // console.log(id, songlistInfoRef.current.source)\n    navigations.pushSonglistDetailScreen(commonState.componentIds.home!, {\n      play_count: undefined,\n      id,\n      author: '',\n      name: '',\n      img: undefined,\n      desc: undefined,\n      source: songlistInfoRef.current.source,\n    })\n  }\n\n  // const handleSourceChange: ModalProps['onSourceChange'] = (source) => {\n  //   songlistInfoRef.current.source = source\n  // }\n\n\n  return (\n    <>\n      <Button style={styles.button} onPress={() => modalRef.current?.show(songlistInfoRef.current.source)}>\n        <Text>{t('songlist_open')}</Text>\n      </Button>\n      <Modal ref={modalRef} onOpenId={handleOpenSonglist} />\n    </>\n  )\n})\n\nconst styles = createStyle({\n  button: {\n    // backgroundColor: '#ccc',\n    alignItems: 'center',\n    justifyContent: 'center',\n    paddingLeft: 12,\n    paddingRight: 12,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/SortTab.tsx",
    "content": "import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'\nimport { ScrollView, TouchableOpacity } from 'react-native'\nimport songlistState, { type SortInfo, type Source } from '@/store/songlist/state'\nimport { useI18n } from '@/lang'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { BorderWidths } from '@/theme'\n\nexport interface SortTabProps {\n  onSortChange: (id: string) => void\n}\n\nexport interface SortTabType {\n  setSource: (source: Source, activeTab: SortInfo['id']) => void\n}\n\n\nexport default forwardRef<SortTabType, SortTabProps>(({ onSortChange }, ref) => {\n  const [sortList, setSortList] = useState<SortInfo[]>([])\n  const [activeId, setActiveId] = useState<SortInfo['id']>('')\n  const t = useI18n()\n  const theme = useTheme()\n  const scrollViewRef = useRef<ScrollView>(null)\n\n  useImperativeHandle(ref, () => ({\n    setSource(source, activeTab) {\n      scrollViewRef.current?.scrollTo({ x: 0 })\n      setSortList(songlistState.sortList[source]!)\n      setActiveId(activeTab)\n    },\n  }))\n\n  const sorts = useMemo(() => {\n    return sortList.map(s => ({ label: t(`songlist_${s.tid}`), id: s.id }))\n  }, [sortList, t])\n\n  const handleSortChange = (id: string) => {\n    onSortChange(id)\n    setActiveId(id)\n  }\n\n  return (\n    <ScrollView ref={scrollViewRef} style={styles.container} keyboardShouldPersistTaps={'always'} horizontal>\n      {\n        sorts.map(s => (\n          <TouchableOpacity style={styles.button} onPress={() => { handleSortChange(s.id) }} key={s.id}>\n            <Text style={{ ...styles.buttonText, borderBottomColor: activeId == s.id ? theme['c-primary-background-active'] : 'transparent' }} color={activeId == s.id ? theme['c-primary-font-active'] : theme['c-font']}>{s.label}</Text>\n          </TouchableOpacity>\n        ))\n      }\n    </ScrollView>\n  )\n})\n\n\nconst styles = createStyle({\n  container: {\n    flexGrow: 1,\n    flexShrink: 1,\n    // paddingLeft: 5,\n    // paddingRight: 5,\n  },\n  button: {\n    // height: 38,\n    // lineHeight: 38,\n    justifyContent: 'center',\n    paddingLeft: 14,\n    paddingRight: 14,\n    // width: 80,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  buttonText: {\n    // height: 38,\n    // lineHeight: 38,\n    textAlign: 'center',\n    paddingHorizontal: 2,\n    paddingVertical: 3,\n    borderBottomWidth: BorderWidths.normal3,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/SourceSelector.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { StyleSheet, View, type ViewStyle } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport SourceSelector, {\n  type SourceSelectorType as _SourceSelectorType,\n  type SourceSelectorProps as _SourceSelectorProps,\n} from '@/components/SourceSelector'\nimport songlistState, { type Source, type InitState } from '@/store/songlist/state'\n\ntype Sources = Readonly<InitState['sources']>\ntype SourceSelectorCommonProps = _SourceSelectorProps<Sources>\ntype SourceSelectorCommonType = _SourceSelectorType<Sources>\n\nexport interface SourceSelectorProps {\n  onSourceChange: SourceSelectorCommonProps['onSourceChange']\n  style?: ViewStyle\n}\n\nexport interface SourceSelectorType {\n  setSource: (source: Source) => void\n}\n\nexport default forwardRef<SourceSelectorType, SourceSelectorProps>(({ style, onSourceChange }, ref) => {\n  const sourceSelectorRef = useRef<SourceSelectorCommonType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setSource(source) {\n      sourceSelectorRef.current?.setSourceList(songlistState.sources, source)\n    },\n  }), [])\n\n\n  return (\n    <View style={StyleSheet.compose<ViewStyle>(styles.selector, style)}>\n      <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} center />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  selector: {\n    // width: 86,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/Tag/CurrentTagBtn.tsx",
    "content": "import Button from '@/components/common/Button'\nimport Text from '@/components/common/Text'\nimport { useI18n } from '@/lang'\nimport { createStyle } from '@/utils/tools'\nimport { forwardRef, useImperativeHandle, useState } from 'react'\n\n\nexport interface CurrentTagBtnProps {\n  onShowList: () => void\n}\n\nexport interface CurrentTagBtnType {\n  setCurrentTagInfo: (name: string) => void\n}\n\nexport default forwardRef<CurrentTagBtnType, CurrentTagBtnProps>(({ onShowList }, ref) => {\n  const t = useI18n()\n  const [name, setName] = useState('')\n\n  useImperativeHandle(ref, () => ({\n    setCurrentTagInfo(name) {\n      if (!name) name = t('songlist_tag_default')\n      setName(name)\n    },\n  }))\n\n  return (\n    <Button style={styles.btn} onPress={onShowList}>\n      <Text style={styles.sourceMenu}>{name}</Text>\n    </Button>\n  )\n})\n\n\nconst styles = createStyle({\n  btn: {\n    paddingLeft: 15,\n    paddingRight: 15,\n    justifyContent: 'center',\n  },\n  sourceMenu: {\n    // height: 38,\n    // lineHeight: 38,\n    textAlign: 'center',\n    // minWidth: 70,\n    // paddingTop: 10,\n    // paddingBottom: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/Tag/index.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\n\n// import TagPopup, { type TagPopupProps, type TagPopupType } from './TagPopup'\nimport CurrentTagBtn, { type CurrentTagBtnType } from './CurrentTagBtn'\nimport { type Source } from '@/store/songlist/state'\n\n\nexport interface TagProps {\n  onTagChange: (name: string, id: string) => void\n}\n\nexport interface TagType {\n  setSelectedTagInfo: (source: Source, name: string, activeId: string) => void\n}\n\nexport default forwardRef<TagType, TagProps>(({ onTagChange }, ref) => {\n  // console.log('render tag btn')\n  const currentTagBtnRef = useRef<CurrentTagBtnType>(null)\n  // const tagPopupRef = useRef<TagPopupType>(null)\n  const tagInfoRef = useRef<{ source: Source, activeId: string }>({ source: 'kw', activeId: '' })\n\n  useEffect(() => {\n    const handleChange = (name: string, id: string) => {\n      onTagChange(name, id)\n      tagInfoRef.current.activeId = id\n      currentTagBtnRef.current?.setCurrentTagInfo(name)\n    }\n\n    global.app_event.on('songlistTagInfoChange', handleChange)\n    return () => {\n      global.app_event.off('songlistTagInfoChange', handleChange)\n    }\n  }, [onTagChange])\n\n  useImperativeHandle(ref, () => ({\n    setSelectedTagInfo(source, name, activeId) {\n      tagInfoRef.current.activeId = activeId\n      tagInfoRef.current.source = source\n      currentTagBtnRef.current?.setCurrentTagInfo(name)\n    },\n  }))\n\n  const handleShowList = () => {\n    global.app_event.showSonglistTagList(tagInfoRef.current.source, tagInfoRef.current.activeId)\n  }\n\n  // const handleChangeTag: TagProps['onTagChange'] = (name, id) => {\n  //   tagInfoRef.current.activeId = id\n  //   onTagChange(name, id)\n  //   currentTagBtnRef.current?.setCurrentTagInfo(name)\n  // }\n\n  return <CurrentTagBtn ref={currentTagBtnRef} onShowList={handleShowList} />\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/HeaderBar/index.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef } from 'react'\nimport { View } from 'react-native'\n\n// import { useGetter, useDispatch } from '@/store'\nimport SortTab, { type SortTabProps, type SortTabType } from './SortTab'\n// import Tag from './Tag'\n// import OpenList from './OpenList'\nimport { createStyle } from '@/utils/tools'\n// import { BorderWidths } from '@/theme'\nimport SourceSelector, {\n  type SourceSelectorType,\n  type SourceSelectorProps,\n} from './SourceSelector'\nimport { type Source } from '@/store/songlist/state'\n// import { useTheme } from '@/store/theme/hook'\nimport Tag, { type TagType, type TagProps } from './Tag'\nimport OpenList, { type OpenListType } from './OpenList'\n// import { BorderWidths } from '@/theme'\n\nexport interface HeaderBarProps {\n  onSortChange: SortTabProps['onSortChange']\n  onTagChange: TagProps['onTagChange']\n  onSourceChange: SourceSelectorProps['onSourceChange']\n}\n\nexport interface HeaderBarType {\n  setSource: (source: Source, sortId: string, tagName: string, tagId: string) => void\n}\n\n\nexport default forwardRef<HeaderBarType, HeaderBarProps>(({ onSortChange, onTagChange, onSourceChange }, ref) => {\n  const sortTabRef = useRef<SortTabType>(null)\n  const tagRef = useRef<TagType>(null)\n  const openListRef = useRef<OpenListType>(null)\n  const sourceSelectorRef = useRef<SourceSelectorType>(null)\n  // const theme = useTheme()\n\n  useImperativeHandle(ref, () => ({\n    setSource(source, sortId, tagName, tagId) {\n      sortTabRef.current?.setSource(source, sortId)\n      tagRef.current?.setSelectedTagInfo(source, tagName, tagId)\n      sourceSelectorRef.current?.setSource(source)\n      openListRef.current?.setInfo(source)\n    },\n  }), [])\n\n\n  return (\n    <View style={styles.searchBar}>\n      <SortTab ref={sortTabRef} onSortChange={onSortChange} />\n      <Tag ref={tagRef} onTagChange={onTagChange} />\n      <OpenList ref={openListRef} />\n      <SourceSelector ref={sourceSelectorRef} onSourceChange={onSourceChange} />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  searchBar: {\n    flexDirection: 'row',\n    height: 38,\n    zIndex: 2,\n    // paddingRight: 10,\n    // borderBottomWidth: BorderWidths.normal,\n  },\n  selector: {\n    width: 86,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/List.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'\nimport Songlist, { type SonglistProps, type SonglistType } from './components/Songlist'\nimport { clearList, getList, setList, setListInfo } from '@/core/songlist'\nimport songlistState from '@/store/songlist/state'\nimport { type Source } from '@/store/songlist/state'\n\n\nexport interface ListType {\n  loadList: (source: Source, sortId: string, tagId: string) => void\n}\n\nexport default forwardRef<ListType, {}>((props, ref) => {\n  const listRef = useRef<SonglistType>(null)\n  const isUnmountedRef = useRef(false)\n  useImperativeHandle(ref, () => ({\n    async loadList(source, sortId, tagId) {\n      const listInfo = songlistState.listInfo\n      listRef.current?.setList([])\n      if (listInfo.tagId == tagId && listInfo.sortId == sortId && listInfo.source == source && listInfo.list.length) {\n        requestAnimationFrame(() => {\n          listRef.current?.setList(listInfo.list)\n        })\n      } else {\n        listRef.current?.setStatus('loading')\n        setListInfo(source, tagId, sortId)\n        const page = 1\n        return getList(source, tagId, sortId, page).then((info) => {\n          const result = setList(info, tagId, sortId, page)\n          if (isUnmountedRef.current) return\n          requestAnimationFrame(() => {\n            listRef.current?.setList(result.list)\n            listRef.current?.setStatus(songlistState.listInfo.maxPage <= page ? 'end' : 'idle')\n          })\n        }).catch(() => {\n          if (songlistState.listInfo.list.length && page == 1) clearList()\n          listRef.current?.setStatus('error')\n        })\n      }\n    },\n  }), [])\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  const handleRefresh: SonglistProps['onRefresh'] = () => {\n    const page = 1\n    listRef.current?.setStatus('refreshing')\n    getList(songlistState.listInfo.source, songlistState.listInfo.tagId, songlistState.listInfo.sortId, page, true).then((info) => {\n      const result = setList(info, songlistState.listInfo.tagId, songlistState.listInfo.sortId, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list)\n      listRef.current?.setStatus(songlistState.listInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (songlistState.listInfo.list.length && page == 1) clearList()\n      listRef.current?.setStatus('error')\n    })\n  }\n  const handleLoadMore: SonglistProps['onLoadMore'] = () => {\n    listRef.current?.setStatus('loading')\n    const page = songlistState.listInfo.list.length ? songlistState.listInfo.page + 1 : 1\n    getList(songlistState.listInfo.source, songlistState.listInfo.tagId, songlistState.listInfo.sortId, page).then((info) => {\n      const result = setList(info, songlistState.listInfo.tagId, songlistState.listInfo.sortId, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list)\n      listRef.current?.setStatus(songlistState.listInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (songlistState.listInfo.list.length && page == 1) clearList()\n      listRef.current?.setStatus('error')\n    })\n  }\n\n  return <Songlist\n    ref={listRef}\n    onRefresh={handleRefresh}\n    onLoadMore={handleLoadMore}\n   />\n})\n\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/TagList/List.tsx",
    "content": "import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'\nimport { View, ScrollView } from 'react-native'\n\nimport { createStyle } from '@/utils/tools'\nimport TagGroup, { type TagGroupProps } from './TagGroup'\nimport { useI18n } from '@/lang'\nimport { type TagInfo, type Source } from '@/store/songlist/state'\nimport { getTags } from '@/core/songlist'\nimport Text from '@/components/common/Text'\n// import { BorderWidths } from '@/theme'\n\nexport interface ListProps {\n  onTagChange: TagGroupProps['onTagChange']\n}\n\nexport interface ListType {\n  loadTag: (source: Source, activeId: string) => void\n}\n\nexport default forwardRef<ListType, ListProps>(({ onTagChange }, ref) => {\n  // const theme = useTheme()\n  const [activeId, setActiveId] = useState('')\n  const [list, setList] = useState<TagInfo['tags']>([])\n  const t = useI18n()\n  const prevSource = useRef('')\n\n  const isUnmountedRef = useRef(false)\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  useImperativeHandle(ref, () => ({\n    loadTag(source, id) {\n      if (id != activeId) setActiveId(id)\n      if (source != prevSource.current) {\n        setList([{ name: '', list: [{ name: t('songlist_tag_default'), id: '', parent_id: '', parent_name: '', source }] }])\n        void getTags(source).then(tagInfo => {\n          if (isUnmountedRef.current) return\n          prevSource.current = source\n          setList([\n            { name: '', list: [{ name: t('songlist_tag_default'), id: '', parent_id: '', parent_name: '', source }] },\n            { name: t('songlist_tag_hot'), list: [...tagInfo.hotTag] },\n            ...tagInfo.tags,\n          ].filter(t => t.list.length))\n        })\n      }\n    },\n  }))\n\n\n  return (\n    <ScrollView style={{ flexShrink: 1, flexGrow: 0 }} keyboardShouldPersistTaps={'always'}>\n      <View style={styles.tagContainer} onStartShouldSetResponder={() => true}>\n        {\n          list.map((type, index) => (\n            <TagGroup\n              key={index}\n              name={type.name}\n              list={type.list}\n              activeId={activeId}\n              onTagChange={onTagChange}\n            />\n          ))\n        }\n        {\n          list.length == 1\n            ? (\n                <View style={styles.blankView}>\n                  <Text>{t('list_loading')}</Text>\n                </View>\n              )\n            : null\n        }\n      </View>\n    </ScrollView>\n  )\n})\n\n\nconst styles = createStyle({\n  tagContainer: {\n    paddingTop: 15,\n    paddingLeft: 15,\n    paddingBottom: 15,\n  },\n  blankView: {\n    paddingTop: '15%',\n    paddingBottom: '15%',\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/TagList/TagGroup.tsx",
    "content": "import { View } from 'react-native'\n\nimport Button from '@/components/common/Button'\nimport { type TagInfoItem } from '@/store/songlist/state'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\nexport interface TagGroupProps {\n  name: string\n  list: TagInfoItem[]\n  onTagChange: (name: string, id: string) => void\n  activeId: string\n}\n\nexport default ({ name, list, onTagChange, activeId }: TagGroupProps) => {\n  const theme = useTheme()\n  return (\n    <View>\n      {\n        name\n          ? <Text style={styles.tagTypeTitle} color={theme['c-font-label']}>{name}</Text>\n          : null\n      }\n      <View style={styles.tagTypeList}>\n        {list.map(item => (\n          activeId == item.id\n            ? (\n                <View style={{ ...styles.tagButton, backgroundColor: theme['c-button-background'] }} key={item.id}>\n                  <Text style={styles.tagButtonText} color={theme['c-primary-font-active']}>{item.name}</Text>\n                </View>\n              )\n            : (\n                <Button\n                  style={{ ...styles.tagButton, backgroundColor: theme['c-button-background'] }}\n                  key={item.id}\n                  onPress={() => { onTagChange(item.name, item.id) }}\n                >\n                  <Text style={styles.tagButtonText} color={theme['c-font']} >{item.name}</Text>\n                </Button>\n              )\n\n        ))}\n      </View>\n    </View>\n  )\n}\n\nconst styles = createStyle({\n  tagTypeTitle: {\n    marginTop: 15,\n    marginBottom: 10,\n  },\n  tagTypeList: {\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n  },\n  tagButton: {\n    // marginRight: 10,\n    borderRadius: 4,\n    marginRight: 10,\n    marginBottom: 10,\n  },\n  tagButtonText: {\n    fontSize: 13,\n    paddingLeft: 12,\n    paddingRight: 12,\n    paddingTop: 8,\n    paddingBottom: 8,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/TagList/index.tsx",
    "content": "import { useEffect, useRef, useState } from 'react'\n\nimport { type Source } from '@/store/songlist/state'\nimport List, { type ListProps, type ListType } from './List'\n\n\nexport default () => {\n  const [visible, setVisible] = useState(false)\n  const listRef = useRef<ListType>(null)\n  // const [info, setInfo] = useState({ souce: 'kw', activeId: '' })\n\n\n  useEffect(() => {\n    let isInited = false\n    const handleShow = (source: Source, id: string) => {\n      if (isInited) {\n        listRef.current?.loadTag(source, id)\n      } else {\n        requestAnimationFrame(() => {\n          setVisible(true)\n          requestAnimationFrame(() => {\n            listRef.current?.loadTag(source, id)\n          })\n        })\n        isInited = true\n      }\n    }\n    global.app_event.on('showSonglistTagList', handleShow)\n\n    return () => {\n      global.app_event.off('showSonglistTagList', handleShow)\n    }\n  }, [])\n\n  const handleTagChange: ListProps['onTagChange'] = (name, id) => {\n    global.app_event.hideSonglistTagList()\n    requestAnimationFrame(() => {\n      global.app_event.songlistTagInfoChange(name, id)\n    })\n  }\n\n  return (\n    visible\n      ? <List ref={listRef} onTagChange={handleTagChange} />\n      : null\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/components/Songlist/List.tsx",
    "content": "import { useRef, useState, useMemo, forwardRef, useImperativeHandle } from 'react'\nimport { FlatList, View, RefreshControl, type FlatListProps } from 'react-native'\n\nimport ListItem from './ListItem'\n// import { navigations } from '@/navigation'\nimport { type ListInfoItem } from '@/store/songlist/state'\nimport { useLayout } from '@/utils/hooks'\nimport { useTheme } from '@/store/theme/hook'\nimport { useI18n } from '@/lang'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\ntype FlatListType = FlatListProps<ListInfoItem>\n\n// const MAX_WIDTH = scaleSizeW(110)\nconst MIN_WIDTH = scaleSizeW(110)\nconst GAP = scaleSizeW(20)\n\nexport interface ListProps {\n  onRefresh: () => void\n  onLoadMore: () => void\n  onOpenDetail: (item: ListInfoItem, index: number) => void\n}\nexport type Status = 'loading' | 'refreshing' | 'end' | 'error' | 'idle'\n\nexport interface ListType {\n  setList: (list: ListInfoItem[], showSource?: boolean) => void\n  setStatus: (val: Status) => void\n}\n\nexport default forwardRef<ListType, ListProps>(({ onRefresh, onLoadMore, onOpenDetail }, ref) => {\n  const flatListRef = useRef<FlatList>(null)\n  const [currentList, setList] = useState<ListInfoItem[]>([])\n  const [showSource, setShowSource] = useState(false)\n  const [status, setStatus] = useState<Status>('idle')\n  const { onLayout, width } = useLayout()\n  const theme = useTheme()\n  // console.log('render songlist')\n\n  useImperativeHandle(ref, () => ({\n    setList(list, showSource = false) {\n      // rawListRef.current = list\n      setList(list)\n      setShowSource(showSource)\n    },\n    setStatus(val) {\n      setStatus(val)\n    },\n  }))\n\n  const handleLoadMore = () => {\n    if (status != 'idle') return\n    onLoadMore()\n  }\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => (\n    <ListItem\n      item={item}\n      index={index}\n      width={rowInfo.width}\n      showSource={showSource}\n      onPress={onOpenDetail}\n    />\n  )\n  const getkey: FlatListType['keyExtractor'] = item => item.id\n  // const getItemLayout: FlatListType['getItemLayout'] = (data, index) => {\n  //   return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }\n  // }\n  const refreshControl = useMemo(() => (\n    <RefreshControl\n      colors={[theme['c-primary']]}\n      // progressBackgroundColor={theme.primary}\n      refreshing={status == 'refreshing'}\n      onRefresh={onRefresh} />\n  ), [status, onRefresh, theme])\n  const footerComponent = useMemo(() => {\n    let label: FooterLabel\n    switch (status) {\n      case 'refreshing': return null\n      case 'loading':\n        label = 'list_loading'\n        break\n      case 'end':\n        label = 'list_end'\n        break\n      case 'error':\n        label = 'list_error'\n        break\n      case 'idle':\n        label = null\n        break\n    }\n    return (\n      <View style={{ width: '100%' }}>\n        <Footer label={label} onLoadMore={onLoadMore} />\n      </View>\n    )\n  }, [onLoadMore, status])\n\n\n  // const itemWidth = useMemo(() => {\n  //   let itemWidth = Math.max(Math.trunc(width * 0.125), MAX_WIDTH)\n  //   // if (itemWidth < )\n  // }, [width])\n  // const computedItemWidth = useMemo(() => {\n  //   let w = width - GAP\n  //   let n = width / (MIN_WIDTH + GAP)\n  //   if (n > 10) n = 10\n  //   return Math.floor(w / n)\n  // }, [width])\n  // console.log(Math.trunc(width * 0.125), itemWidth)\n  // console.log(itemWidth, MIN_WIDTH, GAP, width)\n  const rowInfo = useMemo(() => {\n    let w = width - GAP\n    let n = width / (MIN_WIDTH + GAP)\n    if (n > 10) n = 10\n    let computedItemWidth = Math.floor(w / n)\n    const num = Math.max(Math.floor(width / computedItemWidth), 2)\n    return {\n      num,\n      width: (width - GAP) / num,\n    }\n  }, [width])\n  // console.log(rowNum)\n  const list = useMemo(() => {\n    const list = [...currentList]\n    let whiteItemNum = (list.length % rowInfo.num)\n    if (whiteItemNum > 0) whiteItemNum = rowInfo.num - whiteItemNum\n    for (let i = 0; i < whiteItemNum; i++) {\n      list.push({\n        id: `white__${i}`,\n        play_count: '',\n        author: '',\n        name: '',\n        img: '',\n        desc: '',\n        // @ts-expect-error\n        source: '',\n      })\n    }\n    return list\n  }, [currentList, rowInfo])\n  // console.log(listInfo.list.map((item) => item.id))\n\n  return (\n    <View style={styles.container} onLayout={onLayout}>\n      {\n        width == 0\n          ? null\n          : (\n              <FlatList\n                key={String(rowInfo.num)}\n                ref={flatListRef}\n                style={styles.list}\n                columnWrapperStyle={{ justifyContent: 'space-evenly' }}\n                numColumns={rowInfo.num}\n                data={list}\n                maxToRenderPerBatch={4}\n                // updateCellsBatchingPeriod={80}\n                windowSize={8}\n                removeClippedSubviews={true}\n                // initialNumToRender={12}\n                renderItem={renderItem}\n                keyExtractor={getkey}\n                // getItemLayout={getItemLayout}\n                // onRefresh={onRefresh}\n                // refreshing={refreshing}\n                onEndReachedThreshold={0.6}\n                onEndReached={handleLoadMore}\n                refreshControl={refreshControl}\n                ListFooterComponent={footerComponent}\n              />\n            )\n      }\n    </View>\n  )\n})\n\ntype FooterLabel = 'list_loading' | 'list_end' | 'list_error' | null\nconst Footer = ({ label, onLoadMore }: {\n  label: FooterLabel\n  onLoadMore: () => void\n}) => {\n  const theme = useTheme()\n  const t = useI18n()\n  const handlePress = () => {\n    if (label != 'list_error') return\n    onLoadMore()\n  }\n  return (\n    label\n      ? (\n          <View>\n            <Text onPress={handlePress} style={styles.footer} color={theme['c-font-label']}>{t(label)}</Text>\n          </View>\n        )\n      : null\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    overflow: 'hidden',\n  },\n  list: {\n    flex: 1,\n    paddingLeft: 10,\n    paddingRight: 10,\n  },\n  footer: {\n    textAlign: 'center',\n    padding: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/components/Songlist/ListItem.tsx",
    "content": "import { memo } from 'react'\nimport { View, Platform, TouchableOpacity } from 'react-native'\nimport { createStyle } from '@/utils/tools'\nimport { type ListInfoItem } from '@/store/songlist/state'\nimport Text from '@/components/common/Text'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport { useTheme } from '@/store/theme/hook'\nimport Image from '@/components/common/Image'\n\nconst gap = scaleSizeW(15)\nexport default memo(({ item, index, width, showSource, onPress }: {\n  item: ListInfoItem\n  index: number\n  showSource: boolean\n  width: number\n  onPress: (item: ListInfoItem, index: number) => void\n}) => {\n  const theme = useTheme()\n  const itemWidth = width - gap\n  const handlePress = () => {\n    onPress(item, index)\n  }\n  return (\n    item.source\n      ? (\n          <View style={{ ...styles.listItem, width: itemWidth }}>\n            <View style={{ ...styles.listItemImg, backgroundColor: theme['c-content-background'] }}>\n              <TouchableOpacity activeOpacity={0.5} onPress={handlePress}>\n                <Image url={item.img} nativeID={`${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_from_${item.id}`} style={{ width: itemWidth, height: itemWidth, borderRadius: 4 }} />\n                { showSource ? <Text style={styles.sourceLabel} size={9} color=\"#fff\" >{item.source}</Text> : null }\n              </TouchableOpacity>\n            </View>\n            <TouchableOpacity activeOpacity={0.5} onPress={handlePress}>\n              <Text style={styles.listItemTitle} numberOfLines={ 2 }>{item.name}</Text>\n            </TouchableOpacity>\n            {/* <Text>{JSON.stringify(item)}</Text> */}\n          </View>\n        )\n      : <View style={{ ...styles.listItem, width: itemWidth }} />\n  )\n})\n\nconst styles = createStyle({\n  listItem: {\n    // width: 90,\n    margin: 10,\n  },\n  listItemImg: {\n    // backgroundColor: '#eee',\n    borderRadius: 4,\n    marginBottom: 5,\n    overflow: 'hidden',\n    ...Platform.select({\n      ios: {\n        shadowColor: '#000',\n        shadowOffset: {\n          width: 0,\n          height: 1,\n        },\n        shadowOpacity: 0.20,\n        shadowRadius: 1.41,\n      },\n      android: {\n        elevation: 2,\n      },\n    }),\n  },\n  sourceLabel: {\n    paddingLeft: 4,\n    paddingBottom: 2,\n    paddingRight: 4,\n    position: 'absolute',\n    top: 0,\n    right: 0,\n    borderBottomLeftRadius: 3,\n    backgroundColor: 'rgba(0, 0, 0, 0.3)',\n  },\n  listItemTitle: {\n    fontSize: 12,\n    // overflow: 'hidden',\n    marginBottom: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/components/Songlist/index.tsx",
    "content": "import { useRef, forwardRef, useImperativeHandle } from 'react'\nimport { type ListInfoItem } from '@/store/songlist/state'\n// import LoadingMask, { LoadingMaskType } from '@/components/common/LoadingMask'\nimport List, { type ListProps, type ListType, type Status } from './List'\nimport { navigations } from '@/navigation'\nimport commonState from '@/store/common/state'\n\nexport interface SonglistProps {\n  onRefresh: ListProps['onRefresh']\n  onLoadMore: ListProps['onLoadMore']\n}\nexport interface SonglistType {\n  setList: (list: ListInfoItem[], showSource?: boolean) => void\n  setStatus: (val: Status) => void\n}\n\nexport default forwardRef<SonglistType, SonglistProps>(({\n  onRefresh,\n  onLoadMore,\n}, ref) => {\n  const listRef = useRef<ListType>(null)\n  // const loadingMaskRef = useRef<LoadingMaskType>(null)\n\n  useImperativeHandle(ref, () => ({\n    setList(list, showSource) {\n      listRef.current?.setList(list, showSource)\n    },\n    setStatus(val) {\n      listRef.current?.setStatus(val)\n    },\n  }))\n\n  const handleOpenDetail = (item: ListInfoItem, index: number) => {\n    navigations.pushSonglistDetailScreen(commonState.componentIds.home!, item)\n  }\n\n  return (\n    <List\n      ref={listRef}\n      onRefresh={onRefresh}\n      onLoadMore={onLoadMore}\n      onOpenDetail={handleOpenDetail}\n    />\n  )\n})\n"
  },
  {
    "path": "src/screens/Home/Views/SongList/index.tsx",
    "content": "import { useEffect, useRef } from 'react'\nimport settingState from '@/store/setting/state'\nimport Content from './Content'\nimport TagList from './TagList'\nimport { useTheme } from '@/store/theme/hook'\nimport DrawerLayoutFixed, { type DrawerLayoutFixedType } from '@/components/common/DrawerLayoutFixed'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport type { InitState as CommonState } from '@/store/common/state'\n\nconst MAX_WIDTH = scaleSizeW(560)\n\nexport default () => {\n  const drawer = useRef<DrawerLayoutFixedType>(null)\n  const theme = useTheme()\n\n  useEffect(() => {\n    const handleFixDrawer = (id: CommonState['navActiveId']) => {\n      if (id == 'nav_songlist') drawer.current?.fixWidth()\n    }\n    const handleShow = () => {\n      requestAnimationFrame(() => {\n        drawer.current?.openDrawer()\n      })\n    }\n    const handleHide = () => {\n      drawer.current?.closeDrawer()\n    }\n\n    global.state_event.on('navActiveIdUpdated', handleFixDrawer)\n    global.app_event.on('showSonglistTagList', handleShow)\n    global.app_event.on('hideSonglistTagList', handleHide)\n\n    return () => {\n      global.state_event.off('navActiveIdUpdated', handleFixDrawer)\n      global.app_event.off('showSonglistTagList', handleShow)\n      global.app_event.off('hideSonglistTagList', handleHide)\n    }\n  }, [])\n\n  const navigationView = () => <TagList />\n  // console.log('render drawer content')\n\n  return (\n    <DrawerLayoutFixed\n      ref={drawer}\n      visibleNavNames={[COMPONENT_IDS.home]}\n      widthPercentage={0.8}\n      widthPercentageMax={MAX_WIDTH}\n      drawerPosition={settingState.setting['common.drawerLayoutPosition']}\n      renderNavigationView={navigationView}\n      drawerBackgroundColor={theme['c-content-background']}\n      style={{ elevation: 1 }}\n    >\n      <Content />\n    </DrawerLayoutFixed>\n  )\n}\n"
  },
  {
    "path": "src/screens/Home/index.tsx",
    "content": "import { useEffect } from 'react'\nimport { useHorizontalMode } from '@/utils/hooks'\nimport PageContent from '@/components/PageContent'\nimport { setComponentId } from '@/core/common'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport Vertical from './Vertical'\nimport Horizontal from './Horizontal'\nimport { navigations } from '@/navigation'\nimport settingState from '@/store/setting/state'\n\n\ninterface Props {\n  componentId: string\n}\n\n\nexport default ({ componentId }: Props) => {\n  const isHorizontalMode = useHorizontalMode()\n  useEffect(() => {\n    setComponentId(COMPONENT_IDS.home, componentId)\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n\n    if (settingState.setting['player.startupPushPlayDetailScreen']) {\n      navigations.pushPlayDetailScreen(componentId, true)\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <PageContent>\n      {\n        isHorizontalMode\n          ? <Horizontal />\n          : <Vertical />\n      }\n    </PageContent>\n  )\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Lyric.tsx",
    "content": "import { memo, useMemo, useEffect, useRef, useCallback } from 'react'\nimport { View, FlatList, type FlatListProps, type NativeSyntheticEvent, type NativeScrollEvent, type LayoutChangeEvent } from 'react-native'\n// import { useLayout } from '@/utils/hooks'\nimport { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric'\nimport { createStyle } from '@/utils/tools'\n// import { useComponentIds } from '@/store/common/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { AnimatedColorText } from '@/components/common/Text'\nimport { setSpText } from '@/utils/pixelRatio'\nimport playerState from '@/store/player/state'\nimport { scrollTo } from '@/utils/scroll'\nimport PlayLine, { type PlayLineType } from '../components/PlayLine'\n// import { screenkeepAwake } from '@/utils/nativeModules/utils'\n// import { log } from '@/utils/log'\n// import { toast } from '@/utils/tools'\n\ntype FlatListType = FlatListProps<Line>\n\ninterface LineProps {\n  line: Line\n  lineNum: number\n  activeLine: number\n  onLayout: (lineNum: number, height: number, width: number) => void\n}\nconst LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => {\n  const theme = useTheme()\n  const lrcFontSize = useSettingValue('playDetail.horizontal.style.lrcFontSize')\n  const textAlign = useSettingValue('playDetail.style.align')\n  const size = lrcFontSize / 10\n  const lineHeight = setSpText(size) * 1.3\n\n  const colors = useMemo(() => {\n    const active = activeLine == lineNum\n    return active ? [\n      theme['c-primary'],\n      theme['c-primary-alpha-200'],\n      1,\n    ] as const : [\n      theme['c-350'],\n      theme['c-300'],\n      0.6,\n    ] as const\n  }, [activeLine, lineNum, theme])\n\n  const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {\n    onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width)\n  }\n\n  // textBreakStrategy=\"simple\" 用于解决某些设备上字体被截断的问题\n  // https://stackoverflow.com/a/72822360\n  return (\n    <View style={styles.line} onLayout={handleLayout}>\n      <AnimatedColorText style={{\n        ...styles.lineText,\n        textAlign,\n        lineHeight,\n      }} textBreakStrategy=\"simple\" color={colors[0]} opacity={colors[2]} size={size}>{line.text}</AnimatedColorText>\n      {\n        line.extendedLyrics.map((lrc, index) => {\n          return (<AnimatedColorText style={{\n            ...styles.lineTranslationText,\n            textAlign,\n            lineHeight: lineHeight * 0.8,\n          }} textBreakStrategy=\"simple\" key={index} color={colors[1]} opacity={colors[2]} size={size * 0.8}>{lrc}</AnimatedColorText>)\n        })\n      }\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return prevProps.line === nextProps.line &&\n    prevProps.activeLine != nextProps.lineNum &&\n    nextProps.activeLine != nextProps.lineNum\n})\nconst wait = async() => new Promise(resolve => setTimeout(resolve, 100))\n\nexport default () => {\n  const lyricLines = useLrcSet()\n  const { line } = useLrcPlay()\n  const flatListRef = useRef<FlatList>(null)\n  const playLineRef = useRef<PlayLineType>(null)\n  const isPauseScrollRef = useRef(true)\n  const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)\n  const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)\n  const lineRef = useRef({ line: 0, prevLine: 0 })\n  const isFirstSetLrc = useRef(true)\n  const scrollInfoRef = useRef<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)\n  const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })\n  const scrollCancelRef = useRef<(() => void) | null>(null)\n  const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')\n  // useLock()\n  // const [imgUrl, setImgUrl] = useState(null)\n  // const theme = useGetter('common', 'theme')\n  // const { onLayout, ...layout } = useLayout()\n\n  // useEffect(() => {\n  //   const url = playMusicInfo ? playMusicInfo.musicInfo.img : null\n  //   if (imgUrl == url) return\n  //   setImgUrl(url)\n  // // eslint-disable-next-line react-hooks/exhaustive-deps\n  // }, [playMusicInfo])\n\n  // const imgWidth = useMemo(() => layout.width * 0.75, [layout.width])\n  const handleScrollToActive = (index = lineRef.current.line) => {\n    if (index < 0) return\n    if (flatListRef.current) {\n      // console.log('handleScrollToActive', index)\n      if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) {\n        let offset = listLayoutInfoRef.current.spaceHeight\n        for (let line = 0; line < index; line++) {\n          offset += listLayoutInfoRef.current.lineHeights[line]\n        }\n        offset += (listLayoutInfoRef.current.lineHeights[line] ?? 0) / 2\n        try {\n          scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.42, 600, () => {\n            scrollCancelRef.current = null\n          })\n        } catch {}\n      } else {\n        if (scrollCancelRef.current) {\n          scrollCancelRef.current()\n          scrollCancelRef.current = null\n        }\n        try {\n          flatListRef.current.scrollToIndex({\n            index,\n            animated: true,\n            viewPosition: 0.42,\n          })\n        } catch {}\n      }\n    }\n  }\n\n  const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {\n    scrollInfoRef.current = nativeEvent\n    if (isPauseScrollRef.current) {\n      playLineRef.current?.updateScrollInfo(nativeEvent)\n    }\n  }\n  const handleScrollBeginDrag = () => {\n    isPauseScrollRef.current = true\n    playLineRef.current?.setVisible(true)\n    if (delayScrollTimeout.current) {\n      clearTimeout(delayScrollTimeout.current)\n      delayScrollTimeout.current = null\n    }\n    if (scrollTimoutRef.current) {\n      clearTimeout(scrollTimoutRef.current)\n      scrollTimoutRef.current = null\n    }\n    if (scrollCancelRef.current) {\n      scrollCancelRef.current()\n      scrollCancelRef.current = null\n    }\n  }\n\n  const onScrollEndDrag = () => {\n    if (!isPauseScrollRef.current) return\n    if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)\n    scrollTimoutRef.current = setTimeout(() => {\n      playLineRef.current?.setVisible(false)\n      scrollTimoutRef.current = null\n      isPauseScrollRef.current = false\n      if (!playerState.isPlay) return\n      handleScrollToActive()\n    }, 3000)\n  }\n\n\n  useEffect(() => {\n    return () => {\n      if (delayScrollTimeout.current) {\n        clearTimeout(delayScrollTimeout.current)\n        delayScrollTimeout.current = null\n      }\n      if (scrollTimoutRef.current) {\n        clearTimeout(scrollTimoutRef.current)\n        scrollTimoutRef.current = null\n      }\n    }\n  }, [])\n\n  useEffect(() => {\n    // linesRef.current = lyricLines\n    listLayoutInfoRef.current.lineHeights = []\n    lineRef.current.prevLine = 0\n    lineRef.current.line = 0\n    if (!flatListRef.current) return\n    flatListRef.current.scrollToOffset({\n      offset: 0,\n      animated: false,\n    })\n    if (!lyricLines.length) return\n    playLineRef.current?.updateLyricLines(lyricLines)\n    requestAnimationFrame(() => {\n      if (isFirstSetLrc.current) {\n        isFirstSetLrc.current = false\n        setTimeout(() => {\n          isPauseScrollRef.current = false\n          handleScrollToActive()\n        }, 100)\n      } else {\n        if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current)\n        delayScrollTimeout.current = setTimeout(() => {\n          handleScrollToActive(0)\n        }, 100)\n      }\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [lyricLines])\n\n  useEffect(() => {\n    if (line < 0) return\n    lineRef.current.prevLine = lineRef.current.line\n    lineRef.current.line = line\n    if (!flatListRef.current || isPauseScrollRef.current) return\n\n    if (line - lineRef.current.prevLine != 1) {\n      handleScrollToActive()\n      return\n    }\n\n    delayScrollTimeout.current = setTimeout(() => {\n      delayScrollTimeout.current = null\n      handleScrollToActive()\n    }, 600)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [line])\n\n  useEffect(() => {\n    requestAnimationFrame(() => {\n      playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n      playLineRef.current?.updateLyricLines(lyricLines)\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isShowLyricProgressSetting])\n\n  const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {\n    void wait().then(() => {\n      handleScrollToActive(info.index)\n    })\n  }\n\n  const handleLineLayout = useCallback<LineProps['onLayout']>((lineNum, height) => {\n    listLayoutInfoRef.current.lineHeights[lineNum] = height\n    playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n  }, [])\n\n  const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {\n    listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height\n    playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n  }, [])\n\n  const handlePlayLine = useCallback((time: number) => {\n    playLineRef.current?.setVisible(false)\n    global.app_event.setProgress(time)\n  }, [])\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => {\n    return (\n      <LrcLine line={item} lineNum={index} activeLine={line} onLayout={handleLineLayout} />\n    )\n  }\n  const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}`\n\n  const spaceComponent = useMemo(() => (\n    <View style={styles.space} onLayout={handleSpaceLayout}></View>\n  ), [handleSpaceLayout])\n\n  return (\n    <>\n      <FlatList\n        data={lyricLines}\n        renderItem={renderItem}\n        keyExtractor={getkey}\n        style={styles.container}\n        ref={flatListRef}\n        showsVerticalScrollIndicator={false}\n        ListHeaderComponent={spaceComponent}\n        ListFooterComponent={spaceComponent}\n        onScrollBeginDrag={handleScrollBeginDrag}\n        onScrollEndDrag={onScrollEndDrag}\n        fadingEdgeLength={100}\n        initialNumToRender={Math.max(line + 10, 10)}\n        onScrollToIndexFailed={handleScrollToIndexFailed}\n        onScroll={handleScroll}\n      />\n      { isShowLyricProgressSetting ? <PlayLine ref={playLineRef} onPlayLine={handlePlayLine} /> : null }\n    </>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    paddingLeft: 20,\n    paddingRight: 20,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  space: {\n    paddingTop: '100%',\n  },\n  line: {\n    paddingTop: 10,\n    paddingBottom: 10,\n    // opacity: 0,\n  },\n  lineText: {\n    textAlign: 'center',\n    // fontSize: 16,\n    // lineHeight: 20,\n    // paddingTop: 5,\n    // paddingBottom: 5,\n    // opacity: 0,\n  },\n  lineTranslationText: {\n    textAlign: 'center',\n    // fontSize: 13,\n    // lineHeight: 17,\n    paddingTop: 5,\n    // paddingBottom: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/MoreBtn/Btn.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nexport const BTN_WIDTH = scaleSizeW(32)\nexport const BTN_ICON_SIZE = 22\n\nexport default ({ icon, color, onPress }: {\n  icon: string\n  color?: string\n  onPress: () => void\n}) => {\n  const theme = useTheme()\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: BTN_WIDTH, height: BTN_WIDTH }} activeOpacity={0.5} onPress={onPress}>\n      <Icon name={icon} color={color ?? theme['c-font-label']} size={BTN_ICON_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nconst styles = createStyle({\n  cotrolBtn: {\n    marginBottom: 5,\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/MoreBtn/MusicAddBtn.tsx",
    "content": "import { useRef } from 'react'\nimport MusicAddModal, { type MusicAddModalType } from '@/components/MusicAddModal'\nimport playerState from '@/store/player/state'\nimport Btn from './Btn'\n\n\nexport default () => {\n  const musicAddModalRef = useRef<MusicAddModalType>(null)\n\n  const handleShowMusicAddModal = () => {\n    const musicInfo = playerState.playMusicInfo.musicInfo\n    if (!musicInfo) return\n    musicAddModalRef.current?.show({\n      musicInfo: 'progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo,\n      isMove: false,\n      listId: playerState.playMusicInfo.listId!,\n    })\n  }\n\n  return (\n    <>\n      <Btn icon=\"add-music\" onPress={handleShowMusicAddModal} />\n      <MusicAddModal ref={musicAddModalRef} />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/MoreBtn/PlayModeBtn.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { toast } from '@/utils/tools'\nimport { MUSIC_TOGGLE_MODE_LIST, MUSIC_TOGGLE_MODE } from '@/config/constant'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport Btn from './Btn'\n\n\nexport default memo(() => {\n  const togglePlayMethod = useSettingValue('player.togglePlayMethod')\n  const t = useI18n()\n\n  const toggleNextPlayMode = () => {\n    let index = MUSIC_TOGGLE_MODE_LIST.indexOf(togglePlayMethod)\n    if (++index >= MUSIC_TOGGLE_MODE_LIST.length) index = 0\n    const mode = MUSIC_TOGGLE_MODE_LIST[index]\n    updateSetting({ 'player.togglePlayMethod': mode })\n    let modeName: 'play_list_loop' | 'play_list_random' | 'play_list_order' | 'play_single_loop' | 'play_single'\n    switch (mode) {\n      case MUSIC_TOGGLE_MODE.listLoop:\n        modeName = 'play_list_loop'\n        break\n      case MUSIC_TOGGLE_MODE.random:\n        modeName = 'play_list_random'\n        break\n      case MUSIC_TOGGLE_MODE.list:\n        modeName = 'play_list_order'\n        break\n      case MUSIC_TOGGLE_MODE.singleLoop:\n        modeName = 'play_single_loop'\n        break\n      default:\n        modeName = 'play_single'\n        break\n    }\n    toast(t(modeName))\n  }\n\n  const playModeIcon = useMemo(() => {\n    let playModeIcon = null\n    switch (togglePlayMethod) {\n      case MUSIC_TOGGLE_MODE.listLoop:\n        playModeIcon = 'list-loop'\n        break\n      case MUSIC_TOGGLE_MODE.random:\n        playModeIcon = 'list-random'\n        break\n      case MUSIC_TOGGLE_MODE.list:\n        playModeIcon = 'list-order'\n        break\n      case MUSIC_TOGGLE_MODE.singleLoop:\n        playModeIcon = 'single-loop'\n        break\n      default:\n        playModeIcon = 'single'\n        break\n    }\n    return playModeIcon\n  }, [togglePlayMethod])\n\n  return <Btn icon={playModeIcon} onPress={toggleNextPlayMode} />\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/MoreBtn/TimeoutExitBtn.tsx",
    "content": "import { memo, useRef } from 'react'\nimport TimeoutExitEditModal, { type TimeoutExitEditModalType, useTimeInfo } from '@/components/TimeoutExitEditModal'\nimport { useTheme } from '@/store/theme/hook'\nimport Btn from './Btn'\n\n\nexport default memo(() => {\n  const theme = useTheme()\n  const modalRef = useRef<TimeoutExitEditModalType>(null)\n\n  const timeInfo = useTimeInfo()\n\n  const handleShow = () => {\n    modalRef.current?.show()\n  }\n\n  return (\n    <>\n      <Btn icon=\"music_time\" color={timeInfo.active ? theme['c-primary-font-active'] : theme['c-font-label']} onPress={handleShow} />\n      <TimeoutExitEditModal ref={modalRef} timeInfo={timeInfo} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/MoreBtn/index.tsx",
    "content": "import { createStyle } from '@/utils/tools'\nimport { View } from 'react-native'\nimport PlayModeBtn from './PlayModeBtn'\nimport MusicAddBtn from './MusicAddBtn'\nimport TimeoutExitBtn from './TimeoutExitBtn'\n\nexport default () => {\n  return (\n    <View style={styles.container}>\n      <TimeoutExitBtn />\n      <MusicAddBtn />\n      <PlayModeBtn />\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    flexShrink: 0,\n    flexGrow: 0,\n    flexDirection: 'column',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    justifyContent: 'center',\n    position: 'absolute',\n    height: '100%',\n    left: 0,\n    gap: 16,\n    zIndex: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Pic.tsx",
    "content": "import { memo, useEffect, useState } from 'react'\nimport { View } from 'react-native'\n// import { useLayout } from '@/utils/hooks'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\nimport { useWindowSize } from '@/utils/hooks'\nimport { useNavigationComponentDidAppear } from '@/navigation'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport { createStyle } from '@/utils/tools'\nimport { HEADER_HEIGHT } from './components/Header'\nimport { BTN_WIDTH } from './MoreBtn/Btn'\nimport { marginLeft } from './constant'\nimport Image from '@/components/common/Image'\nimport { useStatusbarHeight } from '@/store/common/hook'\nimport commonState from '@/store/common/state'\n\n\nexport default memo(({ componentId }: { componentId: string }) => {\n  const musicInfo = usePlayerMusicInfo()\n  const { width: winWidth, height: winHeight } = useWindowSize()\n  const statusBarHeight = useStatusbarHeight()\n\n  const [animated, setAnimated] = useState(!!commonState.componentIds.playDetail)\n  const [pic, setPic] = useState(musicInfo.pic)\n  useEffect(() => {\n    if (animated) setPic(musicInfo.pic)\n  }, [musicInfo.pic, animated])\n\n  useNavigationComponentDidAppear(componentId, () => {\n    setAnimated(true)\n  })\n\n  let imgWidth = Math.min((winWidth * 0.45 - marginLeft - BTN_WIDTH) * 0.76, (winHeight - statusBarHeight - HEADER_HEIGHT) * 0.62)\n  imgWidth -= imgWidth * (global.lx.fontSize - 1) * 0.3\n  let contentHeight = (winHeight - statusBarHeight - HEADER_HEIGHT) * 0.66\n  contentHeight -= contentHeight * (global.lx.fontSize - 1) * 0.2\n\n  return (\n    <View style={{ ...styles.container, height: contentHeight }}>\n      <View style={{ ...styles.content, elevation: animated ? 3 : 0 }}>\n        <Image url={pic} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_pic} style={{\n          width: imgWidth,\n          height: imgWidth,\n          borderRadius: 2,\n        }} />\n      </View>\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexShrink: 1,\n    flexGrow: 0,\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    overflow: 'hidden',\n  },\n  content: {\n    // elevation: 3,\n    backgroundColor: 'rgba(0,0,0,0)',\n    borderRadius: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Player/ControlBtn.tsx",
    "content": "import { StyleSheet, TouchableOpacity, View } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { useTheme } from '@/store/theme/hook'\n// import { useIsPlay } from '@/store/player/hook'\nimport { playNext, playPrev, togglePlay } from '@/core/player/player'\n// import { scaleSizeW } from '@/utils/pixelRatio'\nimport { useIsPlay } from '@/store/player/hook'\nimport { useLayout } from '@/utils/hooks'\nimport { marginLeft } from '../constant'\nimport { BTN_WIDTH } from '../MoreBtn/Btn'\n\n// const WIDTH = scaleSizeW(48)\n\nconst PrevBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const handlePlayPrev = () => {\n    void playPrev()\n  }\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={handlePlayPrev}>\n      <Icon name='prevMusic' color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\nconst NextBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const handlePlayNext = () => {\n    void playNext()\n  }\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={handlePlayNext}>\n      <Icon name='nextMusic' color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\n\nconst TogglePlayBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const isPlay = useIsPlay()\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={togglePlay}>\n      <Icon name={isPlay ? 'pause' : 'play'} color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\n\nconst MIN_SIZE = BTN_WIDTH * 1.1\nexport default () => {\n  const { onLayout, height, width } = useLayout()\n  const size = Math.max(Math.min(height * 0.65, (width - marginLeft) * 0.52 * 0.3) * global.lx.fontSize, MIN_SIZE)\n  return (\n    <View style={{ ...styles.content, gap: size * 0.5 }} onLayout={onLayout}>\n      <PrevBtn size={size} />\n      <TogglePlayBtn size={size}/>\n      <NextBtn size={size} />\n    </View>\n  )\n}\n\n\nconst styles = StyleSheet.create({\n  content: {\n    flexGrow: 1,\n    flexShrink: 1,\n    flexDirection: 'row',\n    // paddingVertical: 8,\n    gap: 22,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    alignItems: 'center',\n    justifyContent: 'center',\n  },\n  cotrolBtn: {\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n    // marginLeft: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Player/PlayInfo.tsx",
    "content": "import { memo } from 'react'\nimport { StyleSheet, View } from 'react-native'\n\nimport Progress from '@/components/player/Progress'\nimport Status from './Status'\nimport { useProgress } from '@/store/player/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useBufferProgress } from '@/plugins/player'\n\n// const FONT_SIZE = 13\n\nconst PlayTimeCurrent = ({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  // console.log(timeStr)\n  return <Text color={theme['c-500']}>{timeStr}</Text>\n}\n\nconst PlayTimeMax = memo(({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  return <Text color={theme['c-500']}>{timeStr}</Text>\n})\n\nexport default () => {\n  const theme = useTheme()\n  const { maxPlayTimeStr, nowPlayTimeStr, progress, maxPlayTime } = useProgress()\n  const buffered = useBufferProgress()\n  // console.log('render playInfo')\n\n  return (\n    <View style={styles.container}>\n      <View style={styles.status} >\n        <Status />\n      </View>\n      <View style={{ flexGrow: 0, flexShrink: 0, flexDirection: 'row' }} >\n        <PlayTimeCurrent timeStr={nowPlayTimeStr} />\n        <Text color={theme['c-500']}> / </Text>\n        <PlayTimeMax timeStr={maxPlayTimeStr} />\n      </View>\n      <View style={[StyleSheet.absoluteFill, styles.progress]}><Progress progress={progress} duration={maxPlayTime} buffered={buffered} /></View>\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    // marginLeft: 15,\n    marginVertical: 5,\n    height: 26,\n    // flex: 1,\n    paddingVertical: 2,\n    paddingHorizontal: 5,\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-between',\n  },\n  progress: {\n    flexGrow: 1,\n    flexShrink: 0,\n    flexDirection: 'column',\n    justifyContent: 'center',\n  },\n  info: {\n    flexDirection: 'row',\n    justifyContent: 'space-between',\n    // alignItems: 'center',\n    // backgroundColor: '#ccc',\n  },\n  status: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingRight: 5,\n  },\n})\n// const styles = createStyle({\n//   container: {\n//     flex: 1,\n//     // height: 16,\n//     // flexGrow: 0,\n//     // flexShrink: 0,\n//     // flexDirection: 'column',\n//     // justifyContent: 'center',\n//     // alignItems: 'center',\n//     // marginBottom: -1,\n//     // backgroundColor: '#ccc',\n//     // overflow: 'hidden',\n//     // height:\n//     // position: 'absolute',\n//     // width: '100%',\n//     // top: 0,\n//     paddingVertical: 2,\n//     paddingHorizontal: 5,\n//     flexDirection: 'row',\n//     alignItems: 'center',\n//     justifyContent: 'space-between',\n//   },\n//   progress: {\n//     paddingVertical: 2,\n//     zIndex: 100,\n//   },\n//   status: {\n//     flexGrow: 1,\n//     flexShrink: 1,\n//     paddingRight: 5,\n//   },\n// })\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Player/Status.tsx",
    "content": "// import { useLrcPlay } from '@/plugins/lyric'\nimport { useStatusText } from '@/store/player/hook'\n// import { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\n\nexport default () => {\n  // const { text } = useLrcPlay()\n  const statusText = useStatusText()\n  // console.log('render status')\n\n  // const status = playerStatus.isPlay ? text : playerStatus.statusText\n\n  return <Text numberOfLines={1} size={13}>{statusText}</Text>\n}\n\n// const styles = createStyle({\n//   text: {\n//     fontSize: 10,\n//   },\n// })\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/Player/index.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\n// import Title from './components/Title'\nimport { createStyle } from '@/utils/tools'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport PlayInfo from './PlayInfo'\nimport ControlBtn from './ControlBtn'\nimport { marginLeftRaw } from '../constant'\n\n\nexport default memo(() => {\n  return (\n    <View style={styles.container} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_player}>\n      <ControlBtn />\n      <PlayInfo />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexShrink: 0,\n    flexGrow: 1,\n    marginLeft: marginLeftRaw,\n    // paddingRight: 15,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/components/Btn.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nimport { HEADER_HEIGHT } from '@/config/constant'\nexport const BTN_WIDTH = scaleSizeW(HEADER_HEIGHT)\nexport const BTN_ICON_SIZE = 20\n\nexport default ({ icon, size, color, onPress, onLongPress }: {\n  icon: string\n  size?: number\n  color?: string\n  onPress: () => void\n  onLongPress?: () => void\n}) => {\n  const theme = useTheme()\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: BTN_WIDTH, height: BTN_WIDTH }} activeOpacity={0.5} onPress={onPress} onLongPress={onLongPress}>\n      <Icon name={icon} color={color ?? theme['c-550']} size={size ?? BTN_ICON_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nconst styles = createStyle({\n  cotrolBtn: {\n    // marginLeft: 5,\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/components/CommentBtn.tsx",
    "content": "import Btn from './Btn'\nimport { navigations } from '@/navigation'\nimport commonState from '@/store/common/state'\n\n\nexport default () => {\n  const handleShowCommentScreen = () => {\n    navigations.pushCommentScreen(commonState.componentIds.playDetail!)\n  }\n\n  return <Btn icon=\"comment\" onPress={handleShowCommentScreen} />\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/components/DesktopLyricBtn.tsx",
    "content": "import Btn from './Btn'\nimport { useSettingValue } from '@/store/setting/hook'\nimport DesktopLyricEnable, { type DesktopLyricEnableType } from '@/components/DesktopLyricEnable'\nimport { memo, useRef } from 'react'\nimport { toggleDesktopLyricLock } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\nimport settingState from '@/store/setting/state'\n\n\nexport default memo(() => {\n  const enabledLyric = useSettingValue('desktopLyric.enable')\n  const desktopLyricEnableRef = useRef<DesktopLyricEnableType>(null)\n  const update = () => {\n    desktopLyricEnableRef.current?.setEnabled(!enabledLyric)\n  }\n  const updateLock = () => {\n    const isLock = !settingState.setting['desktopLyric.isLock']\n    void toggleDesktopLyricLock(isLock).then(() => {\n      updateSetting({ 'desktopLyric.isLock': isLock })\n    })\n  }\n\n  return (\n    <>\n      <Btn icon={enabledLyric ? 'lyric-on' : 'lyric-off'} onPress={update} onLongPress={updateLock} />\n      <DesktopLyricEnable ref={desktopLyricEnableRef} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/components/Header.tsx",
    "content": "import { memo, useRef } from 'react'\n\nimport { View, StyleSheet, TouchableOpacity } from 'react-native'\n\nimport { Icon } from '@/components/common/Icon'\nimport { pop } from '@/navigation'\nimport { useTheme } from '@/store/theme/hook'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\nimport Text from '@/components/common/Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { HEADER_HEIGHT as _HEADER_HEIGHT, NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport commonState from '@/store/common/state'\nimport CommentBtn from './CommentBtn'\nimport Btn from './Btn'\nimport SettingPopup, { type SettingPopupType } from '../../components/SettingPopup'\nimport DesktopLyricBtn from './DesktopLyricBtn'\n\nexport const HEADER_HEIGHT = scaleSizeH(_HEADER_HEIGHT)\n\nconst Title = () => {\n  const theme = useTheme()\n  const musicInfo = usePlayerMusicInfo()\n\n\n  return (\n    <View style={styles.titleContent}>\n      <Text numberOfLines={1} style={styles.title} size={14}>{musicInfo.name}</Text>\n      <Text numberOfLines={1} style={styles.title} size={12} color={theme['c-font-label']}>{musicInfo.singer}</Text>\n    </View>\n  )\n}\n\nexport default memo(() => {\n  const popupRef = useRef<SettingPopupType>(null)\n\n  const back = () => {\n    void pop(commonState.componentIds.playDetail!)\n  }\n  const showSetting = () => {\n    popupRef.current?.show()\n  }\n\n  return (\n    <View style={{ height: HEADER_HEIGHT }} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_header}>\n      <View style={styles.container}>\n        <TouchableOpacity onPress={back} style={{ ...styles.button, width: HEADER_HEIGHT }}>\n          <Icon name=\"chevron-left\" size={18} />\n        </TouchableOpacity>\n        <Title />\n        <DesktopLyricBtn />\n        <CommentBtn />\n        <Btn icon=\"slider\" onPress={showSetting} />\n      </View>\n      <SettingPopup ref={popupRef} position=\"left\" direction=\"horizontal\" />\n    </View>\n  )\n})\n\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 0,\n    // backgroundColor: '#ccc',\n    flexDirection: 'row',\n    // justifyContent: 'center',\n    height: '100%',\n  },\n  button: {\n    justifyContent: 'center',\n    alignItems: 'center',\n    height: '100%',\n    flex: 0,\n  },\n  titleContent: {\n    flex: 1,\n    // alignItems: 'center',\n    justifyContent: 'center',\n  },\n  title: {\n    // flex: 1,\n    // textAlign: 'center',\n  },\n  icon: {\n    paddingLeft: 4,\n    paddingRight: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/constant.ts",
    "content": "import { scaleSizeW } from '@/utils/pixelRatio'\n\nexport const marginLeftRaw = 15\nexport const marginLeft = scaleSizeW(marginLeftRaw)\n"
  },
  {
    "path": "src/screens/PlayDetail/Horizontal/index.tsx",
    "content": "import { memo, useEffect } from 'react'\nimport { View, AppState } from 'react-native'\nimport { screenkeepAwake, screenUnkeepAwake } from '@/utils/nativeModules/utils'\nimport StatusBar from '@/components/common/StatusBar'\nimport MoreBtn from './MoreBtn'\n\nimport Header from './components/Header'\nimport { setComponentId } from '@/core/common'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport PageContent from '@/components/PageContent'\nimport commonState, { type InitState as CommonState } from '@/store/common/state'\n\nimport Pic from './Pic'\n// import ControlBtn from './ControlBtn'\nimport Lyric from './Lyric'\nimport Player from './Player'\nimport { createStyle } from '@/utils/tools'\nimport { marginLeftRaw } from './constant'\nimport { useStatusbarHeight } from '@/store/common/hook'\n// import MoreBtn from './MoreBtn2'\n\nexport default memo(({ componentId }: { componentId: string }) => {\n  const statusBarHeight = useStatusbarHeight()\n\n  useEffect(() => {\n    setComponentId(COMPONENT_IDS.playDetail, componentId)\n    screenkeepAwake()\n    let appstateListener = AppState.addEventListener('change', (state) => {\n      switch (state) {\n        case 'active':\n          if (!commonState.componentIds.comment) screenkeepAwake()\n          break\n        case 'background':\n          screenUnkeepAwake()\n          break\n      }\n    })\n\n    const handleComponentIdsChange = (ids: CommonState['componentIds']) => {\n      if (ids.comment) screenUnkeepAwake()\n      else if (AppState.currentState == 'active') screenkeepAwake()\n    }\n\n    global.state_event.on('componentIdsUpdated', handleComponentIdsChange)\n\n    return () => {\n      global.state_event.off('componentIdsUpdated', handleComponentIdsChange)\n      appstateListener.remove()\n      screenUnkeepAwake()\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <PageContent>\n      <StatusBar />\n      <View style={{ ...styles.container, paddingTop: statusBarHeight }}>\n        <View style={styles.left}>\n          <Header />\n          <View style={styles.leftContent}>\n            <MoreBtn />\n            <Pic componentId={componentId} />\n          </View>\n          <Player />\n          {/* <View style={styles.controlBtn} nativeID=\"pageIndicator\">\n            <MoreBtn />\n            <ControlBtn />\n          </View> */}\n        </View>\n        <View style={styles.right}>\n          <Lyric />\n        </View>\n      </View>\n    </PageContent>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    flexDirection: 'row',\n  },\n  left: {\n    flex: 1,\n    width: '45%',\n    paddingBottom: 10,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  leftContent: {\n    flexShrink: 1,\n    flexGrow: 0,\n    marginLeft: marginLeftRaw,\n    // flexDirection: 'row',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    // alignItems: 'center',\n  },\n  right: {\n    width: '55%',\n    flexGrow: 0,\n    flexShrink: 0,\n  },\n  controlBtn: {\n    flex: 1,\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-between',\n    // backgroundColor: '#eee',\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Lyric.tsx",
    "content": "import { memo, useMemo, useEffect, useRef, useCallback } from 'react'\nimport { View, FlatList, type FlatListProps, type LayoutChangeEvent, type NativeSyntheticEvent, type NativeScrollEvent } from 'react-native'\n// import { useLayout } from '@/utils/hooks'\nimport { type Line, useLrcPlay, useLrcSet } from '@/plugins/lyric'\nimport { createStyle } from '@/utils/tools'\n// import { useComponentIds } from '@/store/common/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { AnimatedColorText } from '@/components/common/Text'\nimport { setSpText } from '@/utils/pixelRatio'\nimport playerState from '@/store/player/state'\nimport { scrollTo } from '@/utils/scroll'\nimport PlayLine, { type PlayLineType } from '../components/PlayLine'\n// import { screenkeepAwake } from '@/utils/nativeModules/utils'\n// import { log } from '@/utils/log'\n// import { toast } from '@/utils/tools'\n\ntype FlatListType = FlatListProps<Line>\n\n// const useLock = () => {\n//   const showCommentRef = useRef(false)\n\n\n//   useEffect(() => {\n//     let appstateListener = AppState.addEventListener('change', (state) => {\n//       switch (state) {\n//         case 'active':\n//           if (showLyricRef.current && !showCommentRef.current) screenkeepAwake()\n//           break\n//         case 'background':\n//           screenUnkeepAwake()\n//           break\n//       }\n//     })\n//     return () => {\n//       appstateListener.remove()\n//     }\n//   }, [])\n//   useEffect(() => {\n//     let listener: ReturnType<typeof onNavigationComponentDidDisappearEvent>\n//     showCommentRef.current = !!componentIds.comment\n//     if (showCommentRef.current) {\n//       if (showLyricRef.current) screenUnkeepAwake()\n//       listener = onNavigationComponentDidDisappearEvent(componentIds.comment as string, () => {\n//         if (showLyricRef.current && AppState.currentState == 'active') screenkeepAwake()\n//       })\n//     }\n\n//     const rm = global.state_event.on('componentIdsUpdated', (ids) => {\n\n//     })\n\n//     return () => {\n//       if (listener) listener.remove()\n//     }\n//   }, [])\n// }\n\ninterface LineProps {\n  line: Line\n  lineNum: number\n  activeLine: number\n  onLayout: (lineNum: number, height: number, width: number) => void\n}\nconst LrcLine = memo(({ line, lineNum, activeLine, onLayout }: LineProps) => {\n  const theme = useTheme()\n  const lrcFontSize = useSettingValue('playDetail.vertical.style.lrcFontSize')\n  const textAlign = useSettingValue('playDetail.style.align')\n  const size = lrcFontSize / 10\n  const lineHeight = setSpText(size) * 1.3\n\n  const colors = useMemo(() => {\n    const active = activeLine == lineNum\n    return active ? [\n      theme['c-primary'],\n      theme['c-primary-alpha-200'],\n      1,\n    ] as const : [\n      theme['c-350'],\n      theme['c-300'],\n      0.6,\n    ] as const\n  }, [activeLine, lineNum, theme])\n\n  const handleLayout = ({ nativeEvent }: LayoutChangeEvent) => {\n    onLayout(lineNum, nativeEvent.layout.height, nativeEvent.layout.width)\n  }\n\n\n  // textBreakStrategy=\"simple\" 用于解决某些设备上字体被截断的问题\n  // https://stackoverflow.com/a/72822360\n  return (\n    <View style={styles.line} onLayout={handleLayout}>\n      <AnimatedColorText style={{\n        ...styles.lineText,\n        textAlign,\n        lineHeight,\n      }} textBreakStrategy=\"simple\" color={colors[0]} opacity={colors[2]} size={size}>{line.text}</AnimatedColorText>\n      {\n        line.extendedLyrics.map((lrc, index) => {\n          return (<AnimatedColorText style={{\n            ...styles.lineTranslationText,\n            textAlign,\n            lineHeight: lineHeight * 0.8,\n          }} textBreakStrategy=\"simple\" key={index} color={colors[1]} opacity={colors[2]} size={size * 0.8}>{lrc}</AnimatedColorText>)\n        })\n      }\n    </View>\n  )\n}, (prevProps, nextProps) => {\n  return prevProps.line === nextProps.line &&\n    prevProps.activeLine != nextProps.lineNum &&\n    nextProps.activeLine != nextProps.lineNum\n})\nconst wait = async() => new Promise(resolve => setTimeout(resolve, 100))\n\nexport default () => {\n  const lyricLines = useLrcSet()\n  const { line } = useLrcPlay()\n  const flatListRef = useRef<FlatList>(null)\n  const playLineRef = useRef<PlayLineType>(null)\n  const isPauseScrollRef = useRef(true)\n  const scrollTimoutRef = useRef<NodeJS.Timeout | null>(null)\n  const delayScrollTimeout = useRef<NodeJS.Timeout | null>(null)\n  const lineRef = useRef({ line: 0, prevLine: 0 })\n  const isFirstSetLrc = useRef(true)\n  const scrollInfoRef = useRef<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)\n  const listLayoutInfoRef = useRef<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })\n  const scrollCancelRef = useRef<(() => void) | null>(null)\n  const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')\n  // useLock()\n  // const [imgUrl, setImgUrl] = useState(null)\n  // const theme = useGetter('common', 'theme')\n  // const { onLayout, ...layout } = useLayout()\n\n  // useEffect(() => {\n  //   const url = playMusicInfo ? playMusicInfo.musicInfo.img : null\n  //   if (imgUrl == url) return\n  //   setImgUrl(url)\n  // // eslint-disable-next-line react-hooks/exhaustive-deps\n  // }, [playMusicInfo])\n\n  // const imgWidth = useMemo(() => layout.width * 0.75, [layout.width])\n  const handleScrollToActive = (index = lineRef.current.line) => {\n    if (index < 0) return\n    if (flatListRef.current) {\n      // console.log('handleScrollToActive', index)\n      if (scrollInfoRef.current && lineRef.current.line - lineRef.current.prevLine == 1) {\n        let offset = listLayoutInfoRef.current.spaceHeight\n        for (let line = 0; line < index; line++) {\n          offset += listLayoutInfoRef.current.lineHeights[line]\n        }\n        offset += (listLayoutInfoRef.current.lineHeights[line] ?? 0) / 2\n        try {\n          scrollCancelRef.current = scrollTo(flatListRef.current, scrollInfoRef.current, offset - scrollInfoRef.current.layoutMeasurement.height * 0.42, 600, () => {\n            scrollCancelRef.current = null\n          })\n        } catch {}\n      } else {\n        if (scrollCancelRef.current) {\n          scrollCancelRef.current()\n          scrollCancelRef.current = null\n        }\n        try {\n          flatListRef.current.scrollToIndex({\n            index,\n            animated: true,\n            viewPosition: 0.42,\n          })\n        } catch {}\n      }\n    }\n  }\n\n  const handleScroll = ({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {\n    scrollInfoRef.current = nativeEvent\n    if (isPauseScrollRef.current) {\n      playLineRef.current?.updateScrollInfo(nativeEvent)\n    }\n  }\n  const handleScrollBeginDrag = () => {\n    isPauseScrollRef.current = true\n    playLineRef.current?.setVisible(true)\n    if (delayScrollTimeout.current) {\n      clearTimeout(delayScrollTimeout.current)\n      delayScrollTimeout.current = null\n    }\n    if (scrollTimoutRef.current) {\n      clearTimeout(scrollTimoutRef.current)\n      scrollTimoutRef.current = null\n    }\n    if (scrollCancelRef.current) {\n      scrollCancelRef.current()\n      scrollCancelRef.current = null\n    }\n  }\n\n  const onScrollEndDrag = () => {\n    if (!isPauseScrollRef.current) return\n    if (scrollTimoutRef.current) clearTimeout(scrollTimoutRef.current)\n    scrollTimoutRef.current = setTimeout(() => {\n      playLineRef.current?.setVisible(false)\n      scrollTimoutRef.current = null\n      isPauseScrollRef.current = false\n      if (!playerState.isPlay) return\n      handleScrollToActive()\n    }, 3000)\n  }\n\n\n  useEffect(() => {\n    return () => {\n      if (delayScrollTimeout.current) {\n        clearTimeout(delayScrollTimeout.current)\n        delayScrollTimeout.current = null\n      }\n      if (scrollTimoutRef.current) {\n        clearTimeout(scrollTimoutRef.current)\n        scrollTimoutRef.current = null\n      }\n    }\n  }, [])\n\n  useEffect(() => {\n    // linesRef.current = lyricLines\n    listLayoutInfoRef.current.lineHeights = []\n    lineRef.current.prevLine = 0\n    lineRef.current.line = 0\n    if (!flatListRef.current) return\n    flatListRef.current.scrollToOffset({\n      offset: 0,\n      animated: false,\n    })\n    if (!lyricLines.length) return\n    playLineRef.current?.updateLyricLines(lyricLines)\n    requestAnimationFrame(() => {\n      if (isFirstSetLrc.current) {\n        isFirstSetLrc.current = false\n        setTimeout(() => {\n          isPauseScrollRef.current = false\n          handleScrollToActive()\n        }, 100)\n      } else {\n        if (delayScrollTimeout.current) clearTimeout(delayScrollTimeout.current)\n        delayScrollTimeout.current = setTimeout(() => {\n          handleScrollToActive(0)\n        }, 100)\n      }\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [lyricLines])\n\n  useEffect(() => {\n    if (line < 0) return\n    lineRef.current.prevLine = lineRef.current.line\n    lineRef.current.line = line\n    if (!flatListRef.current || isPauseScrollRef.current) return\n\n    if (line - lineRef.current.prevLine != 1) {\n      handleScrollToActive()\n      return\n    }\n\n    delayScrollTimeout.current = setTimeout(() => {\n      delayScrollTimeout.current = null\n      handleScrollToActive()\n    }, 600)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [line])\n\n  useEffect(() => {\n    requestAnimationFrame(() => {\n      playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n      playLineRef.current?.updateLyricLines(lyricLines)\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isShowLyricProgressSetting])\n\n  const handleScrollToIndexFailed: FlatListType['onScrollToIndexFailed'] = (info) => {\n    void wait().then(() => {\n      handleScrollToActive(info.index)\n    })\n  }\n\n  const handleLineLayout = useCallback<LineProps['onLayout']>((lineNum, height) => {\n    listLayoutInfoRef.current.lineHeights[lineNum] = height\n    playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n  }, [])\n\n  const handleSpaceLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => {\n    listLayoutInfoRef.current.spaceHeight = nativeEvent.layout.height\n    playLineRef.current?.updateLayoutInfo(listLayoutInfoRef.current)\n  }, [])\n\n  const handlePlayLine = useCallback((time: number) => {\n    playLineRef.current?.setVisible(false)\n    global.app_event.setProgress(time)\n  }, [])\n\n  const renderItem: FlatListType['renderItem'] = ({ item, index }) => {\n    return (\n      <LrcLine line={item} lineNum={index} activeLine={line} onLayout={handleLineLayout} />\n    )\n  }\n  const getkey: FlatListType['keyExtractor'] = (item, index) => `${index}${item.text}`\n\n  const spaceComponent = useMemo(() => (\n    <View style={styles.space} onLayout={handleSpaceLayout}></View>\n  ), [handleSpaceLayout])\n\n  return (\n    <>\n      <FlatList\n        data={lyricLines}\n        renderItem={renderItem}\n        keyExtractor={getkey}\n        style={styles.container}\n        ref={flatListRef}\n        showsVerticalScrollIndicator={false}\n        ListHeaderComponent={spaceComponent}\n        ListFooterComponent={spaceComponent}\n        onScrollBeginDrag={handleScrollBeginDrag}\n        onScrollEndDrag={onScrollEndDrag}\n        fadingEdgeLength={100}\n        initialNumToRender={Math.max(line + 10, 10)}\n        onScrollToIndexFailed={handleScrollToIndexFailed}\n        onScroll={handleScroll}\n      />\n      { isShowLyricProgressSetting ? <PlayLine ref={playLineRef} onPlayLine={handlePlayLine} /> : null }\n    </>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    paddingLeft: 20,\n    paddingRight: 20,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  space: {\n    paddingTop: '100%',\n  },\n  line: {\n    paddingTop: 10,\n    paddingBottom: 10,\n    // opacity: 0,\n  },\n  lineText: {\n    textAlign: 'center',\n    // fontSize: 16,\n    // lineHeight: 20,\n    // paddingTop: 5,\n    // paddingBottom: 5,\n    // opacity: 0,\n  },\n  lineTranslationText: {\n    textAlign: 'center',\n    // fontSize: 13,\n    // lineHeight: 17,\n    paddingTop: 5,\n    // paddingBottom: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Pic.tsx",
    "content": "import { useEffect, useMemo, useState } from 'react'\nimport { View } from 'react-native'\n// import { useLayout } from '@/utils/hooks'\nimport { createStyle } from '@/utils/tools'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\nimport { useWindowSize } from '@/utils/hooks'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport { useNavigationComponentDidAppear } from '@/navigation'\nimport { HEADER_HEIGHT } from './components/Header'\nimport Image from '@/components/common/Image'\nimport { useStatusbarHeight } from '@/store/common/hook'\nimport commonState from '@/store/common/state'\n\n\nexport default ({ componentId }: { componentId: string }) => {\n  const musicInfo = usePlayerMusicInfo()\n  const { width: winWidth, height: winHeight } = useWindowSize()\n  const statusBarHeight = useStatusbarHeight()\n\n  const [animated, setAnimated] = useState(!!commonState.componentIds.playDetail)\n  const [pic, setPic] = useState(musicInfo.pic)\n  useEffect(() => {\n    if (animated) setPic(musicInfo.pic)\n  }, [musicInfo.pic, animated])\n\n  useNavigationComponentDidAppear(componentId, () => {\n    setAnimated(true)\n  })\n  // console.log('render pic')\n\n  const style = useMemo(() => {\n    const imgWidth = Math.min(winWidth * 0.8, (winHeight - statusBarHeight - HEADER_HEIGHT) * 0.5)\n    return {\n      width: imgWidth,\n      height: imgWidth,\n      borderRadius: 2,\n    }\n  }, [statusBarHeight, winHeight, winWidth])\n\n  return (\n    <View style={styles.container}>\n      <View style={{ ...styles.content, elevation: animated ? 3 : 0 }}>\n        <Image url={pic} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_pic} style={style} />\n      </View>\n    </View>\n  )\n}\n\nconst styles = createStyle({\n  container: {\n    flexGrow: 1,\n    flexShrink: 1,\n    justifyContent: 'center',\n    alignItems: 'center',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  content: {\n    // elevation: 3,\n    backgroundColor: 'rgba(0,0,0,0)',\n    borderRadius: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/ControlBtn.tsx",
    "content": "import { TouchableOpacity, View } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { useTheme } from '@/store/theme/hook'\n// import { useIsPlay } from '@/store/player/hook'\nimport { playNext, playPrev, togglePlay } from '@/core/player/player'\nimport { useIsPlay } from '@/store/player/hook'\nimport { createStyle } from '@/utils/tools'\nimport { useWindowSize } from '@/utils/hooks'\nimport { BTN_WIDTH } from './MoreBtn/Btn'\nimport { useMemo } from 'react'\n\nconst PrevBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const handlePlayPrev = () => {\n    void playPrev()\n  }\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={handlePlayPrev}>\n      <Icon name='prevMusic' color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\nconst NextBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const handlePlayNext = () => {\n    void playNext()\n  }\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={handlePlayNext}>\n      <Icon name='nextMusic' color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\n\nconst TogglePlayBtn = ({ size }: { size: number }) => {\n  const theme = useTheme()\n  const isPlay = useIsPlay()\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: size, height: size }} activeOpacity={0.5} onPress={togglePlay}>\n      <Icon name={isPlay ? 'pause' : 'play'} color={theme['c-button-font']} rawSize={size * 0.7} />\n    </TouchableOpacity>\n  )\n}\n\nconst MAX_SIZE = BTN_WIDTH * 1.6\nconst MIN_SIZE = BTN_WIDTH * 1.2\n\nexport default () => {\n  const winSize = useWindowSize()\n  const maxHeight = Math.max(winSize.height * 0.11, MIN_SIZE)\n  const containerStyle = useMemo(() => {\n    return {\n      ...styles.conatiner,\n      maxHeight,\n    }\n  }, [maxHeight])\n  const size = Math.min(Math.max(winSize.width * 0.33 * global.lx.fontSize * 0.4, MIN_SIZE), MAX_SIZE, maxHeight)\n\n  return (\n    <View style={containerStyle}>\n      <PrevBtn size={size} />\n      <TogglePlayBtn size={size}/>\n      <NextBtn size={size} />\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  conatiner: {\n    flexDirection: 'row',\n    justifyContent: 'space-evenly',\n    alignItems: 'center',\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingHorizontal: '4%',\n    paddingVertical: 22,\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n  },\n  cotrolBtn: {\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/Btn.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { useTheme } from '@/store/theme/hook'\nimport { scaleSizeW } from '@/utils/pixelRatio'\n\nexport const BTN_WIDTH = scaleSizeW(36)\nexport const BTN_ICON_SIZE = 24\n\nexport default ({ icon, color, onPress, onLongPress }: {\n  icon: string\n  color?: string\n  onPress: () => void\n  onLongPress?: () => void\n}) => {\n  const theme = useTheme()\n  return (\n    <TouchableOpacity style={{ ...styles.cotrolBtn, width: BTN_WIDTH, height: BTN_WIDTH }} activeOpacity={0.5} onPress={onPress} onLongPress={onLongPress}>\n      <Icon name={icon} color={color ?? theme['c-font-label']} size={BTN_ICON_SIZE} />\n    </TouchableOpacity>\n  )\n}\n\nconst styles = createStyle({\n  cotrolBtn: {\n    marginLeft: 5,\n    justifyContent: 'center',\n    alignItems: 'center',\n\n    // backgroundColor: '#ccc',\n    shadowOpacity: 1,\n    textShadowRadius: 1,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/CommentBtn.tsx",
    "content": "import Btn from './Btn'\nimport { navigations } from '@/navigation'\nimport commonState from '@/store/common/state'\n\n\nexport default () => {\n  const handleShowCommentScreen = () => {\n    navigations.pushCommentScreen(commonState.componentIds.playDetail!)\n  }\n\n  return <Btn icon=\"comment\" onPress={handleShowCommentScreen} />\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/DesktopLyricBtn.tsx",
    "content": "import Btn from './Btn'\nimport { useSettingValue } from '@/store/setting/hook'\nimport DesktopLyricEnable, { type DesktopLyricEnableType } from '@/components/DesktopLyricEnable'\nimport { memo, useRef } from 'react'\nimport { toggleDesktopLyricLock } from '@/core/desktopLyric'\nimport { updateSetting } from '@/core/common'\nimport settingState from '@/store/setting/state'\n\n\nexport default memo(() => {\n  const enabledLyric = useSettingValue('desktopLyric.enable')\n  const desktopLyricEnableRef = useRef<DesktopLyricEnableType>(null)\n  const update = () => {\n    desktopLyricEnableRef.current?.setEnabled(!enabledLyric)\n  }\n  const updateLock = () => {\n    const isLock = !settingState.setting['desktopLyric.isLock']\n    void toggleDesktopLyricLock(isLock).then(() => {\n      updateSetting({ 'desktopLyric.isLock': isLock })\n    })\n  }\n\n  return (\n    <>\n      <Btn icon={enabledLyric ? 'lyric-on' : 'lyric-off'} onPress={update} onLongPress={updateLock} />\n      <DesktopLyricEnable ref={desktopLyricEnableRef} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/MusicAddBtn.tsx",
    "content": "import { useRef } from 'react'\nimport MusicAddModal, { type MusicAddModalType } from '@/components/MusicAddModal'\nimport playerState from '@/store/player/state'\nimport Btn from './Btn'\n\n\nexport default () => {\n  const musicAddModalRef = useRef<MusicAddModalType>(null)\n\n  const handleShowMusicAddModal = () => {\n    const musicInfo = playerState.playMusicInfo.musicInfo\n    if (!musicInfo) return\n    musicAddModalRef.current?.show({\n      musicInfo: 'progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo,\n      isMove: false,\n      listId: playerState.playMusicInfo.listId!,\n    })\n  }\n\n  return (\n    <>\n      <Btn icon=\"add-music\" onPress={handleShowMusicAddModal} />\n      <MusicAddModal ref={musicAddModalRef} />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/PlayModeBtn.tsx",
    "content": "import { memo, useMemo } from 'react'\nimport { toast } from '@/utils/tools'\nimport { MUSIC_TOGGLE_MODE_LIST, MUSIC_TOGGLE_MODE } from '@/config/constant'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { useI18n } from '@/lang'\nimport { updateSetting } from '@/core/common'\nimport Btn from './Btn'\n\n\nexport default memo(() => {\n  const togglePlayMethod = useSettingValue('player.togglePlayMethod')\n  const t = useI18n()\n\n  const toggleNextPlayMode = () => {\n    let index = MUSIC_TOGGLE_MODE_LIST.indexOf(togglePlayMethod)\n    if (++index >= MUSIC_TOGGLE_MODE_LIST.length) index = 0\n    const mode = MUSIC_TOGGLE_MODE_LIST[index]\n    updateSetting({ 'player.togglePlayMethod': mode })\n    let modeName: 'play_list_loop' | 'play_list_random' | 'play_list_order' | 'play_single_loop' | 'play_single'\n    switch (mode) {\n      case MUSIC_TOGGLE_MODE.listLoop:\n        modeName = 'play_list_loop'\n        break\n      case MUSIC_TOGGLE_MODE.random:\n        modeName = 'play_list_random'\n        break\n      case MUSIC_TOGGLE_MODE.list:\n        modeName = 'play_list_order'\n        break\n      case MUSIC_TOGGLE_MODE.singleLoop:\n        modeName = 'play_single_loop'\n        break\n      default:\n        modeName = 'play_single'\n        break\n    }\n    toast(t(modeName))\n  }\n\n  const playModeIcon = useMemo(() => {\n    let playModeIcon = null\n    switch (togglePlayMethod) {\n      case MUSIC_TOGGLE_MODE.listLoop:\n        playModeIcon = 'list-loop'\n        break\n      case MUSIC_TOGGLE_MODE.random:\n        playModeIcon = 'list-random'\n        break\n      case MUSIC_TOGGLE_MODE.list:\n        playModeIcon = 'list-order'\n        break\n      case MUSIC_TOGGLE_MODE.singleLoop:\n        playModeIcon = 'single-loop'\n        break\n      default:\n        playModeIcon = 'single'\n        break\n    }\n    return playModeIcon\n  }, [togglePlayMethod])\n\n  return <Btn icon={playModeIcon} onPress={toggleNextPlayMode} />\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/TimeoutExitBtn.tsx",
    "content": "import { memo, useRef } from 'react'\nimport TimeoutExitEditModal, { type TimeoutExitEditModalType, useTimeInfo } from '@/components/TimeoutExitEditModal'\nimport { useTheme } from '@/store/theme/hook'\nimport Btn from './Btn'\n\n\nexport default memo(() => {\n  const theme = useTheme()\n  const modalRef = useRef<TimeoutExitEditModalType>(null)\n\n  const timeInfo = useTimeInfo()\n\n  const handleShow = () => {\n    modalRef.current?.show()\n  }\n\n  return (\n    <>\n      <Btn icon=\"music_time\" color={timeInfo.active ? theme['c-primary-font-active'] : theme['c-font-label']} onPress={handleShow} />\n      <TimeoutExitEditModal ref={modalRef} timeInfo={timeInfo} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/MoreBtn/index.tsx",
    "content": "import { createStyle } from '@/utils/tools'\nimport { View } from 'react-native'\nimport PlayModeBtn from './PlayModeBtn'\nimport MusicAddBtn from './MusicAddBtn'\nimport DesktopLyricBtn from './DesktopLyricBtn'\nimport CommentBtn from './CommentBtn'\n\nexport default () => {\n  return (\n    <View style={styles.container}>\n      <DesktopLyricBtn />\n      <MusicAddBtn />\n      <PlayModeBtn />\n      <CommentBtn />\n    </View>\n  )\n}\n\n\nconst styles = createStyle({\n  container: {\n    // flexShrink: 0,\n    // flexGrow: 0,\n    width: '100%',\n    flexDirection: 'row',\n    alignItems: 'center',\n    justifyContent: 'space-around',\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/PlayInfo.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\nimport Progress from '@/components/player/ProgressBar'\nimport Status from './Status'\nimport { useProgress } from '@/store/player/hook'\nimport { useTheme } from '@/store/theme/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\nimport { useBufferProgress } from '@/plugins/player'\n\n// const FONT_SIZE = 13\n\nconst PlayTimeCurrent = ({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  // console.log(timeStr)\n  return <Text color={theme['c-500']}>{timeStr}</Text>\n}\n\nconst PlayTimeMax = memo(({ timeStr }: { timeStr: string }) => {\n  const theme = useTheme()\n  return <Text color={theme['c-500']}>{timeStr}</Text>\n})\n\nexport default () => {\n  const { maxPlayTimeStr, nowPlayTimeStr, progress, maxPlayTime } = useProgress()\n  const buffered = useBufferProgress()\n\n  // console.log('render playInfo')\n\n  return (\n    <>\n      <View style={styles.progress}><Progress progress={progress} duration={maxPlayTime} buffered={buffered} /></View>\n      <View style={styles.info}>\n        <PlayTimeCurrent timeStr={nowPlayTimeStr} />\n        <View style={styles.status} >\n          <Status />\n        </View>\n        <PlayTimeMax timeStr={maxPlayTimeStr} />\n      </View>\n    </>\n  )\n}\n\n\nconst styles = createStyle({\n  progress: {\n    flexGrow: 1,\n    flexShrink: 0,\n    flexDirection: 'column',\n    justifyContent: 'center',\n  },\n  info: {\n    flexDirection: 'row',\n    justifyContent: 'space-between',\n    // alignItems: 'center',\n    // backgroundColor: '#ccc',\n  },\n  status: {\n    flexGrow: 1,\n    flexShrink: 1,\n    paddingLeft: 10,\n    paddingRight: 10,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/components/Status.tsx",
    "content": "// import { useLrcPlay } from '@/plugins/lyric'\nimport { useStatusText } from '@/store/player/hook'\nimport { createStyle } from '@/utils/tools'\nimport Text from '@/components/common/Text'\n\n\nexport default () => {\n  // const { text } = useLrcPlay()\n  const statusText = useStatusText()\n  // console.log('render status')\n\n  // const status = playerStatus.isPlay ? text : playerStatus.statusText\n\n  return <Text style={styles.text} numberOfLines={1} size={13}>{statusText}</Text>\n}\n\nconst styles = createStyle({\n  text: {\n    textAlign: 'center',\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/Player/index.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\n\n// import Title from './components/Title'\nimport MoreBtn from './components/MoreBtn'\nimport PlayInfo from './components/PlayInfo'\nimport ControlBtn from './components/ControlBtn'\nimport { createStyle } from '@/utils/tools'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\n\n\nexport default memo(() => {\n  return (\n    <View style={styles.container} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_player}>\n      <PlayInfo />\n      <ControlBtn />\n      <MoreBtn />\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 0,\n    width: '100%',\n    // paddingTop: progressContentPadding,\n    // marginTop: -progressContentPadding,\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n    paddingHorizontal: 15,\n    paddingBottom: 15,\n    paddingTop: 5,\n    // backgroundColor: AppColors.primary,\n    // backgroundColor: 'red',\n    flexDirection: 'column',\n  },\n  status: {\n    marginTop: 10,\n    flexDirection: 'column',\n    flex: 0,\n    paddingLeft: 5,\n    justifyContent: 'space-evenly',\n    // backgroundColor: 'rgba(0, 0, 0, .1)',\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/components/Btn.tsx",
    "content": "import { TouchableOpacity } from 'react-native'\nimport { Icon } from '@/components/common/Icon'\nimport { createStyle } from '@/utils/tools'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { HEADER_HEIGHT as _HEADER_HEIGHT } from '@/config/constant'\n\nexport const HEADER_HEIGHT = scaleSizeH(_HEADER_HEIGHT)\n\nexport default ({ icon, color, onPress }: {\n  icon: string\n  color?: string\n  onPress: () => void\n}) => {\n  return (\n    <TouchableOpacity onPress={onPress} style={{ ...styles.button, width: HEADER_HEIGHT }}>\n      <Icon name={icon} color={color} size={18} />\n    </TouchableOpacity>\n  )\n}\n\nconst styles = createStyle({\n  button: {\n    justifyContent: 'center',\n    alignItems: 'center',\n    height: '100%',\n    flex: 0,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/components/Header.tsx",
    "content": "import { memo, useRef } from 'react'\n\nimport { View, StyleSheet } from 'react-native'\n\nimport { pop } from '@/navigation'\nimport StatusBar from '@/components/common/StatusBar'\nimport { useTheme } from '@/store/theme/hook'\nimport { usePlayerMusicInfo } from '@/store/player/hook'\nimport Text from '@/components/common/Text'\nimport { scaleSizeH } from '@/utils/pixelRatio'\nimport { HEADER_HEIGHT as _HEADER_HEIGHT, NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport commonState from '@/store/common/state'\nimport SettingPopup, { type SettingPopupType } from '../../components/SettingPopup'\nimport { useStatusbarHeight } from '@/store/common/hook'\nimport Btn from './Btn'\nimport TimeoutExitBtn from './TimeoutExitBtn'\n\nexport const HEADER_HEIGHT = scaleSizeH(_HEADER_HEIGHT)\n\n\nconst Title = () => {\n  const theme = useTheme()\n  const musicInfo = usePlayerMusicInfo()\n\n\n  return (\n    <View style={styles.titleContent}>\n      <Text numberOfLines={1} style={styles.title}>{musicInfo.name}</Text>\n      <Text numberOfLines={1} style={styles.title} size={12} color={theme['c-font-label']}>{musicInfo.singer}</Text>\n    </View>\n  )\n}\n\nexport default memo(() => {\n  const popupRef = useRef<SettingPopupType>(null)\n  const statusBarHeight = useStatusbarHeight()\n\n  const back = () => {\n    void pop(commonState.componentIds.playDetail!)\n  }\n  const showSetting = () => {\n    popupRef.current?.show()\n  }\n\n  return (\n    <View style={{ height: HEADER_HEIGHT + statusBarHeight, paddingTop: statusBarHeight }} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_header}>\n      <StatusBar />\n      <View style={styles.container}>\n        <Btn icon=\"chevron-left\" onPress={back} />\n        <Title />\n        <TimeoutExitBtn />\n        <Btn icon=\"slider\" onPress={showSetting} />\n      </View>\n      <SettingPopup ref={popupRef} direction=\"vertical\" />\n    </View>\n  )\n})\n\n\nconst styles = StyleSheet.create({\n  container: {\n    flexDirection: 'row',\n    // justifyContent: 'center',\n    height: '100%',\n  },\n  titleContent: {\n    flex: 1,\n    paddingHorizontal: 5,\n    // alignItems: 'center',\n    justifyContent: 'center',\n  },\n  title: {\n    // flex: 1,\n    // textAlign: 'center',\n  },\n  icon: {\n    paddingLeft: 4,\n    paddingRight: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/components/TimeoutExitBtn.tsx",
    "content": "import { memo, useRef } from 'react'\nimport TimeoutExitEditModal, { type TimeoutExitEditModalType, useTimeInfo } from '@/components/TimeoutExitEditModal'\nimport { useTheme } from '@/store/theme/hook'\nimport Btn from './Btn'\n\n\nexport default memo(() => {\n  const theme = useTheme()\n  const modalRef = useRef<TimeoutExitEditModalType>(null)\n\n  const timeInfo = useTimeInfo()\n\n  const handleShow = () => {\n    modalRef.current?.show()\n  }\n\n  return (\n    <>\n      <Btn icon=\"music_time\" color={timeInfo.active ? theme['c-primary-font-active'] : theme['c-font']} onPress={handleShow} />\n      <TimeoutExitEditModal ref={modalRef} timeInfo={timeInfo} />\n    </>\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/Vertical/index.tsx",
    "content": "import { memo, useState, useRef, useMemo, useEffect } from 'react'\nimport { View, AppState } from 'react-native'\n\nimport Header from './components/Header'\n// import Aside from './components/Aside'\n// import Main from './components/Main'\nimport Player from './Player'\nimport PagerView, { type PagerViewOnPageSelectedEvent } from 'react-native-pager-view'\nimport Pic from './Pic'\nimport Lyric from './Lyric'\nimport { screenkeepAwake, screenUnkeepAwake } from '@/utils/nativeModules/utils'\nimport commonState, { type InitState as CommonState } from '@/store/common/state'\nimport { createStyle } from '@/utils/tools'\n// import { useTheme } from '@/store/theme/hook'\n\nconst LyricPage = ({ activeIndex }: { activeIndex: number }) => {\n  const initedRef = useRef(false)\n  const lyric = useMemo(() => <Lyric />, [])\n  switch (activeIndex) {\n    // case 3:\n    case 1:\n      if (!initedRef.current) initedRef.current = true\n      return lyric\n    default:\n      return initedRef.current ? lyric : null\n  }\n  // return activeIndex == 0 || activeIndex == 1 ? setting : null\n}\n\n// global.iskeep = false\nexport default memo(({ componentId }: { componentId: string }) => {\n  // const theme = useTheme()\n  const [pageIndex, setPageIndex] = useState(0)\n  const showLyricRef = useRef(false)\n\n  const onPageSelected = ({ nativeEvent }: PagerViewOnPageSelectedEvent) => {\n    setPageIndex(nativeEvent.position)\n    showLyricRef.current = nativeEvent.position == 1\n    if (showLyricRef.current) {\n      screenkeepAwake()\n    } else {\n      screenUnkeepAwake()\n    }\n  }\n\n  useEffect(() => {\n    let appstateListener = AppState.addEventListener('change', (state) => {\n      switch (state) {\n        case 'active':\n          if (showLyricRef.current && !commonState.componentIds.comment) screenkeepAwake()\n          break\n        case 'background':\n          screenUnkeepAwake()\n          break\n      }\n    })\n\n    const handleComponentIdsChange = (ids: CommonState['componentIds']) => {\n      if (ids.comment) screenUnkeepAwake()\n      else if (AppState.currentState == 'active') screenkeepAwake()\n    }\n\n    global.state_event.on('componentIdsUpdated', handleComponentIdsChange)\n\n    return () => {\n      global.state_event.off('componentIdsUpdated', handleComponentIdsChange)\n      appstateListener.remove()\n      screenUnkeepAwake()\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <>\n      <Header />\n      <View style={styles.container}>\n        <PagerView\n          onPageSelected={onPageSelected}\n          // onPageScrollStateChanged={onPageScrollStateChanged}\n          style={styles.pagerView}\n        >\n          <View collapsable={false}>\n            <Pic componentId={componentId} />\n          </View>\n          <View collapsable={false}>\n            <LyricPage activeIndex={pageIndex} />\n          </View>\n        </PagerView>\n        {/* <View style={styles.pageIndicator} nativeID={NAV_SHEAR_NATIVE_IDS.playDetail_pageIndicator}>\n          <View style={{ ...styles.pageIndicatorItem, backgroundColor: pageIndex == 0 ? theme['c-primary-light-100-alpha-700'] : theme['c-primary-alpha-900'] }}></View>\n          <View style={{ ...styles.pageIndicatorItem, backgroundColor: pageIndex == 1 ? theme['c-primary-light-100-alpha-700'] : theme['c-primary-alpha-900'] }}></View>\n        </View> */}\n        <Player />\n      </View>\n    </>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flex: 1,\n    flexDirection: 'column',\n  },\n  pagerView: {\n    flex: 1,\n  },\n  // pageIndicator: {\n  //   flex: 0,\n  //   flexDirection: 'row',\n  //   justifyContent: 'center',\n  //   paddingTop: 10,\n  //   // backgroundColor: 'rgba(0,0,0,0.1)',\n  // },\n  // pageIndicatorItem: {\n  //   height: 3,\n  //   width: '5%',\n  //   marginLeft: 2,\n  //   marginRight: 2,\n  //   borderRadius: 2,\n  // },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/components/PlayLine.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { type NativeScrollEvent, type NativeSyntheticEvent, View, TouchableOpacity, Animated } from 'react-native'\nimport Text from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport { type Lines } from 'lrc-file-parser'\nimport { useTheme } from '@/store/theme/hook'\nimport { BorderWidths } from '@/theme'\nimport { formatPlayTime2 } from '@/utils'\nimport { Icon } from '@/components/common/Icon'\n\n\nexport interface PlayLineType {\n  updateScrollInfo: (scrollInfo: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null) => void\n  updateLayoutInfo: (listLayoutInfo: { spaceHeight: number, lineHeights: number[] }) => void\n  updateLyricLines: (lyricLines: Lines) => void\n  setVisible: (visible: boolean) => void\n}\n\nexport interface PlayLineProps {\n  onPlayLine: (time: number) => void\n}\n\nconst ANIMATION_DURATION = 300\n\nexport default forwardRef<PlayLineType, PlayLineProps>(({ onPlayLine }, ref) => {\n  const theme = useTheme()\n  const [scrollInfo, setScrollInfo] = useState<NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'] | null>(null)\n  const [listLayoutInfo, setListLayoutInfo] = useState<{ spaceHeight: number, lineHeights: number[] }>({ spaceHeight: 0, lineHeights: [] })\n  const [lyricLines, setLyricLines] = useState<Lines>([])\n  const [visible, setVisible] = useState(false)\n  const opsAnim = useRef<Animated.Value>(\n    new Animated.Value(0),\n  ).current\n\n  const setShow = (visible: boolean) => {\n    Animated.timing(opsAnim, {\n      toValue: visible ? 1 : 0,\n      duration: ANIMATION_DURATION,\n      useNativeDriver: true,\n    }).start(() => {\n      if (!visible) setVisible(false)\n    })\n  }\n\n  useImperativeHandle(ref, () => ({\n    updateScrollInfo(scrollInfo) {\n      setScrollInfo(scrollInfo)\n    },\n    updateLayoutInfo(listLayoutInfo) {\n      setListLayoutInfo(listLayoutInfo)\n    },\n    updateLyricLines(lyricLines) {\n      setLyricLines(lyricLines)\n    },\n    setVisible(visible) {\n      if (visible) {\n        setVisible(true)\n      }\n      requestAnimationFrame(() => {\n        setShow(visible)\n      })\n      // setVisible()\n    },\n  }))\n\n  const handlePlayLine = () => {\n    onPlayLine(time / 1000)\n  }\n\n  if (!scrollInfo || !visible) return null\n  const offset = scrollInfo.contentOffset.y + scrollInfo.layoutMeasurement.height * 0.4\n  let lineOffset = listLayoutInfo.spaceHeight\n  let targetLineNum = -1\n  for (let line = 0; line < listLayoutInfo.lineHeights.length; line++) {\n    lineOffset += listLayoutInfo.lineHeights[line]\n    if (lineOffset < offset) continue\n    targetLineNum = line\n    break\n  }\n  if (targetLineNum == -1) targetLineNum = listLayoutInfo.lineHeights.length - 1\n  const time = lyricLines[targetLineNum]?.time ?? 0\n  const timeLabel = formatPlayTime2(time / 1000)\n  return (\n    <Animated.View style={{ ...styles.playLine, opacity: opsAnim }}>\n      <Text style={styles.label} color={theme['c-primary-font']} size={13}>{timeLabel}</Text>\n      <View style={styles.lineContent}>\n        <View style={{ ...styles.line, borderBottomColor: theme['c-primary-alpha-700'] }} />\n        <TouchableOpacity style={styles.button} onPress={handlePlayLine}>\n          <Icon name=\"play\" color={theme['c-button-font']} size={18} />\n        </TouchableOpacity>\n      </View>\n    </Animated.View>\n  )\n})\n\nconst styles = createStyle({\n  playLine: {\n    position: 'absolute',\n    width: '100%',\n    top: '40%',\n    left: 0,\n    height: 2,\n    // paddingTop: 5,\n    // paddingBottom: 5,\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n  },\n  label: {\n    position: 'absolute',\n    right: 45,\n    bottom: 3,\n    flexDirection: 'row',\n    alignItems: 'center',\n    gap: 5,\n  },\n  lineContent: {\n    // backgroundColor: 'rgba(0,0,0,0.1)',\n    position: 'absolute',\n    width: '100%',\n    height: 20,\n    top: -10,\n    flexDirection: 'row',\n    alignItems: 'center',\n    gap: 5,\n  },\n  line: {\n    marginLeft: 30,\n    borderBottomWidth: BorderWidths.normal2,\n    borderStyle: 'dashed',\n    flex: 1,\n  },\n  button: {\n    flex: 0,\n    paddingLeft: 5,\n    paddingRight: 15,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/index.tsx",
    "content": "import { forwardRef, useImperativeHandle, useRef, useState } from 'react'\nimport { ScrollView, View } from 'react-native'\nimport Popup, { type PopupType, type PopupProps } from '@/components/common/Popup'\nimport { useI18n } from '@/lang'\n\nimport SettingLyricProgress from './settings/SettingLyricProgress'\nimport SettingVolume from './settings/SettingVolume'\nimport SettingPlaybackRate from './settings/SettingPlaybackRate'\nimport SettingLrcFontSize from './settings/SettingLrcFontSize'\nimport SettingLrcAlign from './settings/SettingLrcAlign'\n\nexport interface SettingPopupProps extends Omit<PopupProps, 'children'> {\n  direction: 'vertical' | 'horizontal'\n}\n\nexport interface SettingPopupType {\n  show: () => void\n}\n\nexport default forwardRef<SettingPopupType, SettingPopupProps>(({ direction, ...props }, ref) => {\n  const [visible, setVisible] = useState(false)\n  const popupRef = useRef<PopupType>(null)\n  // console.log('render import export')\n  const t = useI18n()\n\n  useImperativeHandle(ref, () => ({\n    show() {\n      if (visible) popupRef.current?.setVisible(true)\n      else {\n        setVisible(true)\n        requestAnimationFrame(() => {\n          popupRef.current?.setVisible(true)\n        })\n      }\n    },\n  }))\n\n\n  return (\n    visible\n      ? (\n        <Popup ref={popupRef} title={t('play_detail_setting_title')} {...props}>\n          <ScrollView>\n            <View onStartShouldSetResponder={() => true}>\n              <SettingLyricProgress />\n              <SettingVolume />\n              <SettingPlaybackRate />\n              <SettingLrcFontSize direction={direction} />\n              <SettingLrcAlign />\n            </View>\n          </ScrollView>\n        </Popup>\n        )\n      : null\n  )\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/SettingLrcAlign.tsx",
    "content": "import { useMemo } from 'react'\n\nimport { View } from 'react-native'\nimport Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport styles from './style'\nimport CheckBox from '@/components/common/CheckBox'\n\ntype Align_Type = LX.AppSetting['playDetail.style.align']\n\nconst ALIGN_LIST = [\n  'left',\n  'center',\n  'right',\n] as const\n\nconst useActive = (id: Align_Type) => {\n  const x = useSettingValue('playDetail.style.align')\n  const isActive = useMemo(() => x == id, [x, id])\n  return isActive\n}\n\nconst Item = ({ id, name, change }: {\n  id: Align_Type\n  name: string\n  change: (id: Align_Type) => void\n}) => {\n  const isActive = useActive(id)\n  // const [toggleCheckBox, setToggleCheckBox] = useState(false)\n  return <CheckBox marginBottom={3} check={isActive} label={name} onChange={() => { change(id) }} need />\n}\n\nexport default () => {\n  const t = useI18n()\n  const list = useMemo(() => {\n    return ALIGN_LIST.map(id => ({ id, name: t(`play_detail_setting_lrc_align_${id}`) }))\n  }, [t])\n\n  const setPosition = (id: Align_Type) => {\n    updateSetting({ 'playDetail.style.align': id })\n  }\n\n  return (\n    <View style={styles.container}>\n      <Text>{t('play_detail_setting_lrc_align')}</Text>\n      <View style={styles.content}>\n        <View style={styles.list}>\n          {\n            list.map(({ id, name }) => <Item name={name} id={id} key={id} change={setPosition} />)\n          }\n        </View>\n      </View>\n    </View>\n  )\n}\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/SettingLrcFontSize.tsx",
    "content": "import { useState } from 'react'\n\nimport { View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\nimport Slider, { type SliderProps } from '@/components/common/Slider'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport styles from './style'\n\n\nconst LrcFontSize = ({ direction }: {\n  direction: 'horizontal' | 'vertical'\n}) => {\n  const theme = useTheme()\n  const settingKey = direction == 'horizontal' ? 'playDetail.horizontal.style.lrcFontSize' : 'playDetail.vertical.style.lrcFontSize'\n  const lrcFontSize = useSettingValue(settingKey)\n  const [sliderSize, setSliderSize] = useState(lrcFontSize)\n  const [isSliding, setSliding] = useState(false)\n  const t = useI18n()\n\n  const handleSlidingStart: SliderProps['onSlidingStart'] = value => {\n    setSliding(true)\n  }\n  const handleValueChange: SliderProps['onValueChange'] = value => {\n    setSliderSize(value)\n  }\n  const handleSlidingComplete: SliderProps['onSlidingComplete'] = value => {\n    setSliding(false)\n    if (lrcFontSize == value) return\n    updateSetting({ [settingKey]: value })\n  }\n\n  return (\n    <View style={styles.container}>\n      <Text>{t('play_detail_setting_lrc_font_size')}</Text>\n      <View style={styles.content}>\n        <Text style={styles.label} color={theme['c-font-label']}>{isSliding ? sliderSize : lrcFontSize}</Text>\n        <Slider\n          minimumValue={100}\n          maximumValue={300}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={2}\n          value={lrcFontSize}\n        />\n      </View>\n    </View>\n  )\n}\n\nexport default LrcFontSize\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/SettingLyricProgress.tsx",
    "content": "import { View } from 'react-native'\n// import Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport CheckBox from '@/components/common/CheckBox'\nimport styles from './style'\n\n\nexport default () => {\n  const t = useI18n()\n  const isShowLyricProgressSetting = useSettingValue('playDetail.isShowLyricProgressSetting')\n  const setShowLyricProgressSetting = (showLyricProgressSetting: boolean) => {\n    updateSetting({ 'playDetail.isShowLyricProgressSetting': showLyricProgressSetting })\n  }\n\n  return (\n    <View style={styles.container}>\n      <View style={styles.content}>\n        <View style={styles.content}>\n          <CheckBox marginBottom={3} check={isShowLyricProgressSetting} label={t('play_detail_setting_show_lyric_progress_setting')} onChange={setShowLyricProgressSetting} />\n        </View>\n      </View>\n    </View>\n\n  )\n}\n\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/SettingPlaybackRate.tsx",
    "content": "import { useState } from 'react'\n\nimport { View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\nimport Slider, { type SliderProps } from '@/components/common/Slider'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport styles from './style'\nimport { setPlaybackRate, updateMetaData } from '@/plugins/player'\nimport { setPlaybackRate as setLyricPlaybackRate } from '@/core/lyric'\nimport ButtonPrimary from '@/components/common/ButtonPrimary'\nimport playerState from '@/store/player/state'\nimport settingState from '@/store/setting/state'\n\nconst MIN_VALUE = 60\nconst MAX_VALUE = 200\n\nexport default () => {\n  const theme = useTheme()\n  const playbackRate = Math.trunc(useSettingValue('player.playbackRate') * 100)\n  const [sliderSize, setSliderSize] = useState(playbackRate)\n  const [isSliding, setSliding] = useState(false)\n  const t = useI18n()\n\n  const handleSlidingStart: SliderProps['onSlidingStart'] = value => {\n    setSliding(true)\n  }\n  const handleValueChange: SliderProps['onValueChange'] = value => {\n    value = Math.trunc(value)\n    setSliderSize(value)\n    void setPlaybackRate(parseFloat((value / 100).toFixed(2)))\n  }\n  const handleSlidingComplete: SliderProps['onSlidingComplete'] = value => {\n    setSliding(false)\n    value = Math.trunc(value)\n    const rate = value / 100\n    void setLyricPlaybackRate(rate)\n    void updateMetaData(playerState.musicInfo, playerState.isPlay, playerState.lastLyric, true) // 更新通知栏的播放速率\n    if (playbackRate == value) return\n    updateSetting({ 'player.playbackRate': rate })\n  }\n  const handleReset = () => {\n    if (settingState.setting['player.playbackRate'] == 1) return\n    setSliderSize(100)\n    void setPlaybackRate(1).then(() => {\n      void updateMetaData(playerState.musicInfo, playerState.isPlay, playerState.lastLyric, true) // 更新通知栏的播放速率\n      void setLyricPlaybackRate(1)\n    })\n    updateSetting({ 'player.playbackRate': 1 })\n  }\n\n  return (\n    <View style={styles.container}>\n      <Text>{t('play_detail_setting_playback_rate')}</Text>\n      <View style={styles.content}>\n        <Text style={styles.label} color={theme['c-font-label']}>{`${((isSliding ? sliderSize : playbackRate) / 100).toFixed(2)}x`}</Text>\n        <Slider\n          minimumValue={MIN_VALUE}\n          maximumValue={MAX_VALUE}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={1}\n          value={playbackRate}\n        />\n      </View>\n      <ButtonPrimary onPress={handleReset}>{t('play_detail_setting_playback_rate_reset')}</ButtonPrimary>\n    </View>\n  )\n}\n\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/SettingVolume.tsx",
    "content": "import { useState } from 'react'\n\nimport { View } from 'react-native'\nimport { useTheme } from '@/store/theme/hook'\nimport Text from '@/components/common/Text'\nimport { useSettingValue } from '@/store/setting/hook'\nimport Slider, { type SliderProps } from '@/components/common/Slider'\nimport { updateSetting } from '@/core/common'\nimport { useI18n } from '@/lang'\nimport styles from './style'\nimport { setVolume } from '@/plugins/player'\n\n\nconst Volume = () => {\n  const theme = useTheme()\n  const volume = Math.trunc(useSettingValue('player.volume') * 100)\n  const [sliderSize, setSliderSize] = useState(volume)\n  const [isSliding, setSliding] = useState(false)\n  const t = useI18n()\n\n  const handleSlidingStart: SliderProps['onSlidingStart'] = value => {\n    setSliding(true)\n  }\n  const handleValueChange: SliderProps['onValueChange'] = value => {\n    value = Math.trunc(value)\n    setSliderSize(value)\n    void setVolume(value / 100)\n  }\n  const handleSlidingComplete: SliderProps['onSlidingComplete'] = value => {\n    setSliding(false)\n    value = Math.trunc(value)\n    if (volume == value) return\n    updateSetting({ 'player.volume': value / 100 })\n  }\n\n  return (\n    <View style={styles.container}>\n      <Text>{t('play_detail_setting_volume')}</Text>\n      <View style={styles.content}>\n        <Text style={styles.label} color={theme['c-font-label']}>{isSliding ? sliderSize : volume}</Text>\n        <Slider\n          minimumValue={0}\n          maximumValue={100}\n          onSlidingComplete={handleSlidingComplete}\n          onValueChange={handleValueChange}\n          onSlidingStart={handleSlidingStart}\n          step={1}\n          value={volume}\n        />\n      </View>\n    </View>\n  )\n}\n\nexport default Volume\n"
  },
  {
    "path": "src/screens/PlayDetail/components/SettingPopup/settings/style.ts",
    "content": "import { createStyle } from '@/utils/tools'\n\nexport default createStyle({\n  container: {\n    paddingTop: 5,\n    paddingLeft: 15,\n    paddingRight: 15,\n    paddingBottom: 15,\n    alignItems: 'flex-start',\n  },\n  // title: {\n\n  // },\n  label: {\n    width: 50,\n    textAlign: 'center',\n  },\n  content: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'nowrap',\n    alignItems: 'center',\n  },\n  list: {\n    flexGrow: 0,\n    flexShrink: 1,\n    flexDirection: 'row',\n    flexWrap: 'wrap',\n    paddingTop: 5,\n  },\n})\n"
  },
  {
    "path": "src/screens/PlayDetail/index.tsx",
    "content": "import { useEffect } from 'react'\n// import { View, StyleSheet } from 'react-native'\nimport { useHorizontalMode } from '@/utils/hooks'\n\nimport Vertical from './Vertical'\nimport Horizontal from './Horizontal'\nimport PageContent from '@/components/PageContent'\nimport StatusBar from '@/components/common/StatusBar'\nimport { setComponentId } from '@/core/common'\nimport { COMPONENT_IDS } from '@/config/constant'\n\nexport default ({ componentId }: { componentId: string }) => {\n  const isHorizontalMode = useHorizontalMode()\n\n  useEffect(() => {\n    setComponentId(COMPONENT_IDS.playDetail, componentId)\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <PageContent>\n      <StatusBar />\n      {\n        isHorizontalMode\n          ? <Horizontal componentId={componentId} />\n          : <Vertical componentId={componentId} />\n      }\n    </PageContent>\n  )\n}\n"
  },
  {
    "path": "src/screens/SonglistDetail/ActionBar.tsx",
    "content": "import { memo } from 'react'\nimport { View } from 'react-native'\nimport Button from '@/components/common/Button'\n\nimport { createStyle } from '@/utils/tools'\nimport { pop } from '@/navigation'\nimport { useTheme } from '@/store/theme/hook'\nimport commonState from '@/store/common/state'\nimport Text from '@/components/common/Text'\nimport { handleCollect, handlePlay } from './listAction'\nimport songlistState from '@/store/songlist/state'\nimport { useI18n } from '@/lang'\nimport { useListInfo } from './state'\n// import { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\n\nexport default memo(() => {\n  const theme = useTheme()\n  const t = useI18n()\n  const info = useListInfo()\n\n  const back = () => {\n    void pop(commonState.componentIds.songlistDetail!)\n  }\n\n  const handlePlayAll = () => {\n    if (!songlistState.listDetailInfo.info.name) return\n    void handlePlay(info.id, info.source, songlistState.listDetailInfo.list)\n  }\n\n  const handleCollection = () => {\n    if (!songlistState.listDetailInfo.info.name) return\n    void handleCollect(info.id, info.source, songlistState.listDetailInfo.info.name || info.name)\n  }\n\n  return (\n    <View style={styles.container}>\n      <Button onPress={handleCollection} style={styles.controlBtn}>\n        <Text style={{ ...styles.controlBtnText, color: theme['c-button-font'] }}>{t('collect_songlist')}</Text>\n      </Button>\n      <Button onPress={handlePlayAll} style={styles.controlBtn}>\n        <Text style={{ ...styles.controlBtnText, color: theme['c-button-font'] }}>{t('play_all')}</Text>\n      </Button>\n      <Button onPress={back} style={styles.controlBtn}>\n        <Text style={{ ...styles.controlBtnText, color: theme['c-button-font'] }}>{t('back')}</Text>\n      </Button>\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexDirection: 'row',\n    width: '100%',\n    flexGrow: 0,\n    flexShrink: 0,\n  },\n  controlBtn: {\n    flexGrow: 1,\n    flexShrink: 1,\n    width: '33%',\n    paddingTop: 12,\n    paddingBottom: 12,\n    paddingLeft: 10,\n    paddingRight: 10,\n  },\n  controlBtnText: {\n    fontSize: 13,\n    textAlign: 'center',\n  },\n})\n\n"
  },
  {
    "path": "src/screens/SonglistDetail/Header.tsx",
    "content": "import { forwardRef, memo, useEffect, useImperativeHandle, useState } from 'react'\nimport { View } from 'react-native'\nimport { BorderWidths } from '@/theme'\nimport ButtonBar from './ActionBar'\nimport { useNavigationComponentDidAppear } from '@/navigation'\nimport { NAV_SHEAR_NATIVE_IDS } from '@/config/constant'\nimport { scaleSizeW } from '@/utils/pixelRatio'\nimport { useTheme } from '@/store/theme/hook'\nimport Text, { AnimatedText } from '@/components/common/Text'\nimport { createStyle } from '@/utils/tools'\nimport Image from '@/components/common/Image'\nimport { useListInfo } from './state'\nimport { useAnimateOnecNumber } from '@/utils/hooks/useAnimateNumber'\nimport { useStatusbarHeight } from '@/store/common/hook'\n\nconst IMAGE_WIDTH = scaleSizeW(70)\n\nconst CountText = memo(({ count }: { count: string }) => {\n  const [animFade] = useAnimateOnecNumber(0, 1, 250, false)\n  const [animTranslateY] = useAnimateOnecNumber(10, 0, 250, false)\n  return (\n    <AnimatedText style={{\n      ...styles.playCount,\n      opacity: animFade,\n      transform: [\n        { translateY: animTranslateY },\n      ],\n    }} numberOfLines={ 1 }>{count}</AnimatedText>\n  )\n}, (prevProps, nextProps) => {\n  return true\n})\n\nconst Pic = ({ componentId, playCount, imgUrl }: {\n  componentId: string\n  playCount: string\n  imgUrl?: string\n}) => {\n  const [pic, setPic] = useState(imgUrl)\n  const [animated, setAnimated] = useState(false)\n  const info = useListInfo()\n  useEffect(() => {\n    if (animated) setPic(imgUrl)\n  }, [imgUrl, animated])\n\n  useNavigationComponentDidAppear(componentId, () => {\n    setAnimated(true)\n  })\n\n  return (\n    <View style={{ ...styles.listItemImg, width: IMAGE_WIDTH, height: IMAGE_WIDTH }}>\n      <Image nativeID={`${NAV_SHEAR_NATIVE_IDS.songlistDetail_pic}_to_${info.id}`} url={pic} style={{ flex: 1, borderRadius: 4 }} />\n      {\n        playCount && animated ? <CountText count={playCount} /> : null\n      }\n    </View>\n  )\n}\n\nexport interface HeaderProps {\n  componentId: string\n}\n\nexport interface HeaderType {\n  setInfo: (info: DetailInfo) => void\n}\nexport interface DetailInfo {\n  name: string\n  desc: string\n  playCount: string\n  imgUrl?: string\n}\n\nexport default forwardRef<HeaderType, HeaderProps>(({ componentId }: { componentId: string }, ref) => {\n  const statusBarHeight = useStatusbarHeight()\n  const theme = useTheme()\n  const info = useListInfo()\n  const [detailInfo, setDetailInfo] = useState<DetailInfo>({ name: '', desc: '', playCount: '', imgUrl: info.img })\n\n  useImperativeHandle(ref, () => ({\n    setInfo(info) {\n      setDetailInfo(info)\n    },\n  }), [])\n\n  return (\n    <View style={{ ...styles.container, paddingTop: statusBarHeight, borderBottomColor: theme['c-border-background'] }}>\n      <View style={{ flexDirection: 'row', flexGrow: 0, flexShrink: 0, padding: 10 }}>\n        <Pic componentId={componentId} playCount={detailInfo.playCount} imgUrl={detailInfo.imgUrl} />\n        <View style={{ flexDirection: 'column', flexGrow: 1, flexShrink: 1, paddingLeft: 5 }} nativeID={NAV_SHEAR_NATIVE_IDS.songlistDetail_title}>\n          <Text size={14} numberOfLines={ 1 }>{detailInfo.name}</Text>\n          <View style={{ flexGrow: 0, flexShrink: 1 }}>\n            <Text size={13} color={theme['c-font-label']} numberOfLines={ 4 }>{detailInfo.desc}</Text>\n          </View>\n        </View>\n      </View>\n      <ButtonBar />\n      {/* <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>\n        <View style={{ flexGrow: 0, flexShrink: 1, paddingTop: 5, paddingRight: 5 }}>\n              <Text style={{ fontSize: 12, color: AppColors.normal20 }} numberOfLines={ 1 }>{playCount || '-'}</Text>\n              <Text style={{ fontSize: 12, color: AppColors.normal30 }} numberOfLines={ 1 }>{this.props.selectListInfo.author || this.props.listDetailData.info.author}</Text>\n            </View>\n      </View> */}\n    </View>\n  )\n})\n\nconst styles = createStyle({\n  container: {\n    flexDirection: 'column',\n    flexWrap: 'nowrap',\n    borderBottomWidth: BorderWidths.normal,\n  },\n  listItemImg: {\n    // backgroundColor: '#eee',\n    flexGrow: 0,\n    flexShrink: 0,\n    overflow: 'hidden',\n    // width: 70,\n    // height: 70,\n    // ...Platform.select({\n    //   ios: {\n    //     shadowColor: '#000',\n    //     shadowOffset: {\n    //       width: 0,\n    //       height: 1,\n    //     },\n    //     shadowOpacity: 0.20,\n    //     shadowRadius: 1.41,\n    //   },\n    //   android: {\n    //     elevation: 2,\n    //   },\n    // }),\n  },\n  playCount: {\n    position: 'absolute',\n    bottom: 0,\n    left: 0,\n    width: '100%',\n    fontSize: 12,\n    paddingLeft: 3,\n    paddingRight: 3,\n    backgroundColor: 'rgba(0, 0, 0, 0.5)',\n    color: '#fff',\n    borderBottomLeftRadius: 4,\n    borderBottomRightRadius: 4,\n  },\n})\n"
  },
  {
    "path": "src/screens/SonglistDetail/MusicList.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'\nimport OnlineList, { type OnlineListType, type OnlineListProps } from '@/components/OnlineList'\nimport { clearListDetail, getListDetail, setListDetail, setListDetailInfo } from '@/core/songlist'\nimport songlistState from '@/store/songlist/state'\nimport { handlePlay } from './listAction'\nimport Header, { type HeaderType } from './Header'\nimport { useListInfo } from './state'\n\nexport interface MusicListProps {\n  componentId: string\n}\n\nexport interface MusicListType {\n  loadList: (source: LX.OnlineSource, listId: string) => void\n}\n\nexport default forwardRef<MusicListType, MusicListProps>(({ componentId }, ref) => {\n  const listRef = useRef<OnlineListType>(null)\n  const headerRef = useRef<HeaderType>(null)\n  const isUnmountedRef = useRef(false)\n  const info = useListInfo()\n\n  useImperativeHandle(ref, () => ({\n    async loadList(source, id) {\n      clearListDetail()\n      const listDetailInfo = songlistState.listDetailInfo\n      listRef.current?.setList([])\n      if (listDetailInfo.id == id && listDetailInfo.source == source && listDetailInfo.list.length) {\n        requestAnimationFrame(() => {\n          listRef.current?.setList(listDetailInfo.list)\n          headerRef.current?.setInfo({\n            name: (info.name || listDetailInfo.info.name) ?? '',\n            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n            desc: listDetailInfo.info.desc || info.desc || '',\n            playCount: (info.play_count ?? listDetailInfo.info.play_count) ?? '',\n            imgUrl: info.img ?? listDetailInfo.info.img,\n          })\n        })\n      } else {\n        listRef.current?.setStatus('loading')\n        const page = 1\n        setListDetailInfo(info.source, info.id)\n        headerRef.current?.setInfo({\n          name: (info.name || listDetailInfo.info.name) ?? '',\n          // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n          desc: listDetailInfo.info.desc || info.desc || '',\n          playCount: (info.play_count ?? listDetailInfo.info.play_count) ?? '',\n          imgUrl: info.img ?? listDetailInfo.info.img,\n        })\n        return getListDetail(id, source, page).then((listDetail) => {\n          const result = setListDetail(listDetail, id, page)\n          if (isUnmountedRef.current) return\n          requestAnimationFrame(() => {\n            headerRef.current?.setInfo({\n              name: (info.name || listDetailInfo.info.name) ?? '',\n              // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing\n              desc: listDetailInfo.info.desc || info.desc || '',\n              playCount: (info.play_count ?? listDetailInfo.info.play_count) ?? '',\n              imgUrl: info.img ?? listDetailInfo.info.img,\n            })\n            listRef.current?.setList(result.list)\n            listRef.current?.setStatus(songlistState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n          })\n        }).catch(() => {\n          if (songlistState.listDetailInfo.list.length && page == 1) clearListDetail()\n          listRef.current?.setStatus('error')\n        })\n      }\n    },\n  }))\n\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n\n  const handlePlayList: OnlineListProps['onPlayList'] = (index) => {\n    const listDetailInfo = songlistState.listDetailInfo\n    // console.log(songlistState.listDetailInfo)\n    void handlePlay(listDetailInfo.id, listDetailInfo.source, listDetailInfo.list, index)\n  }\n  const handleRefresh: OnlineListProps['onRefresh'] = () => {\n    const page = 1\n    listRef.current?.setStatus('refreshing')\n    getListDetail(songlistState.listDetailInfo.id, songlistState.listDetailInfo.source, page, true).then((listDetail) => {\n      const result = setListDetail(listDetail, songlistState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list)\n      listRef.current?.setStatus(songlistState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (songlistState.listDetailInfo.list.length && page == 1) clearListDetail()\n      listRef.current?.setStatus('error')\n    })\n  }\n  const handleLoadMore: OnlineListProps['onLoadMore'] = () => {\n    listRef.current?.setStatus('loading')\n    const page = songlistState.listDetailInfo.list.length ? songlistState.listDetailInfo.page + 1 : 1\n    getListDetail(songlistState.listDetailInfo.id, songlistState.listDetailInfo.source, page).then((listDetail) => {\n      const result = setListDetail(listDetail, songlistState.listDetailInfo.id, page)\n      if (isUnmountedRef.current) return\n      listRef.current?.setList(result.list, true)\n      listRef.current?.setStatus(songlistState.listDetailInfo.maxPage <= page ? 'end' : 'idle')\n    }).catch(() => {\n      if (songlistState.listDetailInfo.list.length && page == 1) clearListDetail()\n      listRef.current?.setStatus('error')\n    })\n  }\n\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const header = useMemo(() => <Header ref={headerRef} componentId={componentId} />, [])\n\n  return <OnlineList\n    ref={listRef}\n    onPlayList={handlePlayList}\n    onRefresh={handleRefresh}\n    onLoadMore={handleLoadMore}\n    ListHeaderComponent={header}\n    // progressViewOffset={}\n   />\n})\n\n"
  },
  {
    "path": "src/screens/SonglistDetail/index.tsx",
    "content": "import { useEffect, useRef } from 'react'\n\nimport MusicList, { type MusicListType } from './MusicList'\nimport PageContent from '@/components/PageContent'\nimport StatusBar from '@/components/common/StatusBar'\nimport { setComponentId } from '@/core/common'\nimport { COMPONENT_IDS } from '@/config/constant'\nimport { type ListInfoItem } from '@/store/songlist/state'\nimport PlayerBar from '@/components/player/PlayerBar'\nimport { ListInfoContext } from './state'\n\n\nexport default ({ componentId, info }: { componentId: string, info: ListInfoItem }) => {\n  const musicListRef = useRef<MusicListType>(null)\n  const isUnmountedRef = useRef(false)\n\n  useEffect(() => {\n    setComponentId(COMPONENT_IDS.songlistDetail, componentId)\n\n    isUnmountedRef.current = false\n\n    musicListRef.current?.loadList(info.source, info.id)\n\n\n    return () => {\n      isUnmountedRef.current = true\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n\n  return (\n    <PageContent>\n      <StatusBar />\n      <ListInfoContext.Provider value={info}>\n        <MusicList ref={musicListRef} componentId={componentId} />\n      </ListInfoContext.Provider>\n      <PlayerBar />\n    </PageContent>\n  )\n}\n\n// const styles = createStyle({\n//   container: {\n//     width: '100%',\n//     flex: 1,\n//     flexDirection: 'row',\n//     borderTopWidth: BorderWidths.normal,\n//   },\n//   content: {\n//     flex: 1,\n//   },\n// })\n"
  },
  {
    "path": "src/screens/SonglistDetail/listAction.ts",
    "content": "import { createList, setTempList } from '@/core/list'\nimport { playList } from '@/core/player/player'\nimport { getListDetail, getListDetailAll } from '@/core/songlist'\nimport { LIST_IDS } from '@/config/constant'\nimport listState from '@/store/list/state'\nimport syncSourceList from '@/core/syncSourceList'\nimport { confirmDialog, toMD5, toast } from '@/utils/tools'\nimport { type Source } from '@/store/songlist/state'\n\nconst getListId = (id: string, source: LX.OnlineSource) => `${source}__${id}`\n\nexport const handlePlay = async(id: string, source: Source, list?: LX.Music.MusicInfoOnline[], index = 0) => {\n  const listId = getListId(id, source)\n  let isPlayingList = false\n  // console.log(list)\n  if (!list?.length) list = (await getListDetail(id, source, 1)).list\n  if (list?.length) {\n    await setTempList(listId, [...list])\n    void playList(LIST_IDS.TEMP, index)\n    isPlayingList = true\n  }\n  const fullList = await getListDetailAll(source, id)\n  if (!fullList.length) return\n  if (isPlayingList) {\n    if (listState.tempListMeta.id == listId) {\n      await setTempList(listId, [...fullList])\n    }\n  } else {\n    await setTempList(listId, [...fullList])\n    void playList(LIST_IDS.TEMP, index)\n  }\n}\n\nexport const handleCollect = async(id: string, source: Source, name: string) => {\n  const listId = getListId(id, source)\n\n  const targetList = listState.userList.find(l => l.sourceListId == listId)\n  if (targetList) {\n    const confirm = await confirmDialog({\n      message: global.i18n.t('duplicate_list_tip', { name: targetList.name }),\n      cancelButtonText: global.i18n.t('list_import_part_button_cancel'),\n      confirmButtonText: global.i18n.t('confirm_button_text'),\n    })\n    if (!confirm) return\n    void syncSourceList(targetList)\n    return\n  }\n\n  const list = await getListDetailAll(source, id)\n  await createList({\n    name,\n    id: `${source}_${toMD5(listId)}`,\n    list,\n    source,\n    sourceListId: id,\n  })\n  toast(global.i18n.t('collect_success'))\n}\n"
  },
  {
    "path": "src/screens/SonglistDetail/state.ts",
    "content": "import { type ListInfoItem } from '@/store/songlist/state'\nimport { createContext, useContext } from 'react'\n\nexport const ListInfoContext = createContext<ListInfoItem>({\n  id: '',\n  author: '',\n  name: '',\n  source: 'kw',\n})\n\nexport const useListInfo = () => {\n  return useContext(ListInfoContext)\n}\n"
  },
  {
    "path": "src/screens/index.ts",
    "content": "export { default as Home } from './Home'\nexport { default as PlayDetail } from './PlayDetail'\nexport { default as SonglistDetail } from './SonglistDetail'\nexport { default as Comment } from './Comment'\n// export { default as Setting } from './Setting'\n// export { default as LoginScreen } from './LoginScreen/LoginScreen'\n// export { default as SingleAppScreen } from './SingleAppScreen/SingleAppScreen'\n// export { default as Tab1Screen } from './Tab1Screen/Tab1Screen'\n// export { default as Tab2Screen } from './Tab2Screen/Tab2Screen'\n"
  },
  {
    "path": "src/store/Provider/Provider.tsx",
    "content": "// import { PureComponent } from 'react'\n// import PropTypes from 'prop-types'\n// import { Provider } from 'react-redux'\n// import { store } from '../store'\n\n\n// class AppStoreProvider extends PureComponent<{ children: any }> {\n//   getChildContext() {\n//     return {\n//       store,\n//     }\n//   }\n\n//   static childContextTypes = {\n//     store: PropTypes.shape({}),\n//   }\n\n//   render() {\n//     return (\n//       <Provider store={store}>\n//         {this.props.children}\n//       </Provider>\n//     )\n//   }\n// }\n\n// export default AppStoreProvider\n"
  },
  {
    "path": "src/store/Provider/ThemeProvider.tsx",
    "content": "import { memo, useEffect, useState } from 'react'\n\nimport themeState, { ThemeContext } from '../theme/state'\n\n\nexport default memo(({ children }: {\n  children: React.ReactNode\n}) => {\n  const [theme, setTheme] = useState(themeState.theme)\n\n  useEffect(() => {\n    const handleUpdateTheme = (theme: LX.ActiveTheme) => {\n      requestAnimationFrame(() => {\n        setTheme(theme)\n      })\n    }\n    global.state_event.on('themeUpdated', handleUpdateTheme)\n    return () => {\n      global.state_event.off('themeUpdated', handleUpdateTheme)\n    }\n  }, [])\n\n  return (\n    <ThemeContext.Provider value={theme}>\n      {children}\n    </ThemeContext.Provider>\n  )\n})\n"
  },
  {
    "path": "src/store/Provider/index.ts",
    "content": "export { default as Provider } from './ThemeProvider'\n"
  },
  {
    "path": "src/store/common/action.ts",
    "content": "import state, { type InitState } from './state'\nimport { type COMPONENT_IDS } from '@/config/constant'\n\n\nexport default {\n  setFontSize(size: number) {\n    state.fontSize = size\n    global.state_event.fontSizeUpdated(size)\n  },\n  setStatusbarHeight(size: number) {\n    if (state.statusbarHeight == size) return\n    state.statusbarHeight = size\n    global.state_event.statusbarHeightUpdated(size)\n  },\n  setComponentId(name: COMPONENT_IDS, id: string) {\n    state.componentIds[name] = id\n    global.state_event.componentIdsUpdated({ ...state.componentIds })\n  },\n  removeComponentId(id: string) {\n    const name = (Object.entries(state.componentIds) as Array<[COMPONENT_IDS, string]>).find(kv => kv[1] == id)?.[0]\n    if (!name) return\n    delete state.componentIds[name]\n    global.state_event.componentIdsUpdated({ ...state.componentIds })\n  },\n  setNavActiveId(id: InitState['navActiveId']) {\n    state.navActiveId = id\n    if (id != 'nav_setting') state.lastNavActiveId = id\n    global.state_event.navActiveIdUpdated(id)\n  },\n  setLastNavActiveId(id: InitState['navActiveId']) {\n    state.lastNavActiveId = id\n  },\n  setBgPic(pic: string | null) {\n    state.bgPic = pic\n    global.state_event.bgPicUpdated(pic)\n  },\n  setSourceNames(names: InitState['sourceNames']) {\n    state.sourceNames = names\n    global.state_event.sourceNamesUpdated(names)\n  },\n}\n\n"
  },
  {
    "path": "src/store/common/hook.ts",
    "content": "import { type COMPONENT_IDS } from '@/config/constant'\nimport { useEffect, useState } from 'react'\nimport state, { type InitState } from './state'\n\nexport const useFontSize = () => {\n  const [value, update] = useState(state.fontSize)\n\n  useEffect(() => {\n    global.state_event.on('fontSizeUpdated', update)\n    return () => {\n      global.state_event.off('fontSizeUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useStatusbarHeight = () => {\n  const [value, update] = useState(state.statusbarHeight)\n\n  useEffect(() => {\n    global.state_event.on('statusbarHeightUpdated', update)\n    return () => {\n      global.state_event.off('statusbarHeightUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useComponentIds = () => {\n  const [value, update] = useState(state.componentIds)\n\n  useEffect(() => {\n    global.state_event.on('componentIdsUpdated', update)\n    return () => {\n      global.state_event.off('componentIdsUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\nconst hasVisible = (visibleNames: COMPONENT_IDS[], ids: InitState['componentIds']) => {\n  const names = Object.keys(ids)\n  return names.length == visibleNames.length ? visibleNames.every(n => names.includes(n)) : false\n}\nexport const usePageVisible = (visibleNames: COMPONENT_IDS[], onChange: (visible: boolean) => void) => {\n  useEffect(() => {\n    let visible = hasVisible(visibleNames, state.componentIds)\n    const handlecheck = (ids: InitState['componentIds']) => {\n      const res = hasVisible(visibleNames, ids)\n      // console.log(visible, res, res == visible)\n      if (res == visible) return\n      visible = res\n      onChange(visible)\n    }\n    global.state_event.on('componentIdsUpdated', handlecheck)\n    return () => {\n      global.state_event.off('componentIdsUpdated', handlecheck)\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n}\n\n\nexport const useAssertApiSupport = (source: LX.Source) => {\n  const [value, update] = useState(global.lx.qualityList[source] != null || source == 'local')\n\n  useEffect(() => {\n    const handleUpdate = () => {\n      update(global.lx.qualityList[source] != null || source == 'local')\n    }\n\n    global.state_event.on('apiSourceUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('apiSourceUpdated', handleUpdate)\n    }\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return value\n}\n\n\nexport const useNavActiveId = () => {\n  const [value, update] = useState(state.navActiveId)\n\n  useEffect(() => {\n    global.state_event.on('navActiveIdUpdated', update)\n    return () => {\n      global.state_event.off('navActiveIdUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useBgPic = () => {\n  const [value, update] = useState(state.bgPic)\n\n  useEffect(() => {\n    global.state_event.on('bgPicUpdated', update)\n    return () => {\n      global.state_event.off('bgPicUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\n\nexport const useSourceNames = () => {\n  const [value, update] = useState(state.sourceNames)\n\n  useEffect(() => {\n    global.state_event.on('sourceNamesUpdated', update)\n    return () => {\n      global.state_event.off('sourceNamesUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n\n"
  },
  {
    "path": "src/store/common/state.ts",
    "content": "import { type NAV_ID_Type, type COMPONENT_IDS } from '@/config/constant'\n\n\nexport interface InitState {\n  fontSize: number\n  statusbarHeight: number\n  componentIds: Partial<Record<COMPONENT_IDS, string>>\n  navActiveId: NAV_ID_Type\n  lastNavActiveId: NAV_ID_Type\n  sourceNames: Record<LX.OnlineSource | 'all', string>\n  bgPic: string | null\n}\n\nconst initData = {}\n\nconst state: InitState = {\n  fontSize: global.lx.fontSize,\n  statusbarHeight: 0,\n  componentIds: {},\n  navActiveId: 'nav_search',\n  lastNavActiveId: 'nav_search',\n  sourceNames: initData as InitState['sourceNames'],\n  bgPic: null,\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/dislikeList/action.ts",
    "content": "import { state } from './state'\nimport { SPLIT_CHAR } from '@/config/constant'\nimport { event } from './event'\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 state.dislikeInfo.musicNames.has(name) || state.dislikeInfo.singerNames.has(singer) ||\n    state.dislikeInfo.names.has(`${name}${SPLIT_CHAR.DISLIKE_NAME}${singer}`)\n}\n\nexport const setDislikeInfo = (dislikeInfo: LX.Dislike.DislikeInfo) => {\n  state.dislikeInfo.rules = dislikeInfo.rules\n  state.dislikeInfo.names = dislikeInfo.names\n  state.dislikeInfo.musicNames = dislikeInfo.musicNames\n  state.dislikeInfo.singerNames = dislikeInfo.singerNames\n  event.dislike_changed()\n}\n"
  },
  {
    "path": "src/store/dislikeList/event.ts",
    "content": "import Event from '@/event/Event'\n\n\nclass DislikeEvent extends Event {\n  dislike_changed() {\n    this.emit('dislike_changed')\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends DislikeEvent {\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\ntype DislikeEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\n\n\nexport const event: DislikeEventTypes = new DislikeEvent()\n"
  },
  {
    "path": "src/store/dislikeList/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport { state } from './state'\nimport { event } from './event'\n\n\nexport const useRuleNum = () => {\n  const [num, setNum] = useState(state.dislikeInfo.musicNames.size + state.dislikeInfo.singerNames.size + state.dislikeInfo.names.size)\n\n  useEffect(() => {\n    const handleUpdate = () => {\n      setNum(state.dislikeInfo.musicNames.size + state.dislikeInfo.singerNames.size + state.dislikeInfo.names.size)\n    }\n    event.on('dislike_changed', handleUpdate)\n    return () => {\n      event.off('dislike_changed', handleUpdate)\n    }\n  }, [])\n\n  return num\n}\n\n"
  },
  {
    "path": "src/store/dislikeList/index.ts",
    "content": "\nexport * as action from './action'\nexport * from './state'\nexport * from './hook'\n"
  },
  {
    "path": "src/store/dislikeList/state.ts",
    "content": "\ninterface InitState {\n  dislikeInfo: LX.Dislike.DislikeInfo\n}\nconst state: InitState = {\n  dislikeInfo: {\n    names: new Set(),\n    musicNames: new Set(),\n    singerNames: new Set(),\n    rules: '',\n  },\n}\n\n\nexport {\n  state,\n}\n\n\n"
  },
  {
    "path": "src/store/hotSearch/action.ts",
    "content": "import state, { type Source } from './state'\n\nexport type Lists = Array<{ source: LX.OnlineSource, list: string[] }>\n\nconst setList = (source: LX.OnlineSource, list: string[]): string[] => {\n  const l = state.sourceList[source] = list.slice(0, 20)\n  return l\n}\n\nconst setLists = (lists: Lists): string[] => {\n  let wordsMap = new Map<string, number>()\n  for (const { source, list } of lists) {\n    if (!state.sourceList[source]?.length) state.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 state.sourceList.all = words.slice(0, state.sources.length * 10)\n}\n\n\nexport default {\n  setList(source: Source, list: string[] | Lists) {\n    if (source == 'all') {\n      return setLists(list as Lists)\n    }\n    return setList(source, list as string[])\n  },\n  clearList(source: Source) {\n    state.sourceList[source] = []\n  },\n}\n"
  },
  {
    "path": "src/store/hotSearch/state.ts",
    "content": "import musicSdk from '@/utils/musicSdk'\n\n// import { deduplicationList } from '@common/utils/renderer'\n\nexport declare type Source = LX.OnlineSource | 'all'\n\ntype SourceLists = Partial<Record<Source, string[]>>\n\n\nexport interface InitState {\n  sources: Source[]\n  sourceList: SourceLists\n}\n\nconst state: InitState = {\n  sources: [],\n  sourceList: {\n    all: [],\n  },\n}\n\nfor (const source of musicSdk.sources) {\n  if (!musicSdk[source.id as LX.OnlineSource]?.hotSearch) continue\n  state.sources.push(source.id as LX.OnlineSource)\n  state.sourceList[source.id as LX.OnlineSource] = []\n}\nstate.sources.push('all')\n\nexport default state\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "\n\nexport const useGetter = () => {\n\n}\n"
  },
  {
    "path": "src/store/leaderboard/action.ts",
    "content": "import state, { type Source, type Board, type ListDetailInfo } from './state'\n\nexport default {\n  setBoard(board: Board, source: LX.OnlineSource) {\n    state.boards[source] = board\n  },\n  setListDetailInfo(source: Source, id: string) {\n    state.listDetailInfo.source = source\n    state.listDetailInfo.id = id\n  },\n  setListDetail(result: ListDetailInfo, id: string, page: number) {\n    state.listDetailInfo.list = page == 1 ? [...result.list] : [...state.listDetailInfo.list, ...result.list]\n    state.listDetailInfo.id = id\n    state.listDetailInfo.source = result.source\n    if (page == 1 || (result.total && result.list.length)) state.listDetailInfo.total = result.total\n    else state.listDetailInfo.total = result.limit * page\n    state.listDetailInfo.limit = result.limit\n    state.listDetailInfo.page = page\n    state.listDetailInfo.maxPage = Math.ceil(state.listDetailInfo.total / result.limit)\n\n    return state.listDetailInfo\n  },\n  clearListDetail() {\n    state.listDetailInfo.list = []\n    state.listDetailInfo.id = ''\n    state.listDetailInfo.source = null\n    state.listDetailInfo.total = 0\n    state.listDetailInfo.limit = 30\n    state.listDetailInfo.page = 1\n    state.listDetailInfo.maxPage = 1\n    state.listDetailInfo.key = null\n  },\n}\n\n"
  },
  {
    "path": "src/store/leaderboard/state.ts",
    "content": "import music from '@/utils/musicSdk'\n\nexport declare type Source = LX.OnlineSource\n\nexport declare interface BoardItem {\n  id: string\n  name: string\n  bangid: string\n}\nexport declare interface Board {\n  list: BoardItem[]\n  source: LX.OnlineSource\n}\ntype Boards = Partial<Record<LX.OnlineSource, Board>>\n\nexport declare interface ListDetailInfo {\n  list: LX.Music.MusicInfoOnline[]\n  total: number\n  maxPage: number\n  page: number\n  source: LX.OnlineSource | null\n  limit: number\n  key: string | null\n  id: string\n}\n\nexport interface InitState {\n  sources: LX.OnlineSource[]\n  boards: Boards\n  listDetailInfo: ListDetailInfo\n}\n\nconst state: InitState = {\n  sources: [],\n  boards: {},\n  listDetailInfo: {\n    list: [],\n    total: 0,\n    page: 1,\n    maxPage: 1,\n    limit: 30,\n    key: null,\n    source: null,\n    id: '',\n  },\n}\n\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.leaderboard?.getBoards) continue\n  state.sources.push(source.id as LX.OnlineSource)\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/list/action.ts",
    "content": "import state, { type InitState } from './state'\n\n\nexport default {\n  setUserLists(userList: LX.List.UserListInfo[]) {\n    state.userList = userList\n    state.allList = [state.defaultList, state.loveList, ...state.userList]\n\n    global.state_event.mylistUpdated(state.allList)\n  },\n  setActiveList(activeListId: string) {\n    state.activeListId = activeListId\n\n    global.state_event.mylistToggled(activeListId)\n  },\n  setTempListMeta(meta: InitState['tempListMeta']) {\n    state.tempListMeta = meta\n  },\n  setFetchingListStatus(id: string, status: boolean) {\n    state.fetchingListStatus[id] = status\n\n    global.state_event.fetchingListStatusUpdated({ ...state.fetchingListStatus })\n  },\n}\n\n\n// Other code such as selectors can use the imported `RootState` type\n// export const defaultList = (state: LX.State) => state.userList.defaultList\n// export const loveList = (state: LX.State) => state.userList.loveList\n// export const userList = (state: LX.State) => state.userList.userList\n// export const selectAllList = createSelector(defaultList, loveList, userList, (defaultList, loveList, userList) => {\n//   return [defaultList, loveList, ...userList]\n// })\n\n// export const selectActiveListId = (state: LX.State) => state.userList.activeListId\n\n// export default slice.reducer\n"
  },
  {
    "path": "src/store/list/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport state, { type InitState } from './state'\nimport { getListMusics } from '@/core/list'\n\nexport const useMyList = () => {\n  const [lists, setList] = useState(state.allList)\n  lists[0].name = global.i18n.t('list_name_default')\n  lists[1].name = global.i18n.t('list_name_love')\n\n  useEffect(() => {\n    const handleConfigUpdate = (keys: Array<keyof LX.AppSetting>) => {\n      if (!keys.includes('common.langId')) return\n      setList((lists) => {\n        lists[0].name = global.i18n.t('list_name_default')\n        lists[1].name = global.i18n.t('list_name_love')\n        return [...lists]\n      })\n    }\n    global.state_event.on('mylistUpdated', setList)\n    global.state_event.on('configUpdated', handleConfigUpdate)\n    return () => {\n      global.state_event.off('mylistUpdated', setList)\n      global.state_event.off('configUpdated', handleConfigUpdate)\n    }\n  }, [])\n\n  return lists\n}\n\nexport const useActiveListId = () => {\n  const [id, setId] = useState(state.activeListId)\n\n  useEffect(() => {\n    global.state_event.on('mylistToggled', setId)\n    return () => {\n      global.state_event.off('mylistToggled', setId)\n    }\n  }, [])\n\n  return id\n}\n\n\nexport const useMusicList = () => {\n  const [list, setList] = useState<LX.List.ListMusics>([])\n\n  useEffect(() => {\n    const handleToggle = (activeListId: string) => {\n      void getListMusics(activeListId).then((list) => {\n        setList([...list])\n      })\n    }\n    const handleChange = (ids: string[]) => {\n      if (!ids.includes(state.activeListId)) return\n      void getListMusics(state.activeListId).then((list) => {\n        setList([...list])\n      })\n    }\n    global.state_event.on('mylistToggled', handleToggle)\n    global.app_event.on('myListMusicUpdate', handleChange)\n\n    handleToggle(state.activeListId)\n\n    return () => {\n      global.state_event.off('mylistToggled', handleToggle)\n      global.app_event.off('myListMusicUpdate', handleChange)\n    }\n  }, [])\n\n  return list\n}\n\nexport const useMusicExistsList = (list: LX.List.MyListInfo, musicInfo: LX.Music.MusicInfo) => {\n  const [isExists, setExists] = useState(false)\n\n  useEffect(() => {\n    void getListMusics(list.id).then((musics) => {\n      setExists(musics.some(s => s.id == musicInfo.id))\n    })\n  }, [list.id, musicInfo.id])\n\n  return isExists\n}\n\nexport const useListFetching = (listId: string) => {\n  const [fetching, setFetching] = useState(!!state.fetchingListStatus[listId])\n\n  useEffect(() => {\n    let prevStatus = state.fetchingListStatus[listId]\n    const handleUpdate = (status: InitState['fetchingListStatus']) => {\n      let currentStatus = status[listId]\n      if (currentStatus == null || prevStatus == status[listId]) return\n      setFetching(prevStatus = currentStatus)\n    }\n    global.state_event.on('fetchingListStatusUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('fetchingListStatusUpdated', handleUpdate)\n    }\n  }, [listId])\n\n  return fetching\n}\n\n"
  },
  {
    "path": "src/store/list/state.ts",
    "content": "import { LIST_IDS } from '@/config/constant'\n\n\nexport interface InitState {\n  allMusicList: Map<string, LX.Music.MusicInfo[]>\n  defaultList: LX.List.MyDefaultListInfo\n  loveList: LX.List.MyLoveListInfo\n  tempList: LX.List.MyTempListInfo\n  userList: LX.List.UserListInfo[]\n  activeListId: string\n\n  allList: Array<LX.List.MyDefaultListInfo | LX.List.MyLoveListInfo | LX.List.UserListInfo>\n\n  tempListMeta: {\n    id: string\n  }\n\n  fetchingListStatus: Record<string, boolean>\n}\n\nconst state: InitState = {\n  allMusicList: new Map(),\n  defaultList: {\n    id: LIST_IDS.DEFAULT,\n    name: '试听列表',\n  },\n  loveList: {\n    id: LIST_IDS.LOVE,\n    name: '我的收藏',\n  },\n  tempList: {\n    id: LIST_IDS.TEMP,\n    name: '临时列表',\n    meta: {},\n  },\n  userList: [],\n  activeListId: '',\n  allList: [],\n  tempListMeta: {\n    id: '',\n  },\n  fetchingListStatus: {},\n}\n\nstate.allList = [state.defaultList, state.loveList]\n\n\nexport default state\n"
  },
  {
    "path": "src/store/player/action.ts",
    "content": "import { arrPush, arrUnshift, formatPlayTime2 } from '@/utils'\nimport state from './state'\n\ntype PlayerMusicInfoKeys = keyof LX.Player.MusicInfo\nconst musicInfoKeys: PlayerMusicInfoKeys[] = Object.keys(state.musicInfo) as PlayerMusicInfoKeys[]\n\nexport default {\n  updatePlayIndex(playIndex: number, playerPlayIndex: number) {\n    state.playInfo.playIndex = playIndex\n    state.playInfo.playerPlayIndex = playerPlayIndex\n\n    global.state_event.playInfoChanged({ ...state.playInfo })\n  },\n  setPlayListId(playerListId: string | null) {\n    state.playInfo.playerListId = playerListId\n\n    global.state_event.playInfoChanged({ ...state.playInfo })\n  },\n  setPlayMusicInfo(listId: string | null, musicInfo: LX.Download.ListItem | LX.Music.MusicInfo | null, isTempPlay: boolean = false) {\n    state.playMusicInfo = { listId, musicInfo, isTempPlay }\n\n    global.state_event.playMusicInfoChanged(state.playMusicInfo)\n  },\n  setMusicInfo(_musicInfo: Partial<LX.Player.MusicInfo>) {\n    for (const key of musicInfoKeys) {\n      const val = _musicInfo[key]\n      if (val !== undefined) {\n        // @ts-expect-error\n        state.musicInfo[key] = val\n      }\n    }\n\n    global.state_event.playerMusicInfoChanged({ ...state.musicInfo })\n  },\n  setIsPlay(isPlay: boolean) {\n    state.isPlay = isPlay\n\n    global.state_event.playStateChanged(isPlay)\n  },\n  setStatusText(statusText: string) {\n    state.statusText = statusText\n    global.state_event.playStateTextChanged(statusText)\n  },\n  setNowPlayTime(time: number) {\n    state.progress.nowPlayTime = time\n    state.progress.nowPlayTimeStr = formatPlayTime2(time)\n    state.progress.progress = state.progress.maxPlayTime ? time / state.progress.maxPlayTime : 0\n\n    global.state_event.playProgressChanged({ ...state.progress })\n  },\n  setMaxplayTime(time: number) {\n    state.progress.maxPlayTime = time\n    state.progress.maxPlayTimeStr = formatPlayTime2(time)\n    state.progress.progress = time ? state.progress.nowPlayTime / time : 0\n\n    global.state_event.playProgressChanged({ ...state.progress })\n  },\n  setProgress(currentTime: number, totalTime: number) {\n    state.progress.nowPlayTime = currentTime\n    state.progress.nowPlayTimeStr = formatPlayTime2(currentTime)\n    state.progress.maxPlayTime = totalTime\n    state.progress.maxPlayTimeStr = formatPlayTime2(totalTime)\n    state.progress.progress = totalTime ? state.progress.nowPlayTime / currentTime : 0\n\n    global.state_event.playProgressChanged({ ...state.progress })\n  },\n  addPlayedList(info: LX.Player.PlayMusicInfo) {\n    if (state.playedList.some(m => m.musicInfo.id == info.musicInfo.id)) return\n    state.playedList.push(info)\n\n    global.state_event.playPlayedListChanged({ ...state.playedList })\n  },\n  removePlayedList(index: number) {\n    state.playedList.splice(index, 1)\n\n    global.state_event.playPlayedListChanged({ ...state.playedList })\n  },\n  clearPlayedList() {\n    state.playedList = []\n\n    global.state_event.playPlayedListChanged({ ...state.playedList })\n  },\n  addTempPlayList(list: LX.Player.TempPlayListItem[]) {\n    const topList: Array<{ listId: string | null, musicInfo: LX.Music.MusicInfo | LX.Download.ListItem }> = []\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(state.tempPlayList, topList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))\n    if (bottomList.length) arrPush(state.tempPlayList, bottomList.map(({ musicInfo, listId }) => ({ musicInfo, listId, isTempPlay: true })))\n\n    global.state_event.playTempPlayListChanged({ ...state.tempPlayList })\n  },\n  removeTempPlayList(index: number) {\n    state.tempPlayList.splice(index, 1)\n\n    global.state_event.playTempPlayListChanged({ ...state.tempPlayList })\n  },\n  clearTempPlayeList() {\n    state.tempPlayList = []\n\n    global.state_event.playTempPlayListChanged({ ...state.tempPlayList })\n  },\n  setLoadErrorPicUrl(url: string) {\n    state.loadErrorPicUrl = url\n  },\n  setLastLyric(lrc?: string) {\n    state.lastLyric = lrc\n  },\n}\n"
  },
  {
    "path": "src/store/player/hook.ts",
    "content": "import { isActive } from '@/utils/tools'\nimport { useEffect, useState } from 'react'\nimport state, { type InitState } from './state'\n\nexport const usePlayerMusicInfo = () => {\n  const [value, update] = useState(state.musicInfo)\n\n  useEffect(() => {\n    global.state_event.on('playerMusicInfoChanged', update)\n    return () => {\n      global.state_event.off('playerMusicInfoChanged', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const usePlayMusicInfo = () => {\n  const [value, update] = useState(state.playMusicInfo)\n\n  useEffect(() => {\n    global.state_event.on('playMusicInfoChanged', update)\n    return () => {\n      global.state_event.off('playMusicInfoChanged', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const usePlayInfo = () => {\n  const [value, update] = useState(state.playInfo)\n\n  useEffect(() => {\n    global.state_event.on('playInfoChanged', update)\n    return () => {\n      global.state_event.off('playInfoChanged', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useStatusText = () => {\n  const [value, update] = useState(state.statusText)\n\n  useEffect(() => {\n    global.state_event.on('playStateTextChanged', update)\n    return () => {\n      global.state_event.off('playStateTextChanged', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useIsPlay = () => {\n  const [value, update] = useState(state.isPlay)\n\n  useEffect(() => {\n    global.state_event.on('playStateChanged', update)\n    return () => {\n      global.state_event.off('playStateChanged', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useProgress = (autoUpdate = true) => {\n  const [value, update] = useState(state.progress)\n\n  useEffect(() => {\n    if (!autoUpdate) return\n    const handleUpdate = (progress: InitState['progress']) => {\n      if (isActive()) update(progress)\n    }\n    update(state.progress)\n    global.state_event.on('playProgressChanged', handleUpdate)\n    return () => {\n      global.state_event.off('playProgressChanged', handleUpdate)\n    }\n  }, [autoUpdate])\n\n  return value\n}\n"
  },
  {
    "path": "src/store/player/state.ts",
    "content": "export interface InitState {\n  playMusicInfo: {\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  playInfo: LX.Player.PlayInfo\n  musicInfo: LX.Player.MusicInfo\n\n  isPlay: boolean\n  volume: number\n  playRate: number\n  statusText: string\n\n  playedList: LX.Player.PlayMusicInfo[]\n  tempPlayList: LX.Player.PlayMusicInfo[]\n\n  loadErrorPicUrl: string\n\n\n  progress: {\n    nowPlayTime: number\n    maxPlayTime: number\n    progress: number\n    nowPlayTimeStr: string\n    maxPlayTimeStr: string\n  }\n\n  lastLyric: string | undefined\n}\n\nconst state: InitState = {\n  playInfo: {\n    playIndex: -1,\n    playerListId: null,\n    playerPlayIndex: -1,\n  },\n  playMusicInfo: {\n    listId: null,\n    musicInfo: null,\n    isTempPlay: false,\n  },\n  musicInfo: {\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\n  isPlay: false,\n  volume: 1,\n  playRate: 1,\n  statusText: '',\n  loadErrorPicUrl: '',\n\n  playedList: [],\n  tempPlayList: [],\n\n  progress: {\n    nowPlayTime: 0,\n    maxPlayTime: 0,\n    progress: 0,\n    nowPlayTimeStr: '00:00',\n    maxPlayTimeStr: '00:00',\n  },\n\n  lastLyric: undefined,\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/search/action.ts",
    "content": "import state, { type InitState } from './state'\n// let isInitedSearchHistory = false\n// const saveSearchHistoryListThrottle = throttle((list: LX.List.SearchHistoryList) => {\n//   saveSearchHistoryList(list)\n// }, 500)\n\n\n// export const getHistoryList = async() => {\n//   if (isInitedSearchHistory) return\n//   historyList.push(...(await getSearchHistoryList() ?? []))\n//   isInitedSearchHistory = true\n// }\n// export 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 > -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// }\n// export const removeHistoryWord = (index: number) => {\n//   historyList.splice(index, 1)\n//   saveSearchHistoryListThrottle(toRaw(historyList))\n// }\n// export const clearHistoryList = (id: string) => {\n//   historyList.splice(0, historyList.length)\n//   saveSearchHistoryList([])\n// }\n\n\nexport default {\n  setSearchType(type: InitState['searchType']) {\n    state.searchType = type\n  },\n  setSearchText(text: string) {\n    state.searchText = text\n  },\n  setTipListInfo(keyword: InitState['tipListInfo']['text'], source: InitState['tipListInfo']['source']) {\n    state.tipListInfo.text = keyword\n    state.tipListInfo.source = source\n  },\n  setTipList(list: InitState['tipListInfo']['list']) {\n    state.tipListInfo.list = list\n  },\n  setHistoryWord(list: string[]) {\n    state.historyList = list\n  },\n  addHistoryWord(word: string) {\n    let index = state.historyList.indexOf(word)\n    if (index == 0) return\n    if (index > -1) state.historyList.splice(index, 1)\n    if (state.historyList.length >= 15) state.historyList.splice(14, state.historyList.length - 14)\n    state.historyList.unshift(word)\n    return [...state.historyList]\n  },\n  removeHistoryWord(index: number) {\n    state.historyList.splice(index, 1)\n    return [...state.historyList]\n  },\n  clearHistoryList() {\n    state.historyList = []\n    return state.historyList\n  },\n}\n"
  },
  {
    "path": "src/store/search/music/action.ts",
    "content": "import state, { type InitState, type Source } from './state'\nimport { sortInsert, similar, arrPush } from '@/utils/common'\nimport { deduplicationList, toNewMusicInfo } from '@/utils'\n\n\nexport interface SearchResult {\n  list: LX.Music.MusicInfoOnline[]\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.MusicInfoOnline[], keyword: string) => {\n  let arr: any[] = []\n  for (const item of list) {\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\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.MusicInfoOnline[] => {\n  let pages = []\n  let totals = []\n  let limit = 0\n  let list = [] as LX.Music.MusicInfoOnline[]\n  for (const source of results) {\n    state.maxPages[source.source] = source.allPage\n    limit = Math.max(source.limit, limit)\n    if (source.allPage < page) continue\n    arrPush(list, source.list)\n    pages.push(source.allPage)\n    totals.push(source.total)\n  }\n  list = handleSortList(list.map(s => toNewMusicInfo(s) as LX.Music.MusicInfoOnline), text)\n  let listInfo = state.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 = deduplicationList(page > 1 ? [...listInfo.list, ...list] : list)\n  state.source = 'all'\n\n  return listInfo.list\n}\n\nconst setList = (datas: SearchResult, page: number, text: string): LX.Music.MusicInfoOnline[] => {\n  // console.log(datas.source, datas.list)\n  let listInfo = state.listInfos[datas.source]!\n  const list = datas.list.map(s => toNewMusicInfo(s) as LX.Music.MusicInfoOnline)\n  listInfo.list = deduplicationList(page == 1 ? list : [...listInfo.list, ...list])\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  state.source = datas.source\n\n  return listInfo.list\n}\n\nexport default {\n  setSource(source: InitState['source']) {\n    state.source = source\n  },\n  setSearchText(searchText: InitState['searchText']) {\n    state.searchText = searchText\n  },\n  setListInfo(result: SearchResult | SearchResult[], page: number, text: string) {\n    if (Array.isArray(result)) {\n      return setLists(result, page, text)\n    } else {\n      return setList(result, page, text)\n    }\n  },\n  clearListInfo(sourceId: Source) {\n    let listInfo = state.listInfos[sourceId]!\n    listInfo.list = []\n    listInfo.page = 0\n    listInfo.maxPage = 0\n    listInfo.total = 0\n  },\n}\n"
  },
  {
    "path": "src/store/search/music/state.ts",
    "content": "import music from '@/utils/musicSdk'\n\nexport declare interface ListInfo {\n  list: LX.Music.MusicInfoOnline[]\n  total: number\n  page: number\n  maxPage: number\n  limit: number\n  key: string | null\n}\n\ninterface ListInfos extends Partial<Record<LX.OnlineSource, ListInfo>> {\n  'all': ListInfo\n}\n\nexport type Source = LX.OnlineSource | 'all'\n\nexport interface InitState {\n  searchText: string\n  source: Source\n  sources: Source[]\n  listInfos: ListInfos\n  maxPages: Partial<Record<LX.OnlineSource, number>>\n}\n\nconst state: InitState = {\n  searchText: '',\n  source: 'kw',\n  sources: [],\n  listInfos: {\n    all: {\n      page: 1,\n      maxPage: 0,\n      limit: 30,\n      total: 0,\n      list: [],\n      key: null,\n    },\n  },\n  maxPages: {},\n}\n\nfor (const source of music.sources) {\n  if (!music[source.id as LX.OnlineSource]?.musicSearch) continue\n  state.sources.push(source.id as LX.OnlineSource)\n  state.listInfos[source.id as LX.OnlineSource] = {\n    page: 1,\n    maxPage: 0,\n    limit: 30,\n    total: 0,\n    list: [],\n    key: '',\n  }\n  state.maxPages[source.id as LX.OnlineSource] = 0\n}\nstate.sources.push('all')\n\nexport default state\n"
  },
  {
    "path": "src/store/search/songlist/action.ts",
    "content": "import { sortInsert, similar } from '@/utils/common'\n\nimport type { InitState, ListInfoItem, Source } from './state'\nimport state from './state'\n\nexport interface 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    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\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    state.maxPages[source.source] = Math.ceil(source.total / source.limit)\n    limit = Math.max(source.limit, limit)\n  }\n\n  let listInfo = state.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  list = handleSortList(list, text)\n  listInfo.list = page > 1 ? [...listInfo.list, ...list] : list\n  state.source = 'all'\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 = state.listInfos[datas.source]!\n  listInfo.list = page == 1 ? datas.list : [...listInfo.list, ...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  state.source = datas.source\n  return listInfo.list\n}\n\n\nexport default {\n  setSource(source: InitState['source']) {\n    state.source = source\n  },\n  setSearchText(searchText: InitState['searchText']) {\n    state.searchText = searchText\n  },\n  setListInfo(result: SearchResult | SearchResult[], page: number, text: string) {\n    if (Array.isArray(result)) {\n      return setLists(result, page, text)\n    } else {\n      return setList(result, page, text)\n    }\n  },\n  clearListInfo(sourceId: Source) {\n    let listInfo = state.listInfos[sourceId]!\n    listInfo.page = 1\n    listInfo.limit = 20\n    listInfo.total = 0\n    listInfo.list = []\n    listInfo.key = null\n    listInfo.tagId = ''\n    listInfo.sortId = ''\n  },\n}\n"
  },
  {
    "path": "src/store/search/songlist/state.ts",
    "content": "import music from '@/utils/musicSdk'\n\n\n// import { deduplicationList } from '@common/utils/renderer'\n\nimport { type ListInfo } from '@/store/songlist/state'\nexport type { ListInfoItem } from '@/store/songlist/state'\n\nexport type SearchListInfo = Omit<ListInfo, 'source' | 'maxPage'>\n\n\ninterface ListInfos extends Partial<Record<LX.OnlineSource, SearchListInfo>> {\n  'all': SearchListInfo\n}\n\nexport type Source = LX.OnlineSource | 'all'\n\nexport interface InitState {\n  searchText: string\n  source: Source\n  sources: Source[]\n  listInfos: ListInfos\n  maxPages: Partial<Record<Source, number>>\n}\n\nconst state: InitState = {\n  searchText: '',\n  source: 'kw',\n  sources: [],\n  listInfos: {\n    all: {\n      page: 1,\n      limit: 15,\n      total: 0,\n      list: [],\n      key: null,\n      tagId: '',\n      sortId: '',\n    },\n  },\n  maxPages: {},\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  state.sources.push(source.id as LX.OnlineSource)\n  state.listInfos[source.id as LX.OnlineSource] = {\n    page: 1,\n    limit: 18,\n    total: 0,\n    list: [],\n    key: null,\n    tagId: '',\n    sortId: '',\n  }\n  maxPages[source.id as LX.OnlineSource] = 0\n}\nstate.sources.push('all')\n\nexport default state\n"
  },
  {
    "path": "src/store/search/state.ts",
    "content": "export type SearchType = 'music' | 'songlist'\n\nexport interface InitState {\n  temp_source: 'kw'\n  // temp_source: LX.OnlineSource\n  searchType: SearchType\n  searchText: string\n  tipListInfo: {\n    text: string\n    source: 'kw'\n    list: string[]\n  }\n  historyList: string[]\n}\n\nconst state: InitState = {\n  temp_source: 'kw',\n  searchType: 'music',\n  searchText: '',\n  tipListInfo: {\n    text: '',\n    source: 'kw',\n    list: [],\n  },\n  historyList: [],\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/setting/action.ts",
    "content": "import { updateSetting as mergeSetting } from '@/config/setting'\nimport state from './state'\n\n\nexport default {\n  // mergeSetting(newSetting: Partial<LX.AppSetting>) {\n  //   for (const [key, value] of Object.entries(newSetting)) {\n  //     // @ts-expect-error\n  //     state[key] = value\n  //   }\n  // },\n  initSetting(newSetting: LX.AppSetting) {\n    state.setting = newSetting\n  },\n  updateSetting(newSetting: Partial<LX.AppSetting>) {\n    const result = mergeSetting(newSetting)\n    state.setting = result.setting\n    global.state_event.configUpdated(result.updatedSettingKeys, result.updatedSetting)\n  },\n}\n\n"
  },
  {
    "path": "src/store/setting/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport state from './state'\n\nexport const useSetting = () => {\n  const [setting, updateSetting] = useState(state.setting)\n\n  useEffect(() => {\n    const handleUpdate = () => {\n      updateSetting(state.setting)\n    }\n    global.state_event.on('configUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('configUpdated', handleUpdate)\n    }\n  }, [])\n\n  return setting\n}\n\nexport const useSettingValue = <T extends keyof LX.AppSetting>(key: T): LX.AppSetting[T] => {\n  const [value, update] = useState(state.setting[key])\n\n  useEffect(() => {\n    const handleUpdate = (keys: Array<keyof LX.AppSetting>) => {\n      if (!keys.includes(key)) return\n      update(state.setting[key])\n    }\n    global.state_event.on('configUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('configUpdated', handleUpdate)\n    }\n  }, [key])\n\n  return value\n}\n"
  },
  {
    "path": "src/store/setting/state.ts",
    "content": "import defaultSetting from '@/config/defaultSetting'\n\n\ninterface InitState {\n  setting: LX.AppSetting\n}\n\nconst state: InitState = {\n  setting: { ...defaultSetting },\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/songlist/action.ts",
    "content": "import type { TagInfo, ListDetailInfo, ListInfo, Source } from './state'\nimport state from './state'\n\nexport default {\n  setTags(tagInfo: TagInfo, source: LX.OnlineSource) {\n    state.tags[source] = tagInfo\n  },\n  setListInfo(source: Source, tagId: string, sortId: string) {\n    state.listInfo.source = source\n    state.listInfo.tagId = tagId\n    state.listInfo.sortId = sortId\n  },\n  setList(result: ListInfo, tagId: string, sortId: string, page: number) {\n    state.listInfo.list = page == 1 ? [...result.list] : [...state.listInfo.list, ...result.list]\n    if (page == 1 || (result.total && result.list.length)) state.listInfo.total = result.total\n    else state.listInfo.total = result.limit * page\n    state.listInfo.limit = result.limit\n    state.listInfo.page = page\n    state.listInfo.source = result.source\n    state.listInfo.tagId = tagId\n    state.listInfo.sortId = sortId\n    state.listInfo.maxPage = Math.ceil(state.listInfo.total / result.limit)\n\n    return state.listInfo\n  },\n  clearList() {\n    state.listInfo.list = []\n    state.listInfo.total = 0\n    state.listInfo.page = 1\n    state.listInfo.key = ''\n    state.listInfo.maxPage = 1\n  },\n  setListDetailInfo(source: Source, id: string) {\n    state.listDetailInfo.source = source\n    state.listDetailInfo.id = id\n  },\n  setListDetail(result: ListDetailInfo, id: string, page: number) {\n    state.listDetailInfo.list = page == 1 ? [...result.list] : [...state.listDetailInfo.list, ...result.list]\n    state.listDetailInfo.id = id\n    state.listDetailInfo.source = result.source\n    if (page == 1 || (result.total && result.list.length)) state.listDetailInfo.total = result.total\n    else state.listDetailInfo.total = result.limit * page\n    state.listDetailInfo.limit = result.limit\n    state.listDetailInfo.page = page\n    state.listDetailInfo.info = { ...result.info }\n    state.listDetailInfo.maxPage = Math.ceil(state.listDetailInfo.total / result.limit)\n\n    return state.listDetailInfo\n  },\n  clearListDetail() {\n    state.listDetailInfo.list = []\n    state.listDetailInfo.id = ''\n    state.listDetailInfo.source = 'kw'\n    state.listDetailInfo.total = 0\n    state.listDetailInfo.limit = 30\n    state.listDetailInfo.page = 1\n    state.listDetailInfo.key = null\n    state.listDetailInfo.info = {}\n    state.listDetailInfo.maxPage = 1\n  },\n}\n"
  },
  {
    "path": "src/store/songlist/state.ts",
    "content": "import music from '@/utils/musicSdk'\n\nexport declare interface SortInfo {\n  name: string\n  tid: 'recommend' | 'hot' | 'new' | 'hot_collect' | 'rise'\n  id: string\n}\n\nexport declare 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 declare interface TagInfoTypeItem<T extends LX.OnlineSource = LX.OnlineSource> {\n  name: string\n  list: Array<TagInfoItem<T>>\n}\nexport declare 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 declare 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\n  source: LX.OnlineSource\n  total?: string\n}\nexport declare interface ListInfo {\n  list: ListInfoItem[]\n  total: number\n  page: number\n  limit: number\n  maxPage: number\n  key: string | null\n  source: LX.OnlineSource\n  tagId: string\n  sortId: string\n}\n\nexport declare 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  maxPage: 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}\n\n// export const openSongListInputInfo = markRaw({\n//   text: '',\n//   source: '',\n// })\n\nexport type Source = LX.OnlineSource\nexport interface InitState {\n  sources: Source[]\n  sortList: Partial<Record<Source, SortInfo[]>>\n  tags: Tags\n  listInfo: ListInfo\n  listDetailInfo: ListDetailInfo\n}\n\n\nconst state: InitState = {\n  sources: [],\n  sortList: {},\n  tags: {},\n  listInfo: {\n    list: [],\n    total: 0,\n    page: 1,\n    limit: 30,\n    maxPage: 1,\n    key: null,\n    source: 'kw',\n    tagId: '',\n    sortId: '',\n  },\n  listDetailInfo: {\n    list: [],\n    id: '',\n    // desc: null,\n    total: 0,\n    page: 1,\n    limit: 30,\n    maxPage: 1,\n    key: null,\n    source: 'kw',\n    info: {},\n  },\n}\n\n\nfor (const source of music.sources) {\n  const songList = music[source.id as Source]?.songList\n  if (!songList) continue\n  state.sources.push(source.id as Source)\n  state.sortList[source.id as Source] = songList.sortList as SortInfo[]\n}\n\n\nexport default state\n\n"
  },
  {
    "path": "src/store/sync/action.ts",
    "content": "import state from './state'\n\n\nexport default {\n  setStatus(info: LX.Sync.Status) {\n    state.status.status = info.status\n    state.status.message = info.message\n\n    global.state_event.syncStatusUpdated({ ...state.status })\n  },\n  setMessage(message: LX.Sync.Status['message']) {\n    state.status.message = message\n\n    global.state_event.syncStatusUpdated({ ...state.status })\n  },\n  setServerInfo(name: string, type: keyof LX.Sync.ModeTypes) {\n    state.serverName = name\n    state.type = type\n  },\n  setSyncModeComponentId(id: string) {\n    state.syncModeComponentId = id\n  },\n}\n\n"
  },
  {
    "path": "src/store/sync/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport state from './state'\n\nexport const useStatus = () => {\n  const [value, update] = useState(state.status)\n\n  useEffect(() => {\n    global.state_event.on('syncStatusUpdated', update)\n    return () => {\n      global.state_event.off('syncStatusUpdated', update)\n    }\n  }, [])\n\n  return value\n}\n"
  },
  {
    "path": "src/store/sync/state.ts",
    "content": "\ninterface InitState {\n  status: LX.Sync.Status\n  serverName: string\n  type: keyof LX.Sync.ModeTypes\n  syncModeComponentId: string\n}\nconst state: InitState = {\n  status: {\n    status: false,\n    message: '',\n  },\n  serverName: '',\n  type: 'list',\n  syncModeComponentId: '',\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/store/theme/action.ts",
    "content": "import { buildActiveThemeColors } from '@/theme/themes'\nimport state from './state'\n\n\nexport default {\n  setTheme(theme: LX.Theme) {\n    state.theme = buildActiveThemeColors(theme)\n    // ThemeContext.displayName\n    global.state_event.themeUpdated(state.theme)\n  },\n  setShouldUseDarkColors(shouldUseDarkColors: boolean) {\n    if (state.shouldUseDarkColors == shouldUseDarkColors) return\n    state.shouldUseDarkColors = shouldUseDarkColors\n  },\n}\n\n"
  },
  {
    "path": "src/store/theme/hook.ts",
    "content": "import { useContext, useEffect, useState } from 'react'\nimport { ThemeContext } from './state'\nimport settingState from '@/store/setting/state'\n\n// export const useSetting = () => {\n//   const [setting, updateSetting] = useState(state.setting)\n\n//   useEffect(() => {\n//     const handleUpdate = () => {\n//       updateSetting(state.setting)\n//     }\n//     global.state_event.on('configUpdated', handleUpdate)\n//     return () => {\n//       global.state_event.off('configUpdated', handleUpdate)\n//     }\n//   }, [])\n\n//   return setting\n// }\n\n// export const useSettingValue = <T extends keyof LX.AppSetting>(key: T): LX.AppSetting[T] => {\n//   const [value, update] = useState(state.setting[key])\n\n//   useEffect(() => {\n//     const handleUpdate = (keys: Array<keyof LX.AppSetting>) => {\n//       if (!keys.includes(key)) return\n//       update(state.setting[key])\n//     }\n//     global.state_event.on('configUpdated', handleUpdate)\n//     return () => {\n//       global.state_event.off('configUpdated', handleUpdate)\n//     }\n//   }, [key])\n\n//   return value\n// }\n\nexport const useTheme = () => useContext(ThemeContext)\n\nexport const useTextShadow = () => {\n  const [value, update] = useState(settingState.setting['theme.fontShadow'])\n\n  useEffect(() => {\n    const handleUpdate = (keys: Array<keyof LX.AppSetting>, setting: Partial<LX.AppSetting>) => {\n      if (!keys.includes('theme.fontShadow')) return\n      requestAnimationFrame(() => {\n        update(setting['theme.fontShadow']!)\n      })\n    }\n    global.state_event.on('configUpdated', handleUpdate)\n    return () => {\n      global.state_event.off('configUpdated', handleUpdate)\n    }\n  }, [])\n\n  return value\n}\n"
  },
  {
    "path": "src/store/theme/state.ts",
    "content": "import { createContext } from 'react'\n// import type { RootState } from '@/store'\n\n\ninterface InitState {\n  shouldUseDarkColors: boolean\n  theme: LX.ActiveTheme\n}\n\nconst theme = {\n  id: '',\n  name: '',\n  isDark: false,\n  'c-primary': 'rgb(77, 175, 124)',\n  'c-primary-dark-100': 'rgb(69,158,112)',\n  'c-primary-dark-100-alpha-100': 'rgba(69, 158, 112, 0.90)',\n  'c-primary-alpha-100': 'rgba(77, 175, 124, 0.90)',\n  'c-primary-dark-100-alpha-200': 'rgba(69, 158, 112, 0.80)',\n  'c-primary-alpha-200': 'rgba(77, 175, 124, 0.80)',\n  'c-primary-dark-100-alpha-300': 'rgba(69, 158, 112, 0.70)',\n  'c-primary-alpha-300': 'rgba(77, 175, 124, 0.70)',\n  'c-primary-dark-100-alpha-400': 'rgba(69, 158, 112, 0.60)',\n  'c-primary-alpha-400': 'rgba(77, 175, 124, 0.60)',\n  'c-primary-dark-100-alpha-500': 'rgba(69, 158, 112, 0.50)',\n  'c-primary-alpha-500': 'rgba(77, 175, 124, 0.50)',\n  'c-primary-dark-100-alpha-600': 'rgba(69, 158, 112, 0.40)',\n  'c-primary-alpha-600': 'rgba(77, 175, 124, 0.40)',\n  'c-primary-dark-100-alpha-700': 'rgba(69, 158, 112, 0.30)',\n  'c-primary-alpha-700': 'rgba(77, 175, 124, 0.30)',\n  'c-primary-dark-100-alpha-800': 'rgba(69, 158, 112, 0.20)',\n  'c-primary-alpha-800': 'rgba(77, 175, 124, 0.20)',\n  'c-primary-dark-100-alpha-900': 'rgba(69, 158, 112, 0.10)',\n  'c-primary-alpha-900': 'rgba(77, 175, 124, 0.10)',\n  'c-primary-dark-200': 'rgb(62,142,101)',\n  'c-primary-dark-200-alpha-100': 'rgba(62, 142, 101, 0.90)',\n  'c-primary-dark-200-alpha-200': 'rgba(62, 142, 101, 0.80)',\n  'c-primary-dark-200-alpha-300': 'rgba(62, 142, 101, 0.70)',\n  'c-primary-dark-200-alpha-400': 'rgba(62, 142, 101, 0.60)',\n  'c-primary-dark-200-alpha-500': 'rgba(62, 142, 101, 0.50)',\n  'c-primary-dark-200-alpha-600': 'rgba(62, 142, 101, 0.40)',\n  'c-primary-dark-200-alpha-700': 'rgba(62, 142, 101, 0.30)',\n  'c-primary-dark-200-alpha-800': 'rgba(62, 142, 101, 0.20)',\n  'c-primary-dark-200-alpha-900': 'rgba(62, 142, 101, 0.10)',\n  'c-primary-dark-300': 'rgb(56,128,91)',\n  'c-primary-dark-300-alpha-100': 'rgba(56, 128, 91, 0.90)',\n  'c-primary-dark-300-alpha-200': 'rgba(56, 128, 91, 0.80)',\n  'c-primary-dark-300-alpha-300': 'rgba(56, 128, 91, 0.70)',\n  'c-primary-dark-300-alpha-400': 'rgba(56, 128, 91, 0.60)',\n  'c-primary-dark-300-alpha-500': 'rgba(56, 128, 91, 0.50)',\n  'c-primary-dark-300-alpha-600': 'rgba(56, 128, 91, 0.40)',\n  'c-primary-dark-300-alpha-700': 'rgba(56, 128, 91, 0.30)',\n  'c-primary-dark-300-alpha-800': 'rgba(56, 128, 91, 0.20)',\n  'c-primary-dark-300-alpha-900': 'rgba(56, 128, 91, 0.10)',\n  'c-primary-dark-400': 'rgb(50,115,82)',\n  'c-primary-dark-400-alpha-100': 'rgba(50, 115, 82, 0.90)',\n  'c-primary-dark-400-alpha-200': 'rgba(50, 115, 82, 0.80)',\n  'c-primary-dark-400-alpha-300': 'rgba(50, 115, 82, 0.70)',\n  'c-primary-dark-400-alpha-400': 'rgba(50, 115, 82, 0.60)',\n  'c-primary-dark-400-alpha-500': 'rgba(50, 115, 82, 0.50)',\n  'c-primary-dark-400-alpha-600': 'rgba(50, 115, 82, 0.40)',\n  'c-primary-dark-400-alpha-700': 'rgba(50, 115, 82, 0.30)',\n  'c-primary-dark-400-alpha-800': 'rgba(50, 115, 82, 0.20)',\n  'c-primary-dark-400-alpha-900': 'rgba(50, 115, 82, 0.10)',\n  'c-primary-dark-500': 'rgb(45,104,74)',\n  'c-primary-dark-500-alpha-100': 'rgba(45, 104, 74, 0.90)',\n  'c-primary-dark-500-alpha-200': 'rgba(45, 104, 74, 0.80)',\n  'c-primary-dark-500-alpha-300': 'rgba(45, 104, 74, 0.70)',\n  'c-primary-dark-500-alpha-400': 'rgba(45, 104, 74, 0.60)',\n  'c-primary-dark-500-alpha-500': 'rgba(45, 104, 74, 0.50)',\n  'c-primary-dark-500-alpha-600': 'rgba(45, 104, 74, 0.40)',\n  'c-primary-dark-500-alpha-700': 'rgba(45, 104, 74, 0.30)',\n  'c-primary-dark-500-alpha-800': 'rgba(45, 104, 74, 0.20)',\n  'c-primary-dark-500-alpha-900': 'rgba(45, 104, 74, 0.10)',\n  'c-primary-dark-600': 'rgb(41,94,67)',\n  'c-primary-dark-600-alpha-100': 'rgba(41, 94, 67, 0.90)',\n  'c-primary-dark-600-alpha-200': 'rgba(41, 94, 67, 0.80)',\n  'c-primary-dark-600-alpha-300': 'rgba(41, 94, 67, 0.70)',\n  'c-primary-dark-600-alpha-400': 'rgba(41, 94, 67, 0.60)',\n  'c-primary-dark-600-alpha-500': 'rgba(41, 94, 67, 0.50)',\n  'c-primary-dark-600-alpha-600': 'rgba(41, 94, 67, 0.40)',\n  'c-primary-dark-600-alpha-700': 'rgba(41, 94, 67, 0.30)',\n  'c-primary-dark-600-alpha-800': 'rgba(41, 94, 67, 0.20)',\n  'c-primary-dark-600-alpha-900': 'rgba(41, 94, 67, 0.10)',\n  'c-primary-dark-700': 'rgb(37,85,60)',\n  'c-primary-dark-700-alpha-100': 'rgba(37, 85, 60, 0.90)',\n  'c-primary-dark-700-alpha-200': 'rgba(37, 85, 60, 0.80)',\n  'c-primary-dark-700-alpha-300': 'rgba(37, 85, 60, 0.70)',\n  'c-primary-dark-700-alpha-400': 'rgba(37, 85, 60, 0.60)',\n  'c-primary-dark-700-alpha-500': 'rgba(37, 85, 60, 0.50)',\n  'c-primary-dark-700-alpha-600': 'rgba(37, 85, 60, 0.40)',\n  'c-primary-dark-700-alpha-700': 'rgba(37, 85, 60, 0.30)',\n  'c-primary-dark-700-alpha-800': 'rgba(37, 85, 60, 0.20)',\n  'c-primary-dark-700-alpha-900': 'rgba(37, 85, 60, 0.10)',\n  'c-primary-dark-800': 'rgb(33,77,54)',\n  'c-primary-dark-800-alpha-100': 'rgba(33, 77, 54, 0.90)',\n  'c-primary-dark-800-alpha-200': 'rgba(33, 77, 54, 0.80)',\n  'c-primary-dark-800-alpha-300': 'rgba(33, 77, 54, 0.70)',\n  'c-primary-dark-800-alpha-400': 'rgba(33, 77, 54, 0.60)',\n  'c-primary-dark-800-alpha-500': 'rgba(33, 77, 54, 0.50)',\n  'c-primary-dark-800-alpha-600': 'rgba(33, 77, 54, 0.40)',\n  'c-primary-dark-800-alpha-700': 'rgba(33, 77, 54, 0.30)',\n  'c-primary-dark-800-alpha-800': 'rgba(33, 77, 54, 0.20)',\n  'c-primary-dark-800-alpha-900': 'rgba(33, 77, 54, 0.10)',\n  'c-primary-dark-900': 'rgb(30,69,49)',\n  'c-primary-dark-900-alpha-100': 'rgba(30, 69, 49, 0.90)',\n  'c-primary-dark-900-alpha-200': 'rgba(30, 69, 49, 0.80)',\n  'c-primary-dark-900-alpha-300': 'rgba(30, 69, 49, 0.70)',\n  'c-primary-dark-900-alpha-400': 'rgba(30, 69, 49, 0.60)',\n  'c-primary-dark-900-alpha-500': 'rgba(30, 69, 49, 0.50)',\n  'c-primary-dark-900-alpha-600': 'rgba(30, 69, 49, 0.40)',\n  'c-primary-dark-900-alpha-700': 'rgba(30, 69, 49, 0.30)',\n  'c-primary-dark-900-alpha-800': 'rgba(30, 69, 49, 0.20)',\n  'c-primary-dark-900-alpha-900': 'rgba(30, 69, 49, 0.10)',\n  'c-primary-dark-1000': 'rgb(27,62,44)',\n  'c-primary-dark-1000-alpha-100': 'rgba(27, 62, 44, 0.90)',\n  'c-primary-dark-1000-alpha-200': 'rgba(27, 62, 44, 0.80)',\n  'c-primary-dark-1000-alpha-300': 'rgba(27, 62, 44, 0.70)',\n  'c-primary-dark-1000-alpha-400': 'rgba(27, 62, 44, 0.60)',\n  'c-primary-dark-1000-alpha-500': 'rgba(27, 62, 44, 0.50)',\n  'c-primary-dark-1000-alpha-600': 'rgba(27, 62, 44, 0.40)',\n  'c-primary-dark-1000-alpha-700': 'rgba(27, 62, 44, 0.30)',\n  'c-primary-dark-1000-alpha-800': 'rgba(27, 62, 44, 0.20)',\n  'c-primary-dark-1000-alpha-900': 'rgba(27, 62, 44, 0.10)',\n  'c-primary-light-100': 'rgb(113,191,150)',\n  'c-primary-light-100-alpha-100': 'rgba(113, 191, 150, 0.90)',\n  'c-primary-light-100-alpha-200': 'rgba(113, 191, 150, 0.80)',\n  'c-primary-light-100-alpha-300': 'rgba(113, 191, 150, 0.70)',\n  'c-primary-light-100-alpha-400': 'rgba(113, 191, 150, 0.60)',\n  'c-primary-light-100-alpha-500': 'rgba(113, 191, 150, 0.50)',\n  'c-primary-light-100-alpha-600': 'rgba(113, 191, 150, 0.40)',\n  'c-primary-light-100-alpha-700': 'rgba(113, 191, 150, 0.30)',\n  'c-primary-light-100-alpha-800': 'rgba(113, 191, 150, 0.20)',\n  'c-primary-light-100-alpha-900': 'rgba(113, 191, 150, 0.10)',\n  'c-primary-light-200': 'rgb(141,204,171)',\n  'c-primary-light-200-alpha-100': 'rgba(141, 204, 171, 0.90)',\n  'c-primary-light-200-alpha-200': 'rgba(141, 204, 171, 0.80)',\n  'c-primary-light-200-alpha-300': 'rgba(141, 204, 171, 0.70)',\n  'c-primary-light-200-alpha-400': 'rgba(141, 204, 171, 0.60)',\n  'c-primary-light-200-alpha-500': 'rgba(141, 204, 171, 0.50)',\n  'c-primary-light-200-alpha-600': 'rgba(141, 204, 171, 0.40)',\n  'c-primary-light-200-alpha-700': 'rgba(141, 204, 171, 0.30)',\n  'c-primary-light-200-alpha-800': 'rgba(141, 204, 171, 0.20)',\n  'c-primary-light-200-alpha-900': 'rgba(141, 204, 171, 0.10)',\n  'c-primary-light-300': 'rgb(164,214,188)',\n  'c-primary-light-300-alpha-100': 'rgba(164, 214, 188, 0.90)',\n  'c-primary-light-300-alpha-200': 'rgba(164, 214, 188, 0.80)',\n  'c-primary-light-300-alpha-300': 'rgba(164, 214, 188, 0.70)',\n  'c-primary-light-300-alpha-400': 'rgba(164, 214, 188, 0.60)',\n  'c-primary-light-300-alpha-500': 'rgba(164, 214, 188, 0.50)',\n  'c-primary-light-300-alpha-600': 'rgba(164, 214, 188, 0.40)',\n  'c-primary-light-300-alpha-700': 'rgba(164, 214, 188, 0.30)',\n  'c-primary-light-300-alpha-800': 'rgba(164, 214, 188, 0.20)',\n  'c-primary-light-300-alpha-900': 'rgba(164, 214, 188, 0.10)',\n  'c-primary-light-400': 'rgb(182,222,201)',\n  'c-primary-light-400-alpha-100': 'rgba(182, 222, 201, 0.90)',\n  'c-primary-light-400-alpha-200': 'rgba(182, 222, 201, 0.80)',\n  'c-primary-light-400-alpha-300': 'rgba(182, 222, 201, 0.70)',\n  'c-primary-light-400-alpha-400': 'rgba(182, 222, 201, 0.60)',\n  'c-primary-light-400-alpha-500': 'rgba(182, 222, 201, 0.50)',\n  'c-primary-light-400-alpha-600': 'rgba(182, 222, 201, 0.40)',\n  'c-primary-light-400-alpha-700': 'rgba(182, 222, 201, 0.30)',\n  'c-primary-light-400-alpha-800': 'rgba(182, 222, 201, 0.20)',\n  'c-primary-light-400-alpha-900': 'rgba(182, 222, 201, 0.10)',\n  'c-primary-light-500': 'rgb(197,229,212)',\n  'c-primary-light-500-alpha-100': 'rgba(197, 229, 212, 0.90)',\n  'c-primary-light-500-alpha-200': 'rgba(197, 229, 212, 0.80)',\n  'c-primary-light-500-alpha-300': 'rgba(197, 229, 212, 0.70)',\n  'c-primary-light-500-alpha-400': 'rgba(197, 229, 212, 0.60)',\n  'c-primary-light-500-alpha-500': 'rgba(197, 229, 212, 0.50)',\n  'c-primary-light-500-alpha-600': 'rgba(197, 229, 212, 0.40)',\n  'c-primary-light-500-alpha-700': 'rgba(197, 229, 212, 0.30)',\n  'c-primary-light-500-alpha-800': 'rgba(197, 229, 212, 0.20)',\n  'c-primary-light-500-alpha-900': 'rgba(197, 229, 212, 0.10)',\n  'c-primary-light-600': 'rgb(209,234,221)',\n  'c-primary-light-600-alpha-100': 'rgba(209, 234, 221, 0.90)',\n  'c-primary-light-600-alpha-200': 'rgba(209, 234, 221, 0.80)',\n  'c-primary-light-600-alpha-300': 'rgba(209, 234, 221, 0.70)',\n  'c-primary-light-600-alpha-400': 'rgba(209, 234, 221, 0.60)',\n  'c-primary-light-600-alpha-500': 'rgba(209, 234, 221, 0.50)',\n  'c-primary-light-600-alpha-600': 'rgba(209, 234, 221, 0.40)',\n  'c-primary-light-600-alpha-700': 'rgba(209, 234, 221, 0.30)',\n  'c-primary-light-600-alpha-800': 'rgba(209, 234, 221, 0.20)',\n  'c-primary-light-600-alpha-900': 'rgba(209, 234, 221, 0.10)',\n  'c-primary-light-700': 'rgb(218,238,228)',\n  'c-primary-light-700-alpha-100': 'rgba(218, 238, 228, 0.90)',\n  'c-primary-light-700-alpha-200': 'rgba(218, 238, 228, 0.80)',\n  'c-primary-light-700-alpha-300': 'rgba(218, 238, 228, 0.70)',\n  'c-primary-light-700-alpha-400': 'rgba(218, 238, 228, 0.60)',\n  'c-primary-light-700-alpha-500': 'rgba(218, 238, 228, 0.50)',\n  'c-primary-light-700-alpha-600': 'rgba(218, 238, 228, 0.40)',\n  'c-primary-light-700-alpha-700': 'rgba(218, 238, 228, 0.30)',\n  'c-primary-light-700-alpha-800': 'rgba(218, 238, 228, 0.20)',\n  'c-primary-light-700-alpha-900': 'rgba(218, 238, 228, 0.10)',\n  'c-primary-light-800': 'rgb(225,241,233)',\n  'c-primary-light-800-alpha-100': 'rgba(225, 241, 233, 0.90)',\n  'c-primary-light-800-alpha-200': 'rgba(225, 241, 233, 0.80)',\n  'c-primary-light-800-alpha-300': 'rgba(225, 241, 233, 0.70)',\n  'c-primary-light-800-alpha-400': 'rgba(225, 241, 233, 0.60)',\n  'c-primary-light-800-alpha-500': 'rgba(225, 241, 233, 0.50)',\n  'c-primary-light-800-alpha-600': 'rgba(225, 241, 233, 0.40)',\n  'c-primary-light-800-alpha-700': 'rgba(225, 241, 233, 0.30)',\n  'c-primary-light-800-alpha-800': 'rgba(225, 241, 233, 0.20)',\n  'c-primary-light-800-alpha-900': 'rgba(225, 241, 233, 0.10)',\n  'c-primary-light-900': 'rgb(231,244,237)',\n  'c-primary-light-900-alpha-100': 'rgba(231, 244, 237, 0.90)',\n  'c-primary-light-900-alpha-200': 'rgba(231, 244, 237, 0.80)',\n  'c-primary-light-900-alpha-300': 'rgba(231, 244, 237, 0.70)',\n  'c-primary-light-900-alpha-400': 'rgba(231, 244, 237, 0.60)',\n  'c-primary-light-900-alpha-500': 'rgba(231, 244, 237, 0.50)',\n  'c-primary-light-900-alpha-600': 'rgba(231, 244, 237, 0.40)',\n  'c-primary-light-900-alpha-700': 'rgba(231, 244, 237, 0.30)',\n  'c-primary-light-900-alpha-800': 'rgba(231, 244, 237, 0.20)',\n  'c-primary-light-900-alpha-900': 'rgba(231, 244, 237, 0.10)',\n  'c-primary-light-1000': 'rgb(255,255,255)',\n  'c-primary-light-1000-alpha-100': 'rgba(255, 255, 255, 0.90)',\n  'c-primary-light-1000-alpha-200': 'rgba(255, 255, 255, 0.80)',\n  'c-primary-light-1000-alpha-300': 'rgba(255, 255, 255, 0.70)',\n  'c-primary-light-1000-alpha-400': 'rgba(255, 255, 255, 0.60)',\n  'c-primary-light-1000-alpha-500': 'rgba(255, 255, 255, 0.50)',\n  'c-primary-light-1000-alpha-600': 'rgba(255, 255, 255, 0.40)',\n  'c-primary-light-1000-alpha-700': 'rgba(255, 255, 255, 0.30)',\n  'c-primary-light-1000-alpha-800': 'rgba(255, 255, 255, 0.20)',\n  'c-primary-light-1000-alpha-900': 'rgba(255, 255, 255, 0.10)',\n  'c-theme': 'rgb(77, 175, 124)',\n\n\n  'c-000': 'rgb(255,255,255)',\n  'c-050': 'rgb(244,244,244)',\n  'c-100': 'rgb(233,233,233)',\n  'c-150': 'rgb(222,222,222)',\n  'c-200': 'rgb(211,211,211)',\n  'c-250': 'rgb(200,200,200)',\n  'c-300': 'rgb(188,188,188)',\n  'c-350': 'rgb(177,177,177)',\n  'c-400': 'rgb(166,166,166)',\n  'c-450': 'rgb(155,155,155)',\n  'c-500': 'rgb(144,144,144)',\n  'c-550': 'rgb(133,133,133)',\n  'c-600': 'rgb(122,122,122)',\n  'c-650': 'rgb(111,111,111)',\n  'c-700': 'rgb(100,100,100)',\n  'c-750': 'rgb(89,89,89)',\n  'c-800': 'rgb(77,77,77)',\n  'c-850': 'rgb(66,66,66)',\n  'c-900': 'rgb(55,55,55)',\n  'c-950': 'rgb(44,44,44)',\n  'c-1000': 'rgb(33, 33, 33)',\n}\n\nconst state: InitState = {\n  shouldUseDarkColors: false,\n  theme: {\n    ...theme,\n    'c-app-background': theme['c-primary-light-600-alpha-600'],\n    'c-main-background': 'rgba(255, 255, 255, 0.9)',\n\n    'c-badge-primary': theme['c-primary'],\n    'c-badge-secondary': '#4baed5',\n    'c-badge-tertiary': '#e7aa36',\n\n    'c-font': theme['c-850'],\n    'c-font-label': theme['c-450'],\n    'c-primary-font': theme['c-primary'],\n    'c-primary-font-hover': theme['c-primary-alpha-300'],\n    'c-primary-font-active': theme['c-primary-dark-100-alpha-200'],\n    'c-primary-background': theme['c-primary-light-400-alpha-700'],\n    'c-primary-background-hover': theme['c-primary-light-300-alpha-800'],\n    'c-primary-background-active': theme['c-primary-light-100-alpha-800'],\n    'c-primary-input-background': theme['c-primary-light-400-alpha-700'],\n    'c-button-font': theme['c-primary-alpha-100'],\n    'c-button-font-selected': theme['c-primary-dark-100-alpha-100'],\n    'c-button-background': theme['c-primary-light-400-alpha-700'],\n    'c-button-background-selected': theme['c-primary-alpha-600'],\n    'c-button-background-hover': theme['c-primary-light-300-alpha-600'],\n    'c-button-background-active': theme['c-primary-light-100-alpha-600'],\n    'c-list-header-border-bottom': theme['c-primary-alpha-900'],\n    'c-content-background': theme['c-primary-light-1000'],\n    'c-border-background': theme['c-primary-light-100-alpha-700'],\n\n    'bg-image-position': 'center',\n    'bg-image-size': 'cover',\n  },\n}\n\nexport const ThemeContext = createContext(state.theme)\n\nexport default state\n"
  },
  {
    "path": "src/store/userApi/action.ts",
    "content": "import { state } from './state'\nimport { event } from './event'\n\nexport const setStatus = (status: LX.UserApi.UserApiStatus['status'], message: LX.UserApi.UserApiStatus['message']) => {\n  state.status.status = status\n  state.status.message = message\n\n  event.status_changed({ status, message })\n}\n\n\nexport const setUserApiList = (list: LX.UserApi.UserApiInfo[]) => {\n  state.list = list\n\n  event.list_changed([...list])\n}\n\nexport const addUserApi = (info: LX.UserApi.UserApiInfo) => {\n  state.list.push(info)\n\n  event.list_changed([...state.list])\n}\n\n\nexport const setUserApiAllowShowUpdateAlert = (id: string, enable: boolean) => {\n  const targetIndex = state.list.findIndex(api => api.id == id)\n  if (targetIndex < 0) return\n  state.list[targetIndex].allowShowUpdateAlert = enable\n  state.list.splice(targetIndex, 1, { ...state.list[targetIndex] })\n\n  event.list_changed([...state.list])\n}\n"
  },
  {
    "path": "src/store/userApi/event.ts",
    "content": "import Event from '@/event/Event'\n\n\nclass UserApiEvent extends Event {\n  status_changed(status: { status: boolean, message?: string }) {\n    this.emit('status_changed', status)\n  }\n\n  list_changed(list: LX.UserApi.UserApiInfo[]) {\n    this.emit('list_changed', list)\n  }\n}\n\n\ntype EventMethods = Omit<EventType, keyof Event>\n\n\ndeclare class EventType extends UserApiEvent {\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\ntype UserApiEventTypes = Omit<EventType, keyof Omit<Event, 'on' | 'off'>>\n\n\nexport const event: UserApiEventTypes = new UserApiEvent()\n"
  },
  {
    "path": "src/store/userApi/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport { state } from './state'\nimport { event } from './event'\n\nexport const useStatus = () => {\n  const [value, update] = useState(state.status)\n\n  useEffect(() => {\n    event.on('status_changed', update)\n    return () => {\n      event.off('status_changed', update)\n    }\n  }, [])\n\n  return value\n}\n\nexport const useUserApiList = () => {\n  const [value, update] = useState(state.list)\n\n  useEffect(() => {\n    event.on('list_changed', update)\n    return () => {\n      event.off('list_changed', update)\n    }\n  }, [])\n\n  return value\n}\n"
  },
  {
    "path": "src/store/userApi/index.ts",
    "content": "\nexport * as action from './action'\nexport * from './state'\nexport * from './hook'\n"
  },
  {
    "path": "src/store/userApi/state.ts",
    "content": "\ninterface InitState {\n  list: LX.UserApi.UserApiInfo[]\n  status: {\n    status: boolean\n    message?: string\n  }\n  apis: Partial<LX.UserApi.UserApiSources>\n}\nconst state: InitState = {\n  list: [],\n  status: {\n    status: false,\n    message: 'initing',\n  },\n  apis: {},\n}\n\n\nexport {\n  state,\n}\n"
  },
  {
    "path": "src/store/version/action.ts",
    "content": "import state, { type InitState } from './state'\n\n\nexport default {\n  setVersionInfo(info: Partial<InitState['versionInfo']>) {\n    Object.assign(state.versionInfo, info)\n    global.state_event.versionInfoUpdated({ ...state.versionInfo })\n  },\n  setIgnoreVersion(version: InitState['ignoreVersion']) {\n    state.ignoreVersion = version\n    global.state_event.versionInfoIgnoreVersionUpdated(version)\n  },\n  setProgress(info: InitState['progress']) {\n    if (state.progress.total != info.total) {\n      state.progress.total = info.total\n    }\n    state.progress.current = info.current\n\n    global.state_event.versionDownloadProgressUpdated({ ...state.progress })\n  },\n  setVisibleModal(visible: boolean) {\n    if (state.showModal == visible) return\n    state.showModal = visible\n  },\n}\n\n"
  },
  {
    "path": "src/store/version/hook.ts",
    "content": "import { useEffect, useState } from 'react'\nimport state from './state'\n\nexport const useVersionInfo = () => {\n  const [info, setInfo] = useState(state.versionInfo)\n\n  useEffect(() => {\n    global.state_event.on('versionInfoUpdated', setInfo)\n    return () => {\n      global.state_event.off('versionInfoUpdated', setInfo)\n    }\n  }, [])\n\n  return info\n}\n\nexport const useVersionDownloadProgressUpdated = () => {\n  const [status, setStatus] = useState(state.progress)\n\n  useEffect(() => {\n    global.state_event.on('versionDownloadProgressUpdated', setStatus)\n    return () => {\n      global.state_event.off('versionDownloadProgressUpdated', setStatus)\n    }\n  }, [])\n\n  return status\n}\n\nexport const useVersionInfoIgnoreVersionUpdated = () => {\n  const [version, setVersion] = useState(state.ignoreVersion)\n\n  useEffect(() => {\n    global.state_event.on('versionInfoIgnoreVersionUpdated', setVersion)\n    return () => {\n      global.state_event.off('versionInfoIgnoreVersionUpdated', setVersion)\n    }\n  }, [])\n\n  return version\n}\n\n// export const useActiveListId = () => {\n//   const [id, setId] = useState(state.activeListId)\n\n//   useEffect(() => {\n//     global.state_event.on('mylistToggled', setId)\n//     return () => {\n//       global.state_event.off('mylistToggled', setId)\n//     }\n//   }, [])\n\n//   return id\n// }\n\n\n// export const useMusicList = () => {\n//   const [list, setList] = useState<LX.List.ListMusics>([])\n\n//   useEffect(() => {\n//     const handleToggle = (activeListId: string) => {\n//       void getListMusics(activeListId).then(setList)\n//     }\n//     const handleChange = (ids: string[]) => {\n//       if (!ids.includes(state.activeListId)) return\n//       void getListMusics(state.activeListId).then(setList)\n//     }\n//     global.state_event.on('mylistToggled', handleToggle)\n//     global.app_event.on('myListMusicUpdate', handleChange)\n\n//     handleToggle(state.activeListId)\n\n//     return () => {\n//       global.state_event.off('mylistToggled', handleToggle)\n//       global.app_event.off('myListMusicUpdate', handleChange)\n//     }\n//   }, [])\n\n//   return list\n// }\n"
  },
  {
    "path": "src/store/version/state.ts",
    "content": "import { version } from '../../../package.json'\n\nexport interface ProgressInfo {\n  total: number\n  current: number\n}\n\nexport interface VersionInfo {\n  version: string\n  desc: string\n  history?: LX.VersionInfo[]\n}\n\nexport interface InitState {\n  showModal: boolean\n  versionInfo: {\n    version: string\n    newVersion: VersionInfo | null\n    showModal: boolean\n    isUnknown: boolean\n    isLatest: boolean\n    reCheck: boolean\n    status: LX.UpdateStatus\n  }\n  ignoreVersion: string | null\n  progress: ProgressInfo\n}\n\nconst state: InitState = {\n  showModal: false,\n  versionInfo: {\n    version,\n    newVersion: null,\n    showModal: false,\n    reCheck: false,\n    isUnknown: false,\n    isLatest: false,\n    status: 'checking',\n  },\n  ignoreVersion: null,\n  progress: {\n    total: 0,\n    current: 0,\n  },\n}\n\n\nexport default state\n"
  },
  {
    "path": "src/theme/Colors.js",
    "content": "/**\n * Material Colors following material design guidelines for\n * miscellaneous colors\n */\n\nexport const MaterialColors = {\n  red: {\n    100: '#ffcdd2',\n    200: '#ef9a9a',\n    300: '#e57373',\n    400: '#ef5350',\n    500: '#f44336',\n    600: '#e53935',\n    700: '#d32f2f',\n    800: '#c62828',\n    900: '#b71c1c',\n  },\n  purple: {\n    100: '#e1bee7',\n    200: '#ce93d8',\n    300: '#ba68c8',\n    400: '#ab47bc',\n    500: '#9c27b0',\n    600: '#8e24aa',\n    700: '#7b1fa2',\n    800: '#6a1b9a',\n    900: '#4a148c',\n  },\n  blue: {\n    100: '#bbdefb',\n    200: '#90caf9',\n    300: '#64b5f6',\n    400: '#42a5f5',\n    500: '#2196f3',\n    600: '#1e88e5',\n    700: '#1976d2',\n    800: '#1565c0',\n    900: '#0d47a1',\n  },\n  green: {\n    100: '#c8e6c9',\n    200: '#a5d6a7',\n    300: '#81c784',\n    400: '#66bb6a',\n    500: '#4caf50',\n    600: '#43a047',\n    700: '#388e3c',\n    800: '#2e7d32',\n    900: '#1b5e20',\n  },\n  yellow: {\n    100: '#fff9c4',\n    200: '#fff59d',\n    300: '#fff176',\n    400: '#ffee58',\n    500: '#ffeb3b',\n    600: '#fdd835',\n    700: '#fbc02d',\n    800: '#f9a825',\n    900: '#f57f17',\n  },\n  orange: {\n    100: '#ffe0b2',\n    200: '#ffcc80',\n    300: '#ffb74d',\n    400: '#ffa726',\n    500: '#ff9800',\n    600: '#fb8c00',\n    700: '#f57c00',\n    800: '#ef6c00',\n    900: '#e65100',\n  },\n  brown: {\n    100: '#d7ccc8',\n    200: '#bcaaa4',\n    300: '#a1887f',\n    400: '#8d6e63',\n    500: '#795548',\n    600: '#6d4c41',\n    700: '#5d4037',\n    800: '#4e342e',\n    900: '#3e2723',\n  },\n  grey: {\n    100: '#f5f5f5',\n    200: '#eeeeee',\n    300: '#e0e0e0',\n    400: '#bdbdbd',\n    500: '#9e9e9e',\n    600: '#757575',\n    700: '#616161',\n    800: '#424242',\n    900: '#212121',\n  },\n}\n\n/**\n * App Colors:\n * This contains all the color config for the application\n */\n\nexport const AppColors = {\n  primary: '#fff',\n\n  normal: '#333',\n  normal10: '#4c4c4c',\n  normal20: '#666',\n  normal30: '#808080',\n  normal40: '#999',\n\n  secondary: '#4eb17d',\n  secondary10: '#73bf97',\n  secondary20: '#95d0b2',\n  secondary30: '#b9dfcb',\n  secondary40: '#dcefe5',\n\n  borderColor: '#e6e6e6',\n  borderColor2: '#ebebeb',\n  borderColor3: '#f0f0f0',\n  borderColor4: '#f5f5f5',\n  borderColor5: '#fafafa',\n}\n"
  },
  {
    "path": "src/theme/Typography.js",
    "content": "/**\n * Typography:\n * This contains all the typography config for the application\n * #Note: color and font size are defaulted as they can be overridden\n *        as required.\n */\n\nexport const FontWeights = {\n  Bold: {\n    fontFamily: 'SFProDisplay-Bold',\n    color: '#000',\n  },\n  Regular: {\n    fontFamily: 'SFProDisplay-Regular',\n    color: '#000',\n  },\n  Light: {\n    fontFamily: 'SFProDisplay-Light',\n    color: '#000',\n  },\n}\n\nexport const FontSizes = {\n  Heading: {\n    fontSize: 32,\n  },\n  SubHeading: {\n    fontSize: 24,\n  },\n  Label: {\n    fontSize: 20,\n  },\n  Body: {\n    fontSize: 16,\n  },\n  Caption: {\n    fontSize: 14,\n  },\n}\n\nexport const BorderWidths = {\n  normal: 0.4,\n  normal1: 0.6,\n  normal2: 1,\n  normal3: 1.4,\n  normal4: 2,\n}\n\nexport const BorderRadius = {\n  normal: 4,\n}\n"
  },
  {
    "path": "src/theme/index.js",
    "content": "import { AppColors, MaterialColors } from './Colors'\nimport {\n  FontWeights,\n  FontSizes,\n  BorderWidths,\n  BorderRadius,\n} from './Typography'\n\nexport { default as Themes } from './themes'\nexport {\n  AppColors,\n  MaterialColors,\n  FontWeights,\n  FontSizes,\n  BorderWidths,\n  BorderRadius,\n}\n"
  },
  {
    "path": "src/theme/themes/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/theme/themes/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    config: {\n      primary: 'rgb(77, 175, 124)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#4baed5',\n      'c-badge-tertiary': '#e7aa36',\n    },\n  },\n  {\n    id: 'blue',\n    name: '蓝田生玉',\n    isDark: false,\n    config: {\n      primary: 'rgb(52, 152, 219)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#5cbf9b',\n      'c-badge-tertiary': '#5cbf9b',\n    },\n  },\n  {\n    id: 'blue_plus',\n    name: '蛋雅深蓝',\n    isDark: false,\n    config: {\n      primary: 'rgb(77, 131, 175)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-600)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': 'rgba(66.6, 150.7, 171, 1)',\n      'c-badge-tertiary': 'rgba(54, 196, 231, 1)',\n    },\n  },\n  {\n    id: 'orange',\n    name: '橙黄橘绿',\n    isDark: false,\n    config: {\n      primary: 'rgb(245, 171, 53)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#9ed458',\n      'c-badge-tertiary': '#9ed458',\n    },\n  },\n  {\n    id: 'brown',\n    name: '泥牛入海',\n    isDark: false,\n    config: {\n      primary: 'rgba(188, 128, 68, 1)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#483472',\n      'c-badge-tertiary': '#647D39',\n    },\n  },\n  {\n    id: 'red',\n    name: '热情似火',\n    isDark: false,\n    config: {\n      primary: 'rgb(214, 69, 65)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#dfbb6b',\n      'c-badge-tertiary': '#dfbb6b',\n    },\n  },\n  {\n    id: 'pink',\n    name: '粉装玉琢',\n    isDark: false,\n    config: {\n      primary: 'rgb(241, 130, 141)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#f5b684',\n      'c-badge-tertiary': '#f5b684',\n    },\n  },\n  {\n    id: 'purple',\n    name: '重斤球紫',\n    isDark: false,\n    config: {\n      primary: 'rgb(155, 89, 182)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#e5a39f',\n      'c-badge-tertiary': '#e5a39f',\n    },\n  },\n  {\n    id: 'grey',\n    name: '灰常美丽',\n    isDark: false,\n    config: {\n      primary: 'rgb(108, 122, 137)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#b19b9f',\n      'c-badge-tertiary': '#b19b9f',\n    },\n  },\n  {\n    id: 'ming',\n    name: '青出于黑',\n    isDark: false,\n    config: {\n      primary: 'rgb(51, 110, 123)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#6376a2',\n      'c-badge-tertiary': '#6376a2',\n    },\n  },\n  {\n    id: 'blue2',\n    name: '清热板蓝',\n    isDark: false,\n    config: {\n      primary: 'rgb(79, 98, 208)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'var(c-primary-light-600-alpha-700)',\n      'c-main-background': 'rgba(255, 255, 255, 1)',\n      'bg-image': '',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#b080db',\n      'c-badge-tertiary': '#b080db',\n    },\n  },\n  {\n    id: 'black',\n    name: '黑灯瞎火',\n    isDark: true,\n    config: {\n      primary: 'rgb(190, 190, 190)',\n      font: 'rgb(255, 255, 255)',\n      'c-app-background': 'rgba(0, 0, 0, 0)',\n      'c-main-background': 'rgba(19, 19, 19, 0.95)',\n      'bg-image': 'landingMoon.png',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary-dark-200)',\n      'c-badge-secondary': 'var(c-primary)',\n      'c-badge-tertiary': 'var(c-primary-dark-300)',\n    },\n  },\n  {\n    id: 'mid_autumn',\n    name: '月里嫦娥',\n    isDark: false,\n    config: {\n      primary: 'rgb(74, 55, 82)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'rgba(255, 255, 255, 0)',\n      'c-main-background': 'rgba(255, 255, 255, 0.9)',\n      'bg-image': 'jqbg.jpg',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': '#af9479',\n      'c-badge-tertiary': '#af9479',\n    },\n  },\n  {\n    id: 'naruto',\n    name: '木叶之村',\n    isDark: false,\n    config: {\n      primary: 'rgb(87, 144, 167)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'rgba(255, 255, 255, 0.15)',\n      'c-main-background': 'rgba(255, 255, 255, 0.8)',\n      'bg-image': 'myzcbg.jpg',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': 'var(c-primary)',\n      'c-badge-secondary': 'var(c-primary-light-100)',\n      'c-badge-tertiary': 'var(c-primary-light-100)',\n    },\n  },\n  {\n    id: 'china_ink',\n    name: '近墨者黑',\n    isDark: false,\n    config: {\n      primary: 'rgba(47, 47, 47, 1)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'rgba(255, 255, 255, 0)',\n      'c-main-background': 'rgba(255, 255, 255, 0.8)',\n      'bg-image': 'china_ink.jpg',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n\n      'c-badge-primary': 'rgba(137, 70, 70, 1)',\n      'c-badge-secondary': 'rgba(67, 139, 65, 1)',\n      'c-badge-tertiary': 'rgba(132, 135, 65, 1)',\n    },\n  },\n  {\n    id: 'happy_new_year',\n    name: '新年快乐',\n    isDark: false,\n    config: {\n      primary: 'rgb(192, 57, 43)',\n      font: 'rgb(33, 33, 33)',\n      'c-app-background': 'rgba(255, 255, 255, 0.15)',\n      'c-main-background': 'rgba(255, 255, 255, 0.8)',\n      'bg-image': 'xnkl.png',\n      'bg-image-position': 'center',\n      'bg-image-size': 'cover',\n\n      'c-badge-primary': '#7fb575',\n      'c-badge-secondary': '#dfbb6b',\n      'c-badge-tertiary': 'var(c-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, 'themes.ts'), `/* eslint-disable */\\n//! 此文件由 createThemes.js 生成\\n\\nexport default ${JSON.stringify(themes, null, 2)} as const`)\n\n"
  },
  {
    "path": "src/theme/themes/index.ts",
    "content": "/* eslint-disable @typescript-eslint/no-var-requires */\nimport { getUserTheme, saveUserTheme } from '@/utils/data'\nimport themes from '@/theme/themes/themes'\nimport settingState from '@/store/setting/state'\nimport themeState from '@/store/theme/state'\nimport { isUrl } from '@/utils'\nimport { privateStorageDirectoryPath } from '@/utils/fs'\nimport { type ImageSourcePropType } from 'react-native'\n\nexport const BG_IMAGES = {\n  'china_ink.jpg': require('./images/china_ink.jpg') as ImageSourcePropType,\n  'jqbg.jpg': require('./images/jqbg.jpg') as ImageSourcePropType,\n  'landingMoon.png': require('./images/landingMoon2.png') as ImageSourcePropType,\n  'myzcbg.jpg': require('./images/myzcbg.jpg') as ImageSourcePropType,\n  'xnkl.png': require('./images/xnkl.png') as ImageSourcePropType,\n} as const\n\n\nlet userThemes: LX.Theme[]\nexport const getAllThemes = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  userThemes ??= await getUserTheme()\n  return {\n    themes,\n    userThemes,\n    dataPath: privateStorageDirectoryPath + '/theme_images',\n  }\n}\n\nexport const saveTheme = async(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  await saveUserTheme(userThemes)\n}\n\nexport const removeTheme = async(id: string) => {\n  const index = userThemes.findIndex(t => t.id === id)\n  if (index < 0) return\n  userThemes.splice(index, 1)\n  await saveUserTheme(userThemes)\n}\n\nexport type LocalTheme = typeof themes[number]\ntype ColorsKey = keyof LX.Theme['config']['themeColors']\ntype ExtInfoKey = keyof LX.Theme['config']['extInfo']\nconst varColorRxp = /^var\\((.+)\\)$/\nexport const buildActiveThemeColors = (theme: LX.Theme): LX.ActiveTheme => {\n  let bgImg: ImageSourcePropType | undefined\n  if (theme.isCustom) {\n    if (theme.config.extInfo['bg-image']) {\n      theme.config.extInfo['bg-image'] =\n        isUrl(theme.config.extInfo['bg-image'])\n          ? theme.config.extInfo['bg-image']\n          : `${privateStorageDirectoryPath}/theme_images/${theme.config.extInfo['bg-image']}`\n    }\n  } else {\n    const extInfo = (theme as LocalTheme).config.extInfo\n    if (extInfo['bg-image']) {\n      if (!theme.isDark || !settingState.setting['theme.hideBgDark']) bgImg = BG_IMAGES[extInfo['bg-image']]\n    }\n  }\n\n  theme.config.extInfo = { ...theme.config.extInfo }\n\n  for (const [k, v] of Object.entries(theme.config.extInfo) as Array<[ExtInfoKey, LX.Theme['config']['extInfo'][ExtInfoKey]]>) {\n    if (!v.startsWith('var(')) continue\n    theme.config.extInfo[k] = theme.config.themeColors[v.replace(varColorRxp, '$1') as ColorsKey]\n  }\n\n  return {\n    id: theme.id,\n    name: theme.name,\n    isDark: theme.isDark,\n    ...theme.config.themeColors,\n    ...theme.config.extInfo,\n    'c-font': theme.config.themeColors['c-850'],\n    'c-font-label': theme.config.themeColors['c-450'],\n    'c-primary-font': theme.config.themeColors['c-primary'],\n    'c-primary-font-hover': theme.config.themeColors['c-primary-alpha-300'],\n    'c-primary-font-active': theme.config.themeColors['c-primary-dark-100-alpha-200'],\n    'c-primary-background': theme.config.themeColors['c-primary-light-400-alpha-700'],\n    'c-primary-background-hover': theme.config.themeColors['c-primary-light-300-alpha-800'],\n    'c-primary-background-active': theme.config.themeColors['c-primary-light-100-alpha-800'],\n    'c-primary-input-background': theme.config.themeColors['c-primary-light-400-alpha-700'],\n    'c-button-font': theme.config.themeColors['c-primary-alpha-100'],\n    'c-button-font-selected': theme.config.themeColors['c-primary-dark-100-alpha-100'],\n    'c-button-background': theme.config.themeColors['c-primary-light-400-alpha-700'],\n    'c-button-background-selected': theme.config.themeColors['c-primary-alpha-600'],\n    'c-button-background-hover': theme.config.themeColors['c-primary-light-300-alpha-600'],\n    'c-button-background-active': theme.config.themeColors['c-primary-light-100-alpha-600'],\n    'c-list-header-border-bottom': theme.config.themeColors['c-primary-alpha-900'],\n    'c-content-background': theme.config.themeColors['c-primary-light-1000'],\n    'c-border-background': theme.config.themeColors['c-primary-light-100-alpha-700'],\n    'bg-image': bgImg,\n  } as const\n}\n\n\n// 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// type IDS = LocalTheme['id']\nexport const getTheme = async() => {\n  // fs.promises.readdir()\n  const shouldUseDarkColors = themeState.shouldUseDarkColors\n  // let themeId = settingState.setting['theme.id'] == 'auto'\n  //   ? shouldUseDarkColors\n  //     ? settingState.setting['theme.darkId']\n  //     : settingState.setting['theme.lightId']\n  //   // : 'china_ink'\n  //   : settingState.setting['theme.id']\n  let themeId = settingState.setting['common.isAutoTheme'] && shouldUseDarkColors\n    ? 'black'\n    : settingState.setting['theme.id']\n  // themeId = 'naruto'\n  // themeId = 'pink'\n  // themeId = 'black'\n  let theme: LocalTheme | LX.Theme | undefined = themes.find(theme => theme.id == themeId)\n  if (!theme) {\n    userThemes = await getUserTheme()\n    theme = userThemes.find(theme => theme.id == themeId)\n    if (!theme) {\n      themeId = settingState.setting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green'\n      theme = themes.find(theme => theme.id == themeId) as LX.Theme\n    }\n  }\n\n  return theme\n}\n"
  },
  {
    "path": "src/theme/themes/themes.ts",
    "content": "/* eslint-disable */\n//! 此文件由 createThemes.js 生成\n\nexport default [\n  {\n    \"id\": \"green\",\n    \"name\": \"绿意盎然\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(77, 175, 124)\",\n        \"c-primary-dark-100\": \"rgb(69,158,112)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(69, 158, 112, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(77, 175, 124, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(69, 158, 112, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(77, 175, 124, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(69, 158, 112, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(77, 175, 124, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(69, 158, 112, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(77, 175, 124, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(69, 158, 112, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(77, 175, 124, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(69, 158, 112, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(77, 175, 124, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(69, 158, 112, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(77, 175, 124, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(69, 158, 112, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(77, 175, 124, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(69, 158, 112, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(77, 175, 124, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(62,142,101)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(62, 142, 101, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(62, 142, 101, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(62, 142, 101, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(62, 142, 101, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(62, 142, 101, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(62, 142, 101, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(62, 142, 101, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(62, 142, 101, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(62, 142, 101, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(56,128,91)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(56, 128, 91, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(56, 128, 91, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(56, 128, 91, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(56, 128, 91, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(56, 128, 91, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(56, 128, 91, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(56, 128, 91, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(56, 128, 91, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(56, 128, 91, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(50,115,82)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(50, 115, 82, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(50, 115, 82, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(50, 115, 82, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(50, 115, 82, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(50, 115, 82, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(50, 115, 82, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(50, 115, 82, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(50, 115, 82, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(50, 115, 82, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(45,104,74)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(45, 104, 74, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(45, 104, 74, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(45, 104, 74, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(45, 104, 74, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(45, 104, 74, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(45, 104, 74, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(45, 104, 74, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(45, 104, 74, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(45, 104, 74, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(41,94,67)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(41, 94, 67, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(41, 94, 67, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(41, 94, 67, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(41, 94, 67, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(41, 94, 67, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(41, 94, 67, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(41, 94, 67, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(41, 94, 67, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(41, 94, 67, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(37,85,60)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(37, 85, 60, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(37, 85, 60, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(37, 85, 60, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(37, 85, 60, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(37, 85, 60, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(37, 85, 60, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(37, 85, 60, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(37, 85, 60, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(37, 85, 60, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(33,77,54)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(33, 77, 54, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(33, 77, 54, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(33, 77, 54, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(33, 77, 54, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(33, 77, 54, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(33, 77, 54, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(33, 77, 54, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(33, 77, 54, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(33, 77, 54, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(30,69,49)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(30, 69, 49, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(30, 69, 49, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(30, 69, 49, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(30, 69, 49, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(30, 69, 49, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(30, 69, 49, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(30, 69, 49, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(30, 69, 49, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(30, 69, 49, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(27,62,44)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(27, 62, 44, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(27, 62, 44, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(27, 62, 44, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(27, 62, 44, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(27, 62, 44, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(27, 62, 44, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(27, 62, 44, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(27, 62, 44, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(27, 62, 44, 0.10)\",\n        \"c-primary-light-100\": \"rgb(113,191,150)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(113, 191, 150, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(113, 191, 150, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(113, 191, 150, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(113, 191, 150, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(113, 191, 150, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(113, 191, 150, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(113, 191, 150, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(113, 191, 150, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(113, 191, 150, 0.10)\",\n        \"c-primary-light-200\": \"rgb(141,204,171)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(141, 204, 171, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(141, 204, 171, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(141, 204, 171, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(141, 204, 171, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(141, 204, 171, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(141, 204, 171, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(141, 204, 171, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(141, 204, 171, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(141, 204, 171, 0.10)\",\n        \"c-primary-light-300\": \"rgb(164,214,188)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(164, 214, 188, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(164, 214, 188, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(164, 214, 188, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(164, 214, 188, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(164, 214, 188, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(164, 214, 188, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(164, 214, 188, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(164, 214, 188, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(164, 214, 188, 0.10)\",\n        \"c-primary-light-400\": \"rgb(182,222,201)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(182, 222, 201, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(182, 222, 201, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(182, 222, 201, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(182, 222, 201, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(182, 222, 201, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(182, 222, 201, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(182, 222, 201, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(182, 222, 201, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(182, 222, 201, 0.10)\",\n        \"c-primary-light-500\": \"rgb(197,229,212)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(197, 229, 212, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(197, 229, 212, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(197, 229, 212, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(197, 229, 212, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(197, 229, 212, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(197, 229, 212, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(197, 229, 212, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(197, 229, 212, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(197, 229, 212, 0.10)\",\n        \"c-primary-light-600\": \"rgb(209,234,221)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(209, 234, 221, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(209, 234, 221, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(209, 234, 221, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(209, 234, 221, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(209, 234, 221, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(209, 234, 221, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(209, 234, 221, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(209, 234, 221, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(209, 234, 221, 0.10)\",\n        \"c-primary-light-700\": \"rgb(218,238,228)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(218, 238, 228, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(218, 238, 228, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(218, 238, 228, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(218, 238, 228, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(218, 238, 228, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(218, 238, 228, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(218, 238, 228, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(218, 238, 228, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(218, 238, 228, 0.10)\",\n        \"c-primary-light-800\": \"rgb(225,241,233)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(225, 241, 233, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(225, 241, 233, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(225, 241, 233, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(225, 241, 233, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(225, 241, 233, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(225, 241, 233, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(225, 241, 233, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(225, 241, 233, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(225, 241, 233, 0.10)\",\n        \"c-primary-light-900\": \"rgb(231,244,237)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(231, 244, 237, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(231, 244, 237, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(231, 244, 237, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(231, 244, 237, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(231, 244, 237, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(231, 244, 237, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(231, 244, 237, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(231, 244, 237, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(231, 244, 237, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(77, 175, 124)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#4baed5\",\n        \"c-badge-tertiary\": \"#e7aa36\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue\",\n    \"name\": \"蓝田生玉\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(52, 152, 219)\",\n        \"c-primary-dark-100\": \"rgb(47,137,197)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(47, 137, 197, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(52, 152, 219, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(47, 137, 197, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(52, 152, 219, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(47, 137, 197, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(52, 152, 219, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(47, 137, 197, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(52, 152, 219, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(47, 137, 197, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(52, 152, 219, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(47, 137, 197, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(52, 152, 219, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(47, 137, 197, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(52, 152, 219, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(47, 137, 197, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(52, 152, 219, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(47, 137, 197, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(52, 152, 219, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(42,123,177)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(42, 123, 177, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(42, 123, 177, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(42, 123, 177, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(42, 123, 177, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(42, 123, 177, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(42, 123, 177, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(42, 123, 177, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(42, 123, 177, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(42, 123, 177, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(38,111,159)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(38, 111, 159, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(38, 111, 159, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(38, 111, 159, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(38, 111, 159, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(38, 111, 159, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(38, 111, 159, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(38, 111, 159, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(38, 111, 159, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(38, 111, 159, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(34,100,143)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(34, 100, 143, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(34, 100, 143, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(34, 100, 143, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(34, 100, 143, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(34, 100, 143, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(34, 100, 143, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(34, 100, 143, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(34, 100, 143, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(34, 100, 143, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(31,90,129)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(31, 90, 129, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(31, 90, 129, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(31, 90, 129, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(31, 90, 129, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(31, 90, 129, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(31, 90, 129, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(31, 90, 129, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(31, 90, 129, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(31, 90, 129, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(28,81,116)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(28, 81, 116, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(28, 81, 116, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(28, 81, 116, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(28, 81, 116, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(28, 81, 116, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(28, 81, 116, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(28, 81, 116, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(28, 81, 116, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(28, 81, 116, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(25,73,104)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(25, 73, 104, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(25, 73, 104, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(25, 73, 104, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(25, 73, 104, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(25, 73, 104, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(25, 73, 104, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(25, 73, 104, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(25, 73, 104, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(25, 73, 104, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(23,66,94)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(23, 66, 94, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(23, 66, 94, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(23, 66, 94, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(23, 66, 94, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(23, 66, 94, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(23, 66, 94, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(23, 66, 94, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(23, 66, 94, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(23, 66, 94, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(21,59,85)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(21, 59, 85, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(21, 59, 85, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(21, 59, 85, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(21, 59, 85, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(21, 59, 85, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(21, 59, 85, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(21, 59, 85, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(21, 59, 85, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(21, 59, 85, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(19,53,77)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(19, 53, 77, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(19, 53, 77, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(19, 53, 77, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(19, 53, 77, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(19, 53, 77, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(19, 53, 77, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(19, 53, 77, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(19, 53, 77, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(19, 53, 77, 0.10)\",\n        \"c-primary-light-100\": \"rgb(93,173,226)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(93, 173, 226, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(93, 173, 226, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(93, 173, 226, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(93, 173, 226, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(93, 173, 226, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(93, 173, 226, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(93, 173, 226, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(93, 173, 226, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(93, 173, 226, 0.10)\",\n        \"c-primary-light-200\": \"rgb(125,189,232)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(125, 189, 232, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(125, 189, 232, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(125, 189, 232, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(125, 189, 232, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(125, 189, 232, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(125, 189, 232, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(125, 189, 232, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(125, 189, 232, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(125, 189, 232, 0.10)\",\n        \"c-primary-light-300\": \"rgb(151,202,237)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(151, 202, 237, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(151, 202, 237, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(151, 202, 237, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(151, 202, 237, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(151, 202, 237, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(151, 202, 237, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(151, 202, 237, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(151, 202, 237, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(151, 202, 237, 0.10)\",\n        \"c-primary-light-400\": \"rgb(172,213,241)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(172, 213, 241, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(172, 213, 241, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(172, 213, 241, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(172, 213, 241, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(172, 213, 241, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(172, 213, 241, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(172, 213, 241, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(172, 213, 241, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(172, 213, 241, 0.10)\",\n        \"c-primary-light-500\": \"rgb(189,221,244)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(189, 221, 244, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(189, 221, 244, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(189, 221, 244, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(189, 221, 244, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(189, 221, 244, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(189, 221, 244, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(189, 221, 244, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(189, 221, 244, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(189, 221, 244, 0.10)\",\n        \"c-primary-light-600\": \"rgb(202,228,246)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(202, 228, 246, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(202, 228, 246, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(202, 228, 246, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(202, 228, 246, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(202, 228, 246, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(202, 228, 246, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(202, 228, 246, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(202, 228, 246, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(202, 228, 246, 0.10)\",\n        \"c-primary-light-700\": \"rgb(213,233,248)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(213, 233, 248, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(213, 233, 248, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(213, 233, 248, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(213, 233, 248, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(213, 233, 248, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(213, 233, 248, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(213, 233, 248, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(213, 233, 248, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(213, 233, 248, 0.10)\",\n        \"c-primary-light-800\": \"rgb(221,237,249)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(221, 237, 249, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(221, 237, 249, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(221, 237, 249, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(221, 237, 249, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(221, 237, 249, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(221, 237, 249, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(221, 237, 249, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(221, 237, 249, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(221, 237, 249, 0.10)\",\n        \"c-primary-light-900\": \"rgb(228,241,250)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(228, 241, 250, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(228, 241, 250, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(228, 241, 250, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(228, 241, 250, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(228, 241, 250, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(228, 241, 250, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(228, 241, 250, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(228, 241, 250, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(228, 241, 250, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(52, 152, 219)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#5cbf9b\",\n        \"c-badge-tertiary\": \"#5cbf9b\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue_plus\",\n    \"name\": \"蛋雅深蓝\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(77, 131, 175)\",\n        \"c-primary-dark-100\": \"rgb(69,118,158)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(69, 118, 158, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(77, 131, 175, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(69, 118, 158, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(77, 131, 175, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(69, 118, 158, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(77, 131, 175, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(69, 118, 158, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(77, 131, 175, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(69, 118, 158, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(77, 131, 175, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(69, 118, 158, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(77, 131, 175, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(69, 118, 158, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(77, 131, 175, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(69, 118, 158, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(77, 131, 175, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(69, 118, 158, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(77, 131, 175, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(62,106,142)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(62, 106, 142, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(62, 106, 142, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(62, 106, 142, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(62, 106, 142, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(62, 106, 142, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(62, 106, 142, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(62, 106, 142, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(62, 106, 142, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(62, 106, 142, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(56,95,128)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(56, 95, 128, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(56, 95, 128, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(56, 95, 128, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(56, 95, 128, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(56, 95, 128, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(56, 95, 128, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(56, 95, 128, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(56, 95, 128, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(56, 95, 128, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(50,86,115)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(50, 86, 115, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(50, 86, 115, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(50, 86, 115, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(50, 86, 115, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(50, 86, 115, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(50, 86, 115, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(50, 86, 115, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(50, 86, 115, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(50, 86, 115, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(45,77,104)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(45, 77, 104, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(45, 77, 104, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(45, 77, 104, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(45, 77, 104, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(45, 77, 104, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(45, 77, 104, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(45, 77, 104, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(45, 77, 104, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(45, 77, 104, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(41,69,94)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(41, 69, 94, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(41, 69, 94, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(41, 69, 94, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(41, 69, 94, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(41, 69, 94, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(41, 69, 94, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(41, 69, 94, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(41, 69, 94, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(41, 69, 94, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(37,62,85)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(37, 62, 85, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(37, 62, 85, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(37, 62, 85, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(37, 62, 85, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(37, 62, 85, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(37, 62, 85, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(37, 62, 85, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(37, 62, 85, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(37, 62, 85, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(33,56,77)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(33, 56, 77, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(33, 56, 77, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(33, 56, 77, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(33, 56, 77, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(33, 56, 77, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(33, 56, 77, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(33, 56, 77, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(33, 56, 77, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(33, 56, 77, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(30,50,69)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(30, 50, 69, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(30, 50, 69, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(30, 50, 69, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(30, 50, 69, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(30, 50, 69, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(30, 50, 69, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(30, 50, 69, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(30, 50, 69, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(30, 50, 69, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(27,45,62)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(27, 45, 62, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(27, 45, 62, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(27, 45, 62, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(27, 45, 62, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(27, 45, 62, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(27, 45, 62, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(27, 45, 62, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(27, 45, 62, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(27, 45, 62, 0.10)\",\n        \"c-primary-light-100\": \"rgb(113,156,191)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(113, 156, 191, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(113, 156, 191, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(113, 156, 191, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(113, 156, 191, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(113, 156, 191, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(113, 156, 191, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(113, 156, 191, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(113, 156, 191, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(113, 156, 191, 0.10)\",\n        \"c-primary-light-200\": \"rgb(141,176,204)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(141, 176, 204, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(141, 176, 204, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(141, 176, 204, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(141, 176, 204, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(141, 176, 204, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(141, 176, 204, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(141, 176, 204, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(141, 176, 204, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(141, 176, 204, 0.10)\",\n        \"c-primary-light-300\": \"rgb(164,192,214)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(164, 192, 214, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(164, 192, 214, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(164, 192, 214, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(164, 192, 214, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(164, 192, 214, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(164, 192, 214, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(164, 192, 214, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(164, 192, 214, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(164, 192, 214, 0.10)\",\n        \"c-primary-light-400\": \"rgb(182,205,222)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(182, 205, 222, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(182, 205, 222, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(182, 205, 222, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(182, 205, 222, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(182, 205, 222, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(182, 205, 222, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(182, 205, 222, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(182, 205, 222, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(182, 205, 222, 0.10)\",\n        \"c-primary-light-500\": \"rgb(197,215,229)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(197, 215, 229, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(197, 215, 229, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(197, 215, 229, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(197, 215, 229, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(197, 215, 229, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(197, 215, 229, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(197, 215, 229, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(197, 215, 229, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(197, 215, 229, 0.10)\",\n        \"c-primary-light-600\": \"rgb(209,223,234)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(209, 223, 234, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(209, 223, 234, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(209, 223, 234, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(209, 223, 234, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(209, 223, 234, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(209, 223, 234, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(209, 223, 234, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(209, 223, 234, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(209, 223, 234, 0.10)\",\n        \"c-primary-light-700\": \"rgb(218,229,238)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(218, 229, 238, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(218, 229, 238, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(218, 229, 238, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(218, 229, 238, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(218, 229, 238, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(218, 229, 238, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(218, 229, 238, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(218, 229, 238, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(218, 229, 238, 0.10)\",\n        \"c-primary-light-800\": \"rgb(225,234,241)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(225, 234, 241, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(225, 234, 241, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(225, 234, 241, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(225, 234, 241, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(225, 234, 241, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(225, 234, 241, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(225, 234, 241, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(225, 234, 241, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(225, 234, 241, 0.10)\",\n        \"c-primary-light-900\": \"rgb(231,238,244)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(231, 238, 244, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(231, 238, 244, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(231, 238, 244, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(231, 238, 244, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(231, 238, 244, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(231, 238, 244, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(231, 238, 244, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(231, 238, 244, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(231, 238, 244, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(77, 131, 175)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-600)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"rgba(66.6, 150.7, 171, 1)\",\n        \"c-badge-tertiary\": \"rgba(54, 196, 231, 1)\"\n      }\n    }\n  },\n  {\n    \"id\": \"orange\",\n    \"name\": \"橙黄橘绿\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(245, 171, 53)\",\n        \"c-primary-dark-100\": \"rgb(221,154,48)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(221, 154, 48, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(245, 171, 53, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(221, 154, 48, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(245, 171, 53, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(221, 154, 48, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(245, 171, 53, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(221, 154, 48, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(245, 171, 53, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(221, 154, 48, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(245, 171, 53, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(221, 154, 48, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(245, 171, 53, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(221, 154, 48, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(245, 171, 53, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(221, 154, 48, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(245, 171, 53, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(221, 154, 48, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(245, 171, 53, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(199,139,43)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(199, 139, 43, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(199, 139, 43, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(199, 139, 43, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(199, 139, 43, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(199, 139, 43, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(199, 139, 43, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(199, 139, 43, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(199, 139, 43, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(199, 139, 43, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(179,125,39)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(179, 125, 39, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(179, 125, 39, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(179, 125, 39, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(179, 125, 39, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(179, 125, 39, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(179, 125, 39, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(179, 125, 39, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(179, 125, 39, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(179, 125, 39, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(161,113,35)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(161, 113, 35, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(161, 113, 35, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(161, 113, 35, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(161, 113, 35, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(161, 113, 35, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(161, 113, 35, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(161, 113, 35, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(161, 113, 35, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(161, 113, 35, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(145,102,32)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(145, 102, 32, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(145, 102, 32, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(145, 102, 32, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(145, 102, 32, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(145, 102, 32, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(145, 102, 32, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(145, 102, 32, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(145, 102, 32, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(145, 102, 32, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(131,92,29)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(131, 92, 29, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(131, 92, 29, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(131, 92, 29, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(131, 92, 29, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(131, 92, 29, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(131, 92, 29, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(131, 92, 29, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(131, 92, 29, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(131, 92, 29, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(118,83,26)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(118, 83, 26, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(118, 83, 26, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(118, 83, 26, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(118, 83, 26, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(118, 83, 26, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(118, 83, 26, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(118, 83, 26, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(118, 83, 26, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(118, 83, 26, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(106,75,23)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(106, 75, 23, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(106, 75, 23, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(106, 75, 23, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(106, 75, 23, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(106, 75, 23, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(106, 75, 23, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(106, 75, 23, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(106, 75, 23, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(106, 75, 23, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(95,68,21)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(95, 68, 21, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(95, 68, 21, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(95, 68, 21, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(95, 68, 21, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(95, 68, 21, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(95, 68, 21, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(95, 68, 21, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(95, 68, 21, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(95, 68, 21, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(86,61,19)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(86, 61, 19, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(86, 61, 19, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(86, 61, 19, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(86, 61, 19, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(86, 61, 19, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(86, 61, 19, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(86, 61, 19, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(86, 61, 19, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(86, 61, 19, 0.10)\",\n        \"c-primary-light-100\": \"rgb(247,188,93)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(247, 188, 93, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(247, 188, 93, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(247, 188, 93, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(247, 188, 93, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(247, 188, 93, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(247, 188, 93, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(247, 188, 93, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(247, 188, 93, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(247, 188, 93, 0.10)\",\n        \"c-primary-light-200\": \"rgb(249,201,125)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(249, 201, 125, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(249, 201, 125, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(249, 201, 125, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(249, 201, 125, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(249, 201, 125, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(249, 201, 125, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(249, 201, 125, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(249, 201, 125, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(249, 201, 125, 0.10)\",\n        \"c-primary-light-300\": \"rgb(250,212,151)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(250, 212, 151, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(250, 212, 151, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(250, 212, 151, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(250, 212, 151, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(250, 212, 151, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(250, 212, 151, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(250, 212, 151, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(250, 212, 151, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(250, 212, 151, 0.10)\",\n        \"c-primary-light-400\": \"rgb(251,221,172)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(251, 221, 172, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(251, 221, 172, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(251, 221, 172, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(251, 221, 172, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(251, 221, 172, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(251, 221, 172, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(251, 221, 172, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(251, 221, 172, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(251, 221, 172, 0.10)\",\n        \"c-primary-light-500\": \"rgb(252,228,189)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(252, 228, 189, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(252, 228, 189, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(252, 228, 189, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(252, 228, 189, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(252, 228, 189, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(252, 228, 189, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(252, 228, 189, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(252, 228, 189, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(252, 228, 189, 0.10)\",\n        \"c-primary-light-600\": \"rgb(253,233,202)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(253, 233, 202, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(253, 233, 202, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(253, 233, 202, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(253, 233, 202, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(253, 233, 202, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(253, 233, 202, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(253, 233, 202, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(253, 233, 202, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(253, 233, 202, 0.10)\",\n        \"c-primary-light-700\": \"rgb(253,237,213)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(253, 237, 213, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(253, 237, 213, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(253, 237, 213, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(253, 237, 213, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(253, 237, 213, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(253, 237, 213, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(253, 237, 213, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(253, 237, 213, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(253, 237, 213, 0.10)\",\n        \"c-primary-light-800\": \"rgb(253,241,221)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(253, 241, 221, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(253, 241, 221, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(253, 241, 221, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(253, 241, 221, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(253, 241, 221, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(253, 241, 221, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(253, 241, 221, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(253, 241, 221, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(253, 241, 221, 0.10)\",\n        \"c-primary-light-900\": \"rgb(253,244,228)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(253, 244, 228, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(253, 244, 228, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(253, 244, 228, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(253, 244, 228, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(253, 244, 228, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(253, 244, 228, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(253, 244, 228, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(253, 244, 228, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(253, 244, 228, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(245, 171, 53)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#9ed458\",\n        \"c-badge-tertiary\": \"#9ed458\"\n      }\n    }\n  },\n  {\n    \"id\": \"brown\",\n    \"name\": \"泥牛入海\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgba(188, 128, 68, 1)\",\n        \"c-primary-dark-100\": \"rgba(169,115,61, 1)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(169, 115, 61, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(188, 128, 68, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(169, 115, 61, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(188, 128, 68, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(169, 115, 61, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(188, 128, 68, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(169, 115, 61, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(188, 128, 68, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(169, 115, 61, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(188, 128, 68, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(169, 115, 61, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(188, 128, 68, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(169, 115, 61, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(188, 128, 68, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(169, 115, 61, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(188, 128, 68, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(169, 115, 61, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(188, 128, 68, 0.10)\",\n        \"c-primary-dark-200\": \"rgba(152,104,55, 1)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(152, 104, 55, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(152, 104, 55, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(152, 104, 55, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(152, 104, 55, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(152, 104, 55, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(152, 104, 55, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(152, 104, 55, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(152, 104, 55, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(152, 104, 55, 0.10)\",\n        \"c-primary-dark-300\": \"rgba(137,94,50, 1)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(137, 94, 50, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(137, 94, 50, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(137, 94, 50, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(137, 94, 50, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(137, 94, 50, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(137, 94, 50, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(137, 94, 50, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(137, 94, 50, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(137, 94, 50, 0.10)\",\n        \"c-primary-dark-400\": \"rgba(123,85,45, 1)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(123, 85, 45, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(123, 85, 45, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(123, 85, 45, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(123, 85, 45, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(123, 85, 45, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(123, 85, 45, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(123, 85, 45, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(123, 85, 45, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(123, 85, 45, 0.10)\",\n        \"c-primary-dark-500\": \"rgba(111,77,41, 1)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(111, 77, 41, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(111, 77, 41, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(111, 77, 41, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(111, 77, 41, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(111, 77, 41, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(111, 77, 41, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(111, 77, 41, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(111, 77, 41, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(111, 77, 41, 0.10)\",\n        \"c-primary-dark-600\": \"rgba(100,69,37, 1)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(100, 69, 37, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(100, 69, 37, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(100, 69, 37, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(100, 69, 37, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(100, 69, 37, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(100, 69, 37, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(100, 69, 37, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(100, 69, 37, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(100, 69, 37, 0.10)\",\n        \"c-primary-dark-700\": \"rgba(90,62,33, 1)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(90, 62, 33, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(90, 62, 33, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(90, 62, 33, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(90, 62, 33, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(90, 62, 33, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(90, 62, 33, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(90, 62, 33, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(90, 62, 33, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(90, 62, 33, 0.10)\",\n        \"c-primary-dark-800\": \"rgba(81,56,30, 1)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(81, 56, 30, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(81, 56, 30, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(81, 56, 30, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(81, 56, 30, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(81, 56, 30, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(81, 56, 30, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(81, 56, 30, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(81, 56, 30, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(81, 56, 30, 0.10)\",\n        \"c-primary-dark-900\": \"rgba(73,50,27, 1)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(73, 50, 27, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(73, 50, 27, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(73, 50, 27, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(73, 50, 27, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(73, 50, 27, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(73, 50, 27, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(73, 50, 27, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(73, 50, 27, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(73, 50, 27, 0.10)\",\n        \"c-primary-dark-1000\": \"rgba(66,45,24, 1)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(66, 45, 24, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(66, 45, 24, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(66, 45, 24, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(66, 45, 24, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(66, 45, 24, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(66, 45, 24, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(66, 45, 24, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(66, 45, 24, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(66, 45, 24, 0.10)\",\n        \"c-primary-light-100\": \"rgba(201,153,105, 1)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(201, 153, 105, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(201, 153, 105, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(201, 153, 105, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(201, 153, 105, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(201, 153, 105, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(201, 153, 105, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(201, 153, 105, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(201, 153, 105, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(201, 153, 105, 0.10)\",\n        \"c-primary-light-200\": \"rgba(212,173,135, 1)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(212, 173, 135, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(212, 173, 135, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(212, 173, 135, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(212, 173, 135, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(212, 173, 135, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(212, 173, 135, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(212, 173, 135, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(212, 173, 135, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(212, 173, 135, 0.10)\",\n        \"c-primary-light-300\": \"rgba(221,189,159, 1)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(221, 189, 159, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(221, 189, 159, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(221, 189, 159, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(221, 189, 159, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(221, 189, 159, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(221, 189, 159, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(221, 189, 159, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(221, 189, 159, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(221, 189, 159, 0.10)\",\n        \"c-primary-light-400\": \"rgba(228,202,178, 1)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(228, 202, 178, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(228, 202, 178, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(228, 202, 178, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(228, 202, 178, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(228, 202, 178, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(228, 202, 178, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(228, 202, 178, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(228, 202, 178, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(228, 202, 178, 0.10)\",\n        \"c-primary-light-500\": \"rgba(233,213,193, 1)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(233, 213, 193, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(233, 213, 193, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(233, 213, 193, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(233, 213, 193, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(233, 213, 193, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(233, 213, 193, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(233, 213, 193, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(233, 213, 193, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(233, 213, 193, 0.10)\",\n        \"c-primary-light-600\": \"rgba(237,221,205, 1)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(237, 221, 205, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(237, 221, 205, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(237, 221, 205, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(237, 221, 205, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(237, 221, 205, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(237, 221, 205, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(237, 221, 205, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(237, 221, 205, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(237, 221, 205, 0.10)\",\n        \"c-primary-light-700\": \"rgba(241,228,215, 1)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(241, 228, 215, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(241, 228, 215, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(241, 228, 215, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(241, 228, 215, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(241, 228, 215, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(241, 228, 215, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(241, 228, 215, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(241, 228, 215, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(241, 228, 215, 0.10)\",\n        \"c-primary-light-800\": \"rgba(244,233,223, 1)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(244, 233, 223, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(244, 233, 223, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(244, 233, 223, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(244, 233, 223, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(244, 233, 223, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(244, 233, 223, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(244, 233, 223, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(244, 233, 223, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(244, 233, 223, 0.10)\",\n        \"c-primary-light-900\": \"rgba(246,237,229, 1)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(246, 237, 229, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(246, 237, 229, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(246, 237, 229, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(246, 237, 229, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(246, 237, 229, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(246, 237, 229, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(246, 237, 229, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(246, 237, 229, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(246, 237, 229, 0.10)\",\n        \"c-primary-light-1000\": \"rgba(255,255,255, 1)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgba(188, 128, 68, 1)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#483472\",\n        \"c-badge-tertiary\": \"#647D39\"\n      }\n    }\n  },\n  {\n    \"id\": \"red\",\n    \"name\": \"热情似火\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(214, 69, 65)\",\n        \"c-primary-dark-100\": \"rgb(193,62,59)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(193, 62, 59, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(214, 69, 65, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(193, 62, 59, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(214, 69, 65, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(193, 62, 59, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(214, 69, 65, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(193, 62, 59, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(214, 69, 65, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(193, 62, 59, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(214, 69, 65, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(193, 62, 59, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(214, 69, 65, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(193, 62, 59, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(214, 69, 65, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(193, 62, 59, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(214, 69, 65, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(193, 62, 59, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(214, 69, 65, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(174,56,53)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(174, 56, 53, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(174, 56, 53, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(174, 56, 53, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(174, 56, 53, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(174, 56, 53, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(174, 56, 53, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(174, 56, 53, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(174, 56, 53, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(174, 56, 53, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(157,50,48)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(157, 50, 48, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(157, 50, 48, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(157, 50, 48, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(157, 50, 48, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(157, 50, 48, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(157, 50, 48, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(157, 50, 48, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(157, 50, 48, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(157, 50, 48, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(141,45,43)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(141, 45, 43, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(141, 45, 43, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(141, 45, 43, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(141, 45, 43, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(141, 45, 43, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(141, 45, 43, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(141, 45, 43, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(141, 45, 43, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(141, 45, 43, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(127,41,39)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(127, 41, 39, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(127, 41, 39, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(127, 41, 39, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(127, 41, 39, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(127, 41, 39, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(127, 41, 39, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(127, 41, 39, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(127, 41, 39, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(127, 41, 39, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(114,37,35)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(114, 37, 35, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(114, 37, 35, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(114, 37, 35, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(114, 37, 35, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(114, 37, 35, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(114, 37, 35, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(114, 37, 35, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(114, 37, 35, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(114, 37, 35, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(103,33,32)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(103, 33, 32, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(103, 33, 32, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(103, 33, 32, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(103, 33, 32, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(103, 33, 32, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(103, 33, 32, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(103, 33, 32, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(103, 33, 32, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(103, 33, 32, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(93,30,29)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(93, 30, 29, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(93, 30, 29, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(93, 30, 29, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(93, 30, 29, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(93, 30, 29, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(93, 30, 29, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(93, 30, 29, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(93, 30, 29, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(93, 30, 29, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(84,27,26)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(84, 27, 26, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(84, 27, 26, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(84, 27, 26, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(84, 27, 26, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(84, 27, 26, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(84, 27, 26, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(84, 27, 26, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(84, 27, 26, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(84, 27, 26, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(76,24,23)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(76, 24, 23, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(76, 24, 23, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(76, 24, 23, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(76, 24, 23, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(76, 24, 23, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(76, 24, 23, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(76, 24, 23, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(76, 24, 23, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(76, 24, 23, 0.10)\",\n        \"c-primary-light-100\": \"rgb(222,106,103)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(222, 106, 103, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(222, 106, 103, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(222, 106, 103, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(222, 106, 103, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(222, 106, 103, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(222, 106, 103, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(222, 106, 103, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(222, 106, 103, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(222, 106, 103, 0.10)\",\n        \"c-primary-light-200\": \"rgb(229,136,133)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(229, 136, 133, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(229, 136, 133, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(229, 136, 133, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(229, 136, 133, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(229, 136, 133, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(229, 136, 133, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(229, 136, 133, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(229, 136, 133, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(229, 136, 133, 0.10)\",\n        \"c-primary-light-300\": \"rgb(234,160,157)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(234, 160, 157, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(234, 160, 157, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(234, 160, 157, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(234, 160, 157, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(234, 160, 157, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(234, 160, 157, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(234, 160, 157, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(234, 160, 157, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(234, 160, 157, 0.10)\",\n        \"c-primary-light-400\": \"rgb(238,179,177)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(238, 179, 177, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(238, 179, 177, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(238, 179, 177, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(238, 179, 177, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(238, 179, 177, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(238, 179, 177, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(238, 179, 177, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(238, 179, 177, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(238, 179, 177, 0.10)\",\n        \"c-primary-light-500\": \"rgb(241,194,193)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(241, 194, 193, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(241, 194, 193, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(241, 194, 193, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(241, 194, 193, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(241, 194, 193, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(241, 194, 193, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(241, 194, 193, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(241, 194, 193, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(241, 194, 193, 0.10)\",\n        \"c-primary-light-600\": \"rgb(244,206,205)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(244, 206, 205, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(244, 206, 205, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(244, 206, 205, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(244, 206, 205, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(244, 206, 205, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(244, 206, 205, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(244, 206, 205, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(244, 206, 205, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(244, 206, 205, 0.10)\",\n        \"c-primary-light-700\": \"rgb(246,216,215)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(246, 216, 215, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(246, 216, 215, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(246, 216, 215, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(246, 216, 215, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(246, 216, 215, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(246, 216, 215, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(246, 216, 215, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(246, 216, 215, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(246, 216, 215, 0.10)\",\n        \"c-primary-light-800\": \"rgb(248,224,223)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(248, 224, 223, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(248, 224, 223, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(248, 224, 223, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(248, 224, 223, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(248, 224, 223, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(248, 224, 223, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(248, 224, 223, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(248, 224, 223, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(248, 224, 223, 0.10)\",\n        \"c-primary-light-900\": \"rgb(249,230,229)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(249, 230, 229, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(249, 230, 229, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(249, 230, 229, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(249, 230, 229, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(249, 230, 229, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(249, 230, 229, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(249, 230, 229, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(249, 230, 229, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(249, 230, 229, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(214, 69, 65)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#dfbb6b\",\n        \"c-badge-tertiary\": \"#dfbb6b\"\n      }\n    }\n  },\n  {\n    \"id\": \"pink\",\n    \"name\": \"粉装玉琢\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(241, 130, 141)\",\n        \"c-primary-dark-100\": \"rgb(217,117,127)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(217, 117, 127, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(241, 130, 141, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(217, 117, 127, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(241, 130, 141, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(217, 117, 127, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(241, 130, 141, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(217, 117, 127, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(241, 130, 141, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(217, 117, 127, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(241, 130, 141, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(217, 117, 127, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(241, 130, 141, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(217, 117, 127, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(241, 130, 141, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(217, 117, 127, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(241, 130, 141, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(217, 117, 127, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(241, 130, 141, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(195,105,114)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(195, 105, 114, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(195, 105, 114, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(195, 105, 114, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(195, 105, 114, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(195, 105, 114, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(195, 105, 114, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(195, 105, 114, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(195, 105, 114, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(195, 105, 114, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(176,95,103)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(176, 95, 103, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(176, 95, 103, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(176, 95, 103, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(176, 95, 103, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(176, 95, 103, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(176, 95, 103, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(176, 95, 103, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(176, 95, 103, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(176, 95, 103, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(158,86,93)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(158, 86, 93, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(158, 86, 93, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(158, 86, 93, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(158, 86, 93, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(158, 86, 93, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(158, 86, 93, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(158, 86, 93, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(158, 86, 93, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(158, 86, 93, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(142,77,84)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(142, 77, 84, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(142, 77, 84, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(142, 77, 84, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(142, 77, 84, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(142, 77, 84, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(142, 77, 84, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(142, 77, 84, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(142, 77, 84, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(142, 77, 84, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(128,69,76)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(128, 69, 76, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(128, 69, 76, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(128, 69, 76, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(128, 69, 76, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(128, 69, 76, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(128, 69, 76, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(128, 69, 76, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(128, 69, 76, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(128, 69, 76, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(115,62,68)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(115, 62, 68, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(115, 62, 68, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(115, 62, 68, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(115, 62, 68, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(115, 62, 68, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(115, 62, 68, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(115, 62, 68, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(115, 62, 68, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(115, 62, 68, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(104,56,61)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(104, 56, 61, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(104, 56, 61, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(104, 56, 61, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(104, 56, 61, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(104, 56, 61, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(104, 56, 61, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(104, 56, 61, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(104, 56, 61, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(104, 56, 61, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(94,50,55)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(94, 50, 55, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(94, 50, 55, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(94, 50, 55, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(94, 50, 55, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(94, 50, 55, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(94, 50, 55, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(94, 50, 55, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(94, 50, 55, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(94, 50, 55, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(85,45,50)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(85, 45, 50, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(85, 45, 50, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(85, 45, 50, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(85, 45, 50, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(85, 45, 50, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(85, 45, 50, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(85, 45, 50, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(85, 45, 50, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(85, 45, 50, 0.10)\",\n        \"c-primary-light-100\": \"rgb(244,155,164)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(244, 155, 164, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(244, 155, 164, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(244, 155, 164, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(244, 155, 164, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(244, 155, 164, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(244, 155, 164, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(244, 155, 164, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(244, 155, 164, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(244, 155, 164, 0.10)\",\n        \"c-primary-light-200\": \"rgb(246,175,182)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(246, 175, 182, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(246, 175, 182, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(246, 175, 182, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(246, 175, 182, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(246, 175, 182, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(246, 175, 182, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(246, 175, 182, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(246, 175, 182, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(246, 175, 182, 0.10)\",\n        \"c-primary-light-300\": \"rgb(248,191,197)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(248, 191, 197, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(248, 191, 197, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(248, 191, 197, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(248, 191, 197, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(248, 191, 197, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(248, 191, 197, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(248, 191, 197, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(248, 191, 197, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(248, 191, 197, 0.10)\",\n        \"c-primary-light-400\": \"rgb(249,204,209)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(249, 204, 209, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(249, 204, 209, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(249, 204, 209, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(249, 204, 209, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(249, 204, 209, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(249, 204, 209, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(249, 204, 209, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(249, 204, 209, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(249, 204, 209, 0.10)\",\n        \"c-primary-light-500\": \"rgb(250,214,218)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(250, 214, 218, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(250, 214, 218, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(250, 214, 218, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(250, 214, 218, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(250, 214, 218, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(250, 214, 218, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(250, 214, 218, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(250, 214, 218, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(250, 214, 218, 0.10)\",\n        \"c-primary-light-600\": \"rgb(251,222,225)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(251, 222, 225, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(251, 222, 225, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(251, 222, 225, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(251, 222, 225, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(251, 222, 225, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(251, 222, 225, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(251, 222, 225, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(251, 222, 225, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(251, 222, 225, 0.10)\",\n        \"c-primary-light-700\": \"rgb(252,229,231)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(252, 229, 231, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(252, 229, 231, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(252, 229, 231, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(252, 229, 231, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(252, 229, 231, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(252, 229, 231, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(252, 229, 231, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(252, 229, 231, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(252, 229, 231, 0.10)\",\n        \"c-primary-light-800\": \"rgb(253,234,236)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(253, 234, 236, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(253, 234, 236, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(253, 234, 236, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(253, 234, 236, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(253, 234, 236, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(253, 234, 236, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(253, 234, 236, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(253, 234, 236, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(253, 234, 236, 0.10)\",\n        \"c-primary-light-900\": \"rgb(253,238,240)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(253, 238, 240, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(253, 238, 240, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(253, 238, 240, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(253, 238, 240, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(253, 238, 240, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(253, 238, 240, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(253, 238, 240, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(253, 238, 240, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(253, 238, 240, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(241, 130, 141)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#f5b684\",\n        \"c-badge-tertiary\": \"#f5b684\"\n      }\n    }\n  },\n  {\n    \"id\": \"purple\",\n    \"name\": \"重斤球紫\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(155, 89, 182)\",\n        \"c-primary-dark-100\": \"rgb(140,80,164)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(140, 80, 164, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(155, 89, 182, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(140, 80, 164, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(155, 89, 182, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(140, 80, 164, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(155, 89, 182, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(140, 80, 164, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(155, 89, 182, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(140, 80, 164, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(155, 89, 182, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(140, 80, 164, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(155, 89, 182, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(140, 80, 164, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(155, 89, 182, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(140, 80, 164, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(155, 89, 182, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(140, 80, 164, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(155, 89, 182, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(126,72,148)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(126, 72, 148, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(126, 72, 148, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(126, 72, 148, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(126, 72, 148, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(126, 72, 148, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(126, 72, 148, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(126, 72, 148, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(126, 72, 148, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(126, 72, 148, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(113,65,133)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(113, 65, 133, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(113, 65, 133, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(113, 65, 133, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(113, 65, 133, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(113, 65, 133, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(113, 65, 133, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(113, 65, 133, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(113, 65, 133, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(113, 65, 133, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(102,59,120)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(102, 59, 120, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(102, 59, 120, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(102, 59, 120, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(102, 59, 120, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(102, 59, 120, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(102, 59, 120, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(102, 59, 120, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(102, 59, 120, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(102, 59, 120, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(92,53,108)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(92, 53, 108, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(92, 53, 108, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(92, 53, 108, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(92, 53, 108, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(92, 53, 108, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(92, 53, 108, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(92, 53, 108, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(92, 53, 108, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(92, 53, 108, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(83,48,97)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(83, 48, 97, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(83, 48, 97, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(83, 48, 97, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(83, 48, 97, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(83, 48, 97, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(83, 48, 97, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(83, 48, 97, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(83, 48, 97, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(83, 48, 97, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(75,43,87)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(75, 43, 87, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(75, 43, 87, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(75, 43, 87, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(75, 43, 87, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(75, 43, 87, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(75, 43, 87, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(75, 43, 87, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(75, 43, 87, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(75, 43, 87, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(68,39,78)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(68, 39, 78, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(68, 39, 78, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(68, 39, 78, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(68, 39, 78, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(68, 39, 78, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(68, 39, 78, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(68, 39, 78, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(68, 39, 78, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(68, 39, 78, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(61,35,70)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(61, 35, 70, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(61, 35, 70, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(61, 35, 70, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(61, 35, 70, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(61, 35, 70, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(61, 35, 70, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(61, 35, 70, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(61, 35, 70, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(61, 35, 70, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(55,32,63)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(55, 32, 63, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(55, 32, 63, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(55, 32, 63, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(55, 32, 63, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(55, 32, 63, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(55, 32, 63, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(55, 32, 63, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(55, 32, 63, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(55, 32, 63, 0.10)\",\n        \"c-primary-light-100\": \"rgb(175,122,197)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(175, 122, 197, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(175, 122, 197, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(175, 122, 197, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(175, 122, 197, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(175, 122, 197, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(175, 122, 197, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(175, 122, 197, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(175, 122, 197, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(175, 122, 197, 0.10)\",\n        \"c-primary-light-200\": \"rgb(191,149,209)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(191, 149, 209, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(191, 149, 209, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(191, 149, 209, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(191, 149, 209, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(191, 149, 209, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(191, 149, 209, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(191, 149, 209, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(191, 149, 209, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(191, 149, 209, 0.10)\",\n        \"c-primary-light-300\": \"rgb(204,170,218)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(204, 170, 218, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(204, 170, 218, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(204, 170, 218, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(204, 170, 218, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(204, 170, 218, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(204, 170, 218, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(204, 170, 218, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(204, 170, 218, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(204, 170, 218, 0.10)\",\n        \"c-primary-light-400\": \"rgb(214,187,225)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(214, 187, 225, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(214, 187, 225, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(214, 187, 225, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(214, 187, 225, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(214, 187, 225, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(214, 187, 225, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(214, 187, 225, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(214, 187, 225, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(214, 187, 225, 0.10)\",\n        \"c-primary-light-500\": \"rgb(222,201,231)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(222, 201, 231, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(222, 201, 231, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(222, 201, 231, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(222, 201, 231, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(222, 201, 231, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(222, 201, 231, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(222, 201, 231, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(222, 201, 231, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(222, 201, 231, 0.10)\",\n        \"c-primary-light-600\": \"rgb(229,212,236)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(229, 212, 236, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(229, 212, 236, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(229, 212, 236, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(229, 212, 236, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(229, 212, 236, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(229, 212, 236, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(229, 212, 236, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(229, 212, 236, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(229, 212, 236, 0.10)\",\n        \"c-primary-light-700\": \"rgb(234,221,240)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(234, 221, 240, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(234, 221, 240, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(234, 221, 240, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(234, 221, 240, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(234, 221, 240, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(234, 221, 240, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(234, 221, 240, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(234, 221, 240, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(234, 221, 240, 0.10)\",\n        \"c-primary-light-800\": \"rgb(238,228,243)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(238, 228, 243, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(238, 228, 243, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(238, 228, 243, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(238, 228, 243, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(238, 228, 243, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(238, 228, 243, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(238, 228, 243, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(238, 228, 243, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(238, 228, 243, 0.10)\",\n        \"c-primary-light-900\": \"rgb(241,233,245)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(241, 233, 245, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(241, 233, 245, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(241, 233, 245, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(241, 233, 245, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(241, 233, 245, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(241, 233, 245, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(241, 233, 245, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(241, 233, 245, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(241, 233, 245, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(155, 89, 182)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#e5a39f\",\n        \"c-badge-tertiary\": \"#e5a39f\"\n      }\n    }\n  },\n  {\n    \"id\": \"grey\",\n    \"name\": \"灰常美丽\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(108, 122, 137)\",\n        \"c-primary-dark-100\": \"rgb(97,110,123)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(97, 110, 123, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(108, 122, 137, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(97, 110, 123, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(108, 122, 137, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(97, 110, 123, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(108, 122, 137, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(97, 110, 123, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(108, 122, 137, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(97, 110, 123, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(108, 122, 137, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(97, 110, 123, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(108, 122, 137, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(97, 110, 123, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(108, 122, 137, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(97, 110, 123, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(108, 122, 137, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(97, 110, 123, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(108, 122, 137, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(87,99,111)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(87, 99, 111, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(87, 99, 111, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(87, 99, 111, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(87, 99, 111, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(87, 99, 111, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(87, 99, 111, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(87, 99, 111, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(87, 99, 111, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(87, 99, 111, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(78,89,100)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(78, 89, 100, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(78, 89, 100, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(78, 89, 100, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(78, 89, 100, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(78, 89, 100, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(78, 89, 100, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(78, 89, 100, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(78, 89, 100, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(78, 89, 100, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(70,80,90)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(70, 80, 90, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(70, 80, 90, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(70, 80, 90, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(70, 80, 90, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(70, 80, 90, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(70, 80, 90, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(70, 80, 90, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(70, 80, 90, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(70, 80, 90, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(63,72,81)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(63, 72, 81, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(63, 72, 81, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(63, 72, 81, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(63, 72, 81, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(63, 72, 81, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(63, 72, 81, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(63, 72, 81, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(63, 72, 81, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(63, 72, 81, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(57,65,73)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(57, 65, 73, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(57, 65, 73, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(57, 65, 73, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(57, 65, 73, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(57, 65, 73, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(57, 65, 73, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(57, 65, 73, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(57, 65, 73, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(57, 65, 73, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(51,59,66)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(51, 59, 66, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(51, 59, 66, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(51, 59, 66, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(51, 59, 66, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(51, 59, 66, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(51, 59, 66, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(51, 59, 66, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(51, 59, 66, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(51, 59, 66, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(46,53,59)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(46, 53, 59, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(46, 53, 59, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(46, 53, 59, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(46, 53, 59, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(46, 53, 59, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(46, 53, 59, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(46, 53, 59, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(46, 53, 59, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(46, 53, 59, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(41,48,53)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(41, 48, 53, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(41, 48, 53, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(41, 48, 53, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(41, 48, 53, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(41, 48, 53, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(41, 48, 53, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(41, 48, 53, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(41, 48, 53, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(41, 48, 53, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(37,43,48)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(37, 43, 48, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(37, 43, 48, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(37, 43, 48, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(37, 43, 48, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(37, 43, 48, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(37, 43, 48, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(37, 43, 48, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(37, 43, 48, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(37, 43, 48, 0.10)\",\n        \"c-primary-light-100\": \"rgb(137,149,161)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(137, 149, 161, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(137, 149, 161, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(137, 149, 161, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(137, 149, 161, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(137, 149, 161, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(137, 149, 161, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(137, 149, 161, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(137, 149, 161, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(137, 149, 161, 0.10)\",\n        \"c-primary-light-200\": \"rgb(161,170,180)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(161, 170, 180, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(161, 170, 180, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(161, 170, 180, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(161, 170, 180, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(161, 170, 180, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(161, 170, 180, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(161, 170, 180, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(161, 170, 180, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(161, 170, 180, 0.10)\",\n        \"c-primary-light-300\": \"rgb(180,187,195)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(180, 187, 195, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(180, 187, 195, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(180, 187, 195, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(180, 187, 195, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(180, 187, 195, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(180, 187, 195, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(180, 187, 195, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(180, 187, 195, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(180, 187, 195, 0.10)\",\n        \"c-primary-light-400\": \"rgb(195,201,207)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(195, 201, 207, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(195, 201, 207, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(195, 201, 207, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(195, 201, 207, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(195, 201, 207, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(195, 201, 207, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(195, 201, 207, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(195, 201, 207, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(195, 201, 207, 0.10)\",\n        \"c-primary-light-500\": \"rgb(207,212,217)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(207, 212, 217, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(207, 212, 217, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(207, 212, 217, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(207, 212, 217, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(207, 212, 217, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(207, 212, 217, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(207, 212, 217, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(207, 212, 217, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(207, 212, 217, 0.10)\",\n        \"c-primary-light-600\": \"rgb(217,221,225)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(217, 221, 225, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(217, 221, 225, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(217, 221, 225, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(217, 221, 225, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(217, 221, 225, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(217, 221, 225, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(217, 221, 225, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(217, 221, 225, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(217, 221, 225, 0.10)\",\n        \"c-primary-light-700\": \"rgb(225,228,231)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(225, 228, 231, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(225, 228, 231, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(225, 228, 231, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(225, 228, 231, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(225, 228, 231, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(225, 228, 231, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(225, 228, 231, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(225, 228, 231, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(225, 228, 231, 0.10)\",\n        \"c-primary-light-800\": \"rgb(231,233,236)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(231, 233, 236, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(231, 233, 236, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(231, 233, 236, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(231, 233, 236, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(231, 233, 236, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(231, 233, 236, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(231, 233, 236, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(231, 233, 236, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(231, 233, 236, 0.10)\",\n        \"c-primary-light-900\": \"rgb(236,237,240)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(236, 237, 240, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(236, 237, 240, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(236, 237, 240, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(236, 237, 240, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(236, 237, 240, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(236, 237, 240, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(236, 237, 240, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(236, 237, 240, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(236, 237, 240, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(108, 122, 137)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#b19b9f\",\n        \"c-badge-tertiary\": \"#b19b9f\"\n      }\n    }\n  },\n  {\n    \"id\": \"ming\",\n    \"name\": \"青出于黑\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(51, 110, 123)\",\n        \"c-primary-dark-100\": \"rgb(46,99,111)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(46, 99, 111, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(51, 110, 123, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(46, 99, 111, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(51, 110, 123, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(46, 99, 111, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(51, 110, 123, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(46, 99, 111, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(51, 110, 123, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(46, 99, 111, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(51, 110, 123, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(46, 99, 111, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(51, 110, 123, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(46, 99, 111, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(51, 110, 123, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(46, 99, 111, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(51, 110, 123, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(46, 99, 111, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(51, 110, 123, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(41,89,100)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(41, 89, 100, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(41, 89, 100, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(41, 89, 100, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(41, 89, 100, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(41, 89, 100, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(41, 89, 100, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(41, 89, 100, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(41, 89, 100, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(41, 89, 100, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(37,80,90)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(37, 80, 90, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(37, 80, 90, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(37, 80, 90, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(37, 80, 90, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(37, 80, 90, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(37, 80, 90, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(37, 80, 90, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(37, 80, 90, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(37, 80, 90, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(33,72,81)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(33, 72, 81, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(33, 72, 81, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(33, 72, 81, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(33, 72, 81, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(33, 72, 81, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(33, 72, 81, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(33, 72, 81, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(33, 72, 81, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(33, 72, 81, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(30,65,73)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(30, 65, 73, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(30, 65, 73, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(30, 65, 73, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(30, 65, 73, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(30, 65, 73, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(30, 65, 73, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(30, 65, 73, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(30, 65, 73, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(30, 65, 73, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(27,59,66)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(27, 59, 66, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(27, 59, 66, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(27, 59, 66, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(27, 59, 66, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(27, 59, 66, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(27, 59, 66, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(27, 59, 66, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(27, 59, 66, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(27, 59, 66, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(24,53,59)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(24, 53, 59, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(24, 53, 59, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(24, 53, 59, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(24, 53, 59, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(24, 53, 59, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(24, 53, 59, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(24, 53, 59, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(24, 53, 59, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(24, 53, 59, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(22,48,53)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(22, 48, 53, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(22, 48, 53, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(22, 48, 53, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(22, 48, 53, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(22, 48, 53, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(22, 48, 53, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(22, 48, 53, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(22, 48, 53, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(22, 48, 53, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(20,43,48)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(20, 43, 48, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(20, 43, 48, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(20, 43, 48, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(20, 43, 48, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(20, 43, 48, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(20, 43, 48, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(20, 43, 48, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(20, 43, 48, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(20, 43, 48, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(18,39,43)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(18, 39, 43, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(18, 39, 43, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(18, 39, 43, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(18, 39, 43, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(18, 39, 43, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(18, 39, 43, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(18, 39, 43, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(18, 39, 43, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(18, 39, 43, 0.10)\",\n        \"c-primary-light-100\": \"rgb(92,139,149)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(92, 139, 149, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(92, 139, 149, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(92, 139, 149, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(92, 139, 149, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(92, 139, 149, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(92, 139, 149, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(92, 139, 149, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(92, 139, 149, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(92, 139, 149, 0.10)\",\n        \"c-primary-light-200\": \"rgb(125,162,170)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(125, 162, 170, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(125, 162, 170, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(125, 162, 170, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(125, 162, 170, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(125, 162, 170, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(125, 162, 170, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(125, 162, 170, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(125, 162, 170, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(125, 162, 170, 0.10)\",\n        \"c-primary-light-300\": \"rgb(151,181,187)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(151, 181, 187, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(151, 181, 187, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(151, 181, 187, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(151, 181, 187, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(151, 181, 187, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(151, 181, 187, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(151, 181, 187, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(151, 181, 187, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(151, 181, 187, 0.10)\",\n        \"c-primary-light-400\": \"rgb(172,196,201)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(172, 196, 201, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(172, 196, 201, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(172, 196, 201, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(172, 196, 201, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(172, 196, 201, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(172, 196, 201, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(172, 196, 201, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(172, 196, 201, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(172, 196, 201, 0.10)\",\n        \"c-primary-light-500\": \"rgb(189,208,212)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(189, 208, 212, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(189, 208, 212, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(189, 208, 212, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(189, 208, 212, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(189, 208, 212, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(189, 208, 212, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(189, 208, 212, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(189, 208, 212, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(189, 208, 212, 0.10)\",\n        \"c-primary-light-600\": \"rgb(202,217,221)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(202, 217, 221, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(202, 217, 221, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(202, 217, 221, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(202, 217, 221, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(202, 217, 221, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(202, 217, 221, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(202, 217, 221, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(202, 217, 221, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(202, 217, 221, 0.10)\",\n        \"c-primary-light-700\": \"rgb(213,225,228)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(213, 225, 228, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(213, 225, 228, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(213, 225, 228, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(213, 225, 228, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(213, 225, 228, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(213, 225, 228, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(213, 225, 228, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(213, 225, 228, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(213, 225, 228, 0.10)\",\n        \"c-primary-light-800\": \"rgb(221,231,233)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(221, 231, 233, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(221, 231, 233, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(221, 231, 233, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(221, 231, 233, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(221, 231, 233, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(221, 231, 233, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(221, 231, 233, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(221, 231, 233, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(221, 231, 233, 0.10)\",\n        \"c-primary-light-900\": \"rgb(228,236,237)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(228, 236, 237, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(228, 236, 237, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(228, 236, 237, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(228, 236, 237, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(228, 236, 237, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(228, 236, 237, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(228, 236, 237, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(228, 236, 237, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(228, 236, 237, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(51, 110, 123)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#6376a2\",\n        \"c-badge-tertiary\": \"#6376a2\"\n      }\n    }\n  },\n  {\n    \"id\": \"blue2\",\n    \"name\": \"清热板蓝\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(79, 98, 208)\",\n        \"c-primary-dark-100\": \"rgb(71,88,187)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(71, 88, 187, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(79, 98, 208, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(71, 88, 187, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(79, 98, 208, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(71, 88, 187, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(79, 98, 208, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(71, 88, 187, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(79, 98, 208, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(71, 88, 187, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(79, 98, 208, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(71, 88, 187, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(79, 98, 208, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(71, 88, 187, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(79, 98, 208, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(71, 88, 187, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(79, 98, 208, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(71, 88, 187, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(79, 98, 208, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(64,79,168)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(64, 79, 168, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(64, 79, 168, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(64, 79, 168, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(64, 79, 168, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(64, 79, 168, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(64, 79, 168, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(64, 79, 168, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(64, 79, 168, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(64, 79, 168, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(58,71,151)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(58, 71, 151, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(58, 71, 151, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(58, 71, 151, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(58, 71, 151, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(58, 71, 151, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(58, 71, 151, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(58, 71, 151, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(58, 71, 151, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(58, 71, 151, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(52,64,136)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(52, 64, 136, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(52, 64, 136, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(52, 64, 136, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(52, 64, 136, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(52, 64, 136, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(52, 64, 136, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(52, 64, 136, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(52, 64, 136, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(52, 64, 136, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(47,58,122)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(47, 58, 122, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(47, 58, 122, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(47, 58, 122, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(47, 58, 122, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(47, 58, 122, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(47, 58, 122, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(47, 58, 122, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(47, 58, 122, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(47, 58, 122, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(42,52,110)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(42, 52, 110, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(42, 52, 110, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(42, 52, 110, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(42, 52, 110, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(42, 52, 110, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(42, 52, 110, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(42, 52, 110, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(42, 52, 110, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(42, 52, 110, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(38,47,99)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(38, 47, 99, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(38, 47, 99, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(38, 47, 99, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(38, 47, 99, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(38, 47, 99, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(38, 47, 99, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(38, 47, 99, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(38, 47, 99, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(38, 47, 99, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(34,42,89)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(34, 42, 89, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(34, 42, 89, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(34, 42, 89, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(34, 42, 89, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(34, 42, 89, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(34, 42, 89, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(34, 42, 89, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(34, 42, 89, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(34, 42, 89, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(31,38,80)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(31, 38, 80, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(31, 38, 80, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(31, 38, 80, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(31, 38, 80, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(31, 38, 80, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(31, 38, 80, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(31, 38, 80, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(31, 38, 80, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(31, 38, 80, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(28,34,72)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(28, 34, 72, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(28, 34, 72, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(28, 34, 72, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(28, 34, 72, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(28, 34, 72, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(28, 34, 72, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(28, 34, 72, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(28, 34, 72, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(28, 34, 72, 0.10)\",\n        \"c-primary-light-100\": \"rgb(114,129,217)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(114, 129, 217, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(114, 129, 217, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(114, 129, 217, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(114, 129, 217, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(114, 129, 217, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(114, 129, 217, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(114, 129, 217, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(114, 129, 217, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(114, 129, 217, 0.10)\",\n        \"c-primary-light-200\": \"rgb(142,154,225)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(142, 154, 225, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(142, 154, 225, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(142, 154, 225, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(142, 154, 225, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(142, 154, 225, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(142, 154, 225, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(142, 154, 225, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(142, 154, 225, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(142, 154, 225, 0.10)\",\n        \"c-primary-light-300\": \"rgb(165,174,231)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(165, 174, 231, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(165, 174, 231, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(165, 174, 231, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(165, 174, 231, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(165, 174, 231, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(165, 174, 231, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(165, 174, 231, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(165, 174, 231, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(165, 174, 231, 0.10)\",\n        \"c-primary-light-400\": \"rgb(183,190,236)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(183, 190, 236, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(183, 190, 236, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(183, 190, 236, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(183, 190, 236, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(183, 190, 236, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(183, 190, 236, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(183, 190, 236, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(183, 190, 236, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(183, 190, 236, 0.10)\",\n        \"c-primary-light-500\": \"rgb(197,203,240)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(197, 203, 240, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(197, 203, 240, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(197, 203, 240, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(197, 203, 240, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(197, 203, 240, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(197, 203, 240, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(197, 203, 240, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(197, 203, 240, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(197, 203, 240, 0.10)\",\n        \"c-primary-light-600\": \"rgb(209,213,243)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(209, 213, 243, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(209, 213, 243, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(209, 213, 243, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(209, 213, 243, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(209, 213, 243, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(209, 213, 243, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(209, 213, 243, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(209, 213, 243, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(209, 213, 243, 0.10)\",\n        \"c-primary-light-700\": \"rgb(218,221,245)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(218, 221, 245, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(218, 221, 245, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(218, 221, 245, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(218, 221, 245, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(218, 221, 245, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(218, 221, 245, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(218, 221, 245, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(218, 221, 245, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(218, 221, 245, 0.10)\",\n        \"c-primary-light-800\": \"rgb(225,228,247)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(225, 228, 247, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(225, 228, 247, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(225, 228, 247, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(225, 228, 247, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(225, 228, 247, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(225, 228, 247, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(225, 228, 247, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(225, 228, 247, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(225, 228, 247, 0.10)\",\n        \"c-primary-light-900\": \"rgb(231,233,249)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(231, 233, 249, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(231, 233, 249, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(231, 233, 249, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(231, 233, 249, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(231, 233, 249, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(231, 233, 249, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(231, 233, 249, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(231, 233, 249, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(231, 233, 249, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(79, 98, 208)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"var(c-primary-light-600-alpha-700)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 1)\",\n        \"bg-image\": \"\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#b080db\",\n        \"c-badge-tertiary\": \"#b080db\"\n      }\n    }\n  },\n  {\n    \"id\": \"black\",\n    \"name\": \"黑灯瞎火\",\n    \"isDark\": true,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(190, 190, 190)\",\n        \"c-primary-dark-100\": \"rgb(203,203,203)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(203, 203, 203, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(190, 190, 190, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(203, 203, 203, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(190, 190, 190, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(203, 203, 203, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(190, 190, 190, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(203, 203, 203, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(190, 190, 190, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(203, 203, 203, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(190, 190, 190, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(203, 203, 203, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(190, 190, 190, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(203, 203, 203, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(190, 190, 190, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(203, 203, 203, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(190, 190, 190, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(203, 203, 203, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(190, 190, 190, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(213,213,213)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(213, 213, 213, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(213, 213, 213, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(213, 213, 213, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(213, 213, 213, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(213, 213, 213, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(213, 213, 213, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(213, 213, 213, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(213, 213, 213, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(213, 213, 213, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(221,221,221)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(221, 221, 221, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(221, 221, 221, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(221, 221, 221, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(221, 221, 221, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(221, 221, 221, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(221, 221, 221, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(221, 221, 221, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(221, 221, 221, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(221, 221, 221, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(228,228,228)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(228, 228, 228, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(228, 228, 228, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(228, 228, 228, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(228, 228, 228, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(228, 228, 228, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(228, 228, 228, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(228, 228, 228, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(228, 228, 228, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(228, 228, 228, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(233,233,233)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(233, 233, 233, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(233, 233, 233, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(233, 233, 233, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(233, 233, 233, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(233, 233, 233, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(233, 233, 233, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(233, 233, 233, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(233, 233, 233, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(233, 233, 233, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(237,237,237)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(237, 237, 237, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(237, 237, 237, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(237, 237, 237, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(237, 237, 237, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(237, 237, 237, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(237, 237, 237, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(237, 237, 237, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(237, 237, 237, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(237, 237, 237, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(241,241,241)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(241, 241, 241, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(241, 241, 241, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(241, 241, 241, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(241, 241, 241, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(241, 241, 241, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(241, 241, 241, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(241, 241, 241, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(241, 241, 241, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(241, 241, 241, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(244,244,244)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(244, 244, 244, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(244, 244, 244, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(244, 244, 244, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(244, 244, 244, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(244, 244, 244, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(244, 244, 244, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(244, 244, 244, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(244, 244, 244, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(244, 244, 244, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(246,246,246)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(246, 246, 246, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(246, 246, 246, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(246, 246, 246, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(246, 246, 246, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(246, 246, 246, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(246, 246, 246, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(246, 246, 246, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(246, 246, 246, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(246, 246, 246, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(248,248,248)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(248, 248, 248, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(248, 248, 248, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(248, 248, 248, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(248, 248, 248, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(248, 248, 248, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(248, 248, 248, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(248, 248, 248, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(248, 248, 248, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(248, 248, 248, 0.10)\",\n        \"c-primary-light-100\": \"rgb(171,171,171)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(171, 171, 171, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(171, 171, 171, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(171, 171, 171, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(171, 171, 171, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(171, 171, 171, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(171, 171, 171, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(171, 171, 171, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(171, 171, 171, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(171, 171, 171, 0.10)\",\n        \"c-primary-light-200\": \"rgb(154,154,154)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(154, 154, 154, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(154, 154, 154, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(154, 154, 154, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(154, 154, 154, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(154, 154, 154, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(154, 154, 154, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(154, 154, 154, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(154, 154, 154, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(154, 154, 154, 0.10)\",\n        \"c-primary-light-300\": \"rgb(139,139,139)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(139, 139, 139, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(139, 139, 139, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(139, 139, 139, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(139, 139, 139, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(139, 139, 139, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(139, 139, 139, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(139, 139, 139, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(139, 139, 139, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(139, 139, 139, 0.10)\",\n        \"c-primary-light-400\": \"rgb(125,125,125)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(125, 125, 125, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(125, 125, 125, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(125, 125, 125, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(125, 125, 125, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(125, 125, 125, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(125, 125, 125, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(125, 125, 125, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(125, 125, 125, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(125, 125, 125, 0.10)\",\n        \"c-primary-light-500\": \"rgb(113,113,113)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(113, 113, 113, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(113, 113, 113, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(113, 113, 113, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(113, 113, 113, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(113, 113, 113, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(113, 113, 113, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(113, 113, 113, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(113, 113, 113, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(113, 113, 113, 0.10)\",\n        \"c-primary-light-600\": \"rgb(102,102,102)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(102, 102, 102, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(102, 102, 102, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(102, 102, 102, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(102, 102, 102, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(102, 102, 102, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(102, 102, 102, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(102, 102, 102, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(102, 102, 102, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(102, 102, 102, 0.10)\",\n        \"c-primary-light-700\": \"rgb(92,92,92)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(92, 92, 92, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(92, 92, 92, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(92, 92, 92, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(92, 92, 92, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(92, 92, 92, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(92, 92, 92, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(92, 92, 92, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(92, 92, 92, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(92, 92, 92, 0.10)\",\n        \"c-primary-light-800\": \"rgb(83,83,83)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(83, 83, 83, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(83, 83, 83, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(83, 83, 83, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(83, 83, 83, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(83, 83, 83, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(83, 83, 83, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(83, 83, 83, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(83, 83, 83, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(83, 83, 83, 0.10)\",\n        \"c-primary-light-900\": \"rgb(75,75,75)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(75, 75, 75, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(75, 75, 75, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(75, 75, 75, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(75, 75, 75, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(75, 75, 75, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(75, 75, 75, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(75, 75, 75, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(75, 75, 75, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(75, 75, 75, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(49,49,49)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(49, 49, 49, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(49, 49, 49, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(49, 49, 49, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(49, 49, 49, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(49, 49, 49, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(49, 49, 49, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(49, 49, 49, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(49, 49, 49, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(49, 49, 49, 0.10)\",\n        \"c-theme\": \"rgb(75,75,75)\",\n        \"c-1000\": \"rgb(255, 255, 255)\",\n        \"c-950\": \"rgb(242,242,242)\",\n        \"c-900\": \"rgb(230,230,230)\",\n        \"c-850\": \"rgb(219,219,219)\",\n        \"c-800\": \"rgb(208,208,208)\",\n        \"c-750\": \"rgb(198,198,198)\",\n        \"c-700\": \"rgb(188,188,188)\",\n        \"c-650\": \"rgb(179,179,179)\",\n        \"c-600\": \"rgb(170,170,170)\",\n        \"c-550\": \"rgb(162,162,162)\",\n        \"c-500\": \"rgb(154,154,154)\",\n        \"c-450\": \"rgb(146,146,146)\",\n        \"c-400\": \"rgb(139,139,139)\",\n        \"c-350\": \"rgb(132,132,132)\",\n        \"c-300\": \"rgb(125,125,125)\",\n        \"c-250\": \"rgb(119,119,119)\",\n        \"c-200\": \"rgb(113,113,113)\",\n        \"c-150\": \"rgb(107,107,107)\",\n        \"c-100\": \"rgb(102,102,102)\",\n        \"c-050\": \"rgb(97,97,97)\",\n        \"c-000\": \"rgb(92,92,92)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"rgba(0, 0, 0, 0)\",\n        \"c-main-background\": \"rgba(19, 19, 19, 0.95)\",\n        \"bg-image\": \"landingMoon.png\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary-dark-200)\",\n        \"c-badge-secondary\": \"var(c-primary)\",\n        \"c-badge-tertiary\": \"var(c-primary-dark-300)\"\n      }\n    }\n  },\n  {\n    \"id\": \"mid_autumn\",\n    \"name\": \"月里嫦娥\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(74, 55, 82)\",\n        \"c-primary-dark-100\": \"rgb(67,50,74)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(67, 50, 74, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(74, 55, 82, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(67, 50, 74, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(74, 55, 82, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(67, 50, 74, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(74, 55, 82, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(67, 50, 74, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(74, 55, 82, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(67, 50, 74, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(74, 55, 82, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(67, 50, 74, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(74, 55, 82, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(67, 50, 74, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(74, 55, 82, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(67, 50, 74, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(74, 55, 82, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(67, 50, 74, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(74, 55, 82, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(60,45,67)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(60, 45, 67, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(60, 45, 67, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(60, 45, 67, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(60, 45, 67, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(60, 45, 67, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(60, 45, 67, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(60, 45, 67, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(60, 45, 67, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(60, 45, 67, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(54,41,60)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(54, 41, 60, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(54, 41, 60, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(54, 41, 60, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(54, 41, 60, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(54, 41, 60, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(54, 41, 60, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(54, 41, 60, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(54, 41, 60, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(54, 41, 60, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(49,37,54)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(49, 37, 54, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(49, 37, 54, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(49, 37, 54, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(49, 37, 54, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(49, 37, 54, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(49, 37, 54, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(49, 37, 54, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(49, 37, 54, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(49, 37, 54, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(44,33,49)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(44, 33, 49, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(44, 33, 49, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(44, 33, 49, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(44, 33, 49, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(44, 33, 49, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(44, 33, 49, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(44, 33, 49, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(44, 33, 49, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(44, 33, 49, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(40,30,44)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(40, 30, 44, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(40, 30, 44, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(40, 30, 44, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(40, 30, 44, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(40, 30, 44, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(40, 30, 44, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(40, 30, 44, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(40, 30, 44, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(40, 30, 44, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(36,27,40)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(36, 27, 40, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(36, 27, 40, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(36, 27, 40, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(36, 27, 40, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(36, 27, 40, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(36, 27, 40, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(36, 27, 40, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(36, 27, 40, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(36, 27, 40, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(32,24,36)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(32, 24, 36, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(32, 24, 36, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(32, 24, 36, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(32, 24, 36, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(32, 24, 36, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(32, 24, 36, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(32, 24, 36, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(32, 24, 36, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(32, 24, 36, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(29,22,32)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(29, 22, 32, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(29, 22, 32, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(29, 22, 32, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(29, 22, 32, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(29, 22, 32, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(29, 22, 32, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(29, 22, 32, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(29, 22, 32, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(29, 22, 32, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(26,20,29)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(26, 20, 29, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(26, 20, 29, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(26, 20, 29, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(26, 20, 29, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(26, 20, 29, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(26, 20, 29, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(26, 20, 29, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(26, 20, 29, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(26, 20, 29, 0.10)\",\n        \"c-primary-light-100\": \"rgb(110,95,117)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(110, 95, 117, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(110, 95, 117, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(110, 95, 117, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(110, 95, 117, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(110, 95, 117, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(110, 95, 117, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(110, 95, 117, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(110, 95, 117, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(110, 95, 117, 0.10)\",\n        \"c-primary-light-200\": \"rgb(139,127,145)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(139, 127, 145, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(139, 127, 145, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(139, 127, 145, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(139, 127, 145, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(139, 127, 145, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(139, 127, 145, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(139, 127, 145, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(139, 127, 145, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(139, 127, 145, 0.10)\",\n        \"c-primary-light-300\": \"rgb(162,153,167)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(162, 153, 167, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(162, 153, 167, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(162, 153, 167, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(162, 153, 167, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(162, 153, 167, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(162, 153, 167, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(162, 153, 167, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(162, 153, 167, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(162, 153, 167, 0.10)\",\n        \"c-primary-light-400\": \"rgb(181,173,185)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(181, 173, 185, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(181, 173, 185, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(181, 173, 185, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(181, 173, 185, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(181, 173, 185, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(181, 173, 185, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(181, 173, 185, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(181, 173, 185, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(181, 173, 185, 0.10)\",\n        \"c-primary-light-500\": \"rgb(196,189,199)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(196, 189, 199, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(196, 189, 199, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(196, 189, 199, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(196, 189, 199, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(196, 189, 199, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(196, 189, 199, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(196, 189, 199, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(196, 189, 199, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(196, 189, 199, 0.10)\",\n        \"c-primary-light-600\": \"rgb(208,202,210)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(208, 202, 210, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(208, 202, 210, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(208, 202, 210, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(208, 202, 210, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(208, 202, 210, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(208, 202, 210, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(208, 202, 210, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(208, 202, 210, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(208, 202, 210, 0.10)\",\n        \"c-primary-light-700\": \"rgb(217,213,219)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(217, 213, 219, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(217, 213, 219, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(217, 213, 219, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(217, 213, 219, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(217, 213, 219, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(217, 213, 219, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(217, 213, 219, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(217, 213, 219, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(217, 213, 219, 0.10)\",\n        \"c-primary-light-800\": \"rgb(225,221,226)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(225, 221, 226, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(225, 221, 226, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(225, 221, 226, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(225, 221, 226, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(225, 221, 226, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(225, 221, 226, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(225, 221, 226, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(225, 221, 226, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(225, 221, 226, 0.10)\",\n        \"c-primary-light-900\": \"rgb(231,228,232)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(231, 228, 232, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(231, 228, 232, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(231, 228, 232, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(231, 228, 232, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(231, 228, 232, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(231, 228, 232, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(231, 228, 232, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(231, 228, 232, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(231, 228, 232, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(74, 55, 82)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"rgba(255, 255, 255, 0)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 0.9)\",\n        \"bg-image\": \"jqbg.jpg\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"#af9479\",\n        \"c-badge-tertiary\": \"#af9479\"\n      }\n    }\n  },\n  {\n    \"id\": \"naruto\",\n    \"name\": \"木叶之村\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(87, 144, 167)\",\n        \"c-primary-dark-100\": \"rgb(78,130,150)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(78, 130, 150, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(87, 144, 167, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(78, 130, 150, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(87, 144, 167, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(78, 130, 150, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(87, 144, 167, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(78, 130, 150, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(87, 144, 167, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(78, 130, 150, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(87, 144, 167, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(78, 130, 150, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(87, 144, 167, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(78, 130, 150, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(87, 144, 167, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(78, 130, 150, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(87, 144, 167, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(78, 130, 150, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(87, 144, 167, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(70,117,135)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(70, 117, 135, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(70, 117, 135, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(70, 117, 135, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(70, 117, 135, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(70, 117, 135, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(70, 117, 135, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(70, 117, 135, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(70, 117, 135, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(70, 117, 135, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(63,105,122)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(63, 105, 122, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(63, 105, 122, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(63, 105, 122, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(63, 105, 122, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(63, 105, 122, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(63, 105, 122, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(63, 105, 122, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(63, 105, 122, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(63, 105, 122, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(57,95,110)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(57, 95, 110, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(57, 95, 110, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(57, 95, 110, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(57, 95, 110, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(57, 95, 110, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(57, 95, 110, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(57, 95, 110, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(57, 95, 110, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(57, 95, 110, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(51,86,99)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(51, 86, 99, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(51, 86, 99, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(51, 86, 99, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(51, 86, 99, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(51, 86, 99, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(51, 86, 99, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(51, 86, 99, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(51, 86, 99, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(51, 86, 99, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(46,77,89)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(46, 77, 89, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(46, 77, 89, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(46, 77, 89, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(46, 77, 89, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(46, 77, 89, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(46, 77, 89, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(46, 77, 89, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(46, 77, 89, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(46, 77, 89, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(41,69,80)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(41, 69, 80, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(41, 69, 80, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(41, 69, 80, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(41, 69, 80, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(41, 69, 80, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(41, 69, 80, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(41, 69, 80, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(41, 69, 80, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(41, 69, 80, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(37,62,72)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(37, 62, 72, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(37, 62, 72, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(37, 62, 72, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(37, 62, 72, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(37, 62, 72, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(37, 62, 72, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(37, 62, 72, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(37, 62, 72, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(37, 62, 72, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(33,56,65)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(33, 56, 65, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(33, 56, 65, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(33, 56, 65, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(33, 56, 65, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(33, 56, 65, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(33, 56, 65, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(33, 56, 65, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(33, 56, 65, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(33, 56, 65, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(30,50,59)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(30, 50, 59, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(30, 50, 59, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(30, 50, 59, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(30, 50, 59, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(30, 50, 59, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(30, 50, 59, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(30, 50, 59, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(30, 50, 59, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(30, 50, 59, 0.10)\",\n        \"c-primary-light-100\": \"rgb(121,166,185)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(121, 166, 185, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(121, 166, 185, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(121, 166, 185, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(121, 166, 185, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(121, 166, 185, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(121, 166, 185, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(121, 166, 185, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(121, 166, 185, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(121, 166, 185, 0.10)\",\n        \"c-primary-light-200\": \"rgb(148,184,199)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(148, 184, 199, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(148, 184, 199, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(148, 184, 199, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(148, 184, 199, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(148, 184, 199, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(148, 184, 199, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(148, 184, 199, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(148, 184, 199, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(148, 184, 199, 0.10)\",\n        \"c-primary-light-300\": \"rgb(169,198,210)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(169, 198, 210, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(169, 198, 210, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(169, 198, 210, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(169, 198, 210, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(169, 198, 210, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(169, 198, 210, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(169, 198, 210, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(169, 198, 210, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(169, 198, 210, 0.10)\",\n        \"c-primary-light-400\": \"rgb(186,209,219)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(186, 209, 219, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(186, 209, 219, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(186, 209, 219, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(186, 209, 219, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(186, 209, 219, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(186, 209, 219, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(186, 209, 219, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(186, 209, 219, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(186, 209, 219, 0.10)\",\n        \"c-primary-light-500\": \"rgb(200,218,226)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(200, 218, 226, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(200, 218, 226, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(200, 218, 226, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(200, 218, 226, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(200, 218, 226, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(200, 218, 226, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(200, 218, 226, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(200, 218, 226, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(200, 218, 226, 0.10)\",\n        \"c-primary-light-600\": \"rgb(211,225,232)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(211, 225, 232, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(211, 225, 232, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(211, 225, 232, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(211, 225, 232, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(211, 225, 232, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(211, 225, 232, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(211, 225, 232, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(211, 225, 232, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(211, 225, 232, 0.10)\",\n        \"c-primary-light-700\": \"rgb(220,231,237)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(220, 231, 237, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(220, 231, 237, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(220, 231, 237, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(220, 231, 237, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(220, 231, 237, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(220, 231, 237, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(220, 231, 237, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(220, 231, 237, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(220, 231, 237, 0.10)\",\n        \"c-primary-light-800\": \"rgb(227,236,241)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(227, 236, 241, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(227, 236, 241, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(227, 236, 241, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(227, 236, 241, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(227, 236, 241, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(227, 236, 241, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(227, 236, 241, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(227, 236, 241, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(227, 236, 241, 0.10)\",\n        \"c-primary-light-900\": \"rgb(233,240,244)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(233, 240, 244, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(233, 240, 244, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(233, 240, 244, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(233, 240, 244, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(233, 240, 244, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(233, 240, 244, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(233, 240, 244, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(233, 240, 244, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(233, 240, 244, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(87, 144, 167)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"rgba(255, 255, 255, 0.15)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"bg-image\": \"myzcbg.jpg\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"var(c-primary)\",\n        \"c-badge-secondary\": \"var(c-primary-light-100)\",\n        \"c-badge-tertiary\": \"var(c-primary-light-100)\"\n      }\n    }\n  },\n  {\n    \"id\": \"china_ink\",\n    \"name\": \"近墨者黑\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgba(47, 47, 47, 1)\",\n        \"c-primary-dark-100\": \"rgba(42,42,42, 1)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(42, 42, 42, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(47, 47, 47, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(42, 42, 42, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(47, 47, 47, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(42, 42, 42, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(47, 47, 47, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(42, 42, 42, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(47, 47, 47, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(42, 42, 42, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(47, 47, 47, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(42, 42, 42, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(47, 47, 47, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(42, 42, 42, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(47, 47, 47, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(42, 42, 42, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(47, 47, 47, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(42, 42, 42, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(47, 47, 47, 0.10)\",\n        \"c-primary-dark-200\": \"rgba(38,38,38, 1)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(38, 38, 38, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(38, 38, 38, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(38, 38, 38, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(38, 38, 38, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(38, 38, 38, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(38, 38, 38, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(38, 38, 38, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(38, 38, 38, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(38, 38, 38, 0.10)\",\n        \"c-primary-dark-300\": \"rgba(34,34,34, 1)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(34, 34, 34, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(34, 34, 34, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(34, 34, 34, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(34, 34, 34, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(34, 34, 34, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(34, 34, 34, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(34, 34, 34, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(34, 34, 34, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(34, 34, 34, 0.10)\",\n        \"c-primary-dark-400\": \"rgba(31,31,31, 1)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(31, 31, 31, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(31, 31, 31, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(31, 31, 31, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(31, 31, 31, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(31, 31, 31, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(31, 31, 31, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(31, 31, 31, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(31, 31, 31, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(31, 31, 31, 0.10)\",\n        \"c-primary-dark-500\": \"rgba(28,28,28, 1)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(28, 28, 28, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(28, 28, 28, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(28, 28, 28, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(28, 28, 28, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(28, 28, 28, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(28, 28, 28, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(28, 28, 28, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(28, 28, 28, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(28, 28, 28, 0.10)\",\n        \"c-primary-dark-600\": \"rgba(25,25,25, 1)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(25, 25, 25, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(25, 25, 25, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(25, 25, 25, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(25, 25, 25, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(25, 25, 25, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(25, 25, 25, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(25, 25, 25, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(25, 25, 25, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(25, 25, 25, 0.10)\",\n        \"c-primary-dark-700\": \"rgba(23,23,23, 1)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(23, 23, 23, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(23, 23, 23, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(23, 23, 23, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(23, 23, 23, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(23, 23, 23, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(23, 23, 23, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(23, 23, 23, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(23, 23, 23, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(23, 23, 23, 0.10)\",\n        \"c-primary-dark-800\": \"rgba(21,21,21, 1)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(21, 21, 21, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(21, 21, 21, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(21, 21, 21, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(21, 21, 21, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(21, 21, 21, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(21, 21, 21, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(21, 21, 21, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(21, 21, 21, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(21, 21, 21, 0.10)\",\n        \"c-primary-dark-900\": \"rgba(19,19,19, 1)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(19, 19, 19, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(19, 19, 19, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(19, 19, 19, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(19, 19, 19, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(19, 19, 19, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(19, 19, 19, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(19, 19, 19, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(19, 19, 19, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(19, 19, 19, 0.10)\",\n        \"c-primary-dark-1000\": \"rgba(17,17,17, 1)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(17, 17, 17, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(17, 17, 17, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(17, 17, 17, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(17, 17, 17, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(17, 17, 17, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(17, 17, 17, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(17, 17, 17, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(17, 17, 17, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(17, 17, 17, 0.10)\",\n        \"c-primary-light-100\": \"rgba(89,89,89, 1)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(89, 89, 89, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(89, 89, 89, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(89, 89, 89, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(89, 89, 89, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(89, 89, 89, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(89, 89, 89, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(89, 89, 89, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(89, 89, 89, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(89, 89, 89, 0.10)\",\n        \"c-primary-light-200\": \"rgba(122,122,122, 1)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(122, 122, 122, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(122, 122, 122, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(122, 122, 122, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(122, 122, 122, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(122, 122, 122, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(122, 122, 122, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(122, 122, 122, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(122, 122, 122, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(122, 122, 122, 0.10)\",\n        \"c-primary-light-300\": \"rgba(149,149,149, 1)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(149, 149, 149, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(149, 149, 149, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(149, 149, 149, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(149, 149, 149, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(149, 149, 149, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(149, 149, 149, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(149, 149, 149, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(149, 149, 149, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(149, 149, 149, 0.10)\",\n        \"c-primary-light-400\": \"rgba(170,170,170, 1)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(170, 170, 170, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(170, 170, 170, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(170, 170, 170, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(170, 170, 170, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(170, 170, 170, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(170, 170, 170, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(170, 170, 170, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(170, 170, 170, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(170, 170, 170, 0.10)\",\n        \"c-primary-light-500\": \"rgba(187,187,187, 1)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(187, 187, 187, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(187, 187, 187, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(187, 187, 187, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(187, 187, 187, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(187, 187, 187, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(187, 187, 187, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(187, 187, 187, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(187, 187, 187, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(187, 187, 187, 0.10)\",\n        \"c-primary-light-600\": \"rgba(201,201,201, 1)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(201, 201, 201, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(201, 201, 201, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(201, 201, 201, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(201, 201, 201, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(201, 201, 201, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(201, 201, 201, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(201, 201, 201, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(201, 201, 201, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(201, 201, 201, 0.10)\",\n        \"c-primary-light-700\": \"rgba(212,212,212, 1)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(212, 212, 212, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(212, 212, 212, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(212, 212, 212, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(212, 212, 212, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(212, 212, 212, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(212, 212, 212, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(212, 212, 212, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(212, 212, 212, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(212, 212, 212, 0.10)\",\n        \"c-primary-light-800\": \"rgba(221,221,221, 1)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(221, 221, 221, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(221, 221, 221, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(221, 221, 221, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(221, 221, 221, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(221, 221, 221, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(221, 221, 221, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(221, 221, 221, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(221, 221, 221, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(221, 221, 221, 0.10)\",\n        \"c-primary-light-900\": \"rgba(228,228,228, 1)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(228, 228, 228, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(228, 228, 228, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(228, 228, 228, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(228, 228, 228, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(228, 228, 228, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(228, 228, 228, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(228, 228, 228, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(228, 228, 228, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(228, 228, 228, 0.10)\",\n        \"c-primary-light-1000\": \"rgba(255,255,255, 1)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgba(47, 47, 47, 1)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"rgba(255, 255, 255, 0)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"bg-image\": \"china_ink.jpg\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"rgba(137, 70, 70, 1)\",\n        \"c-badge-secondary\": \"rgba(67, 139, 65, 1)\",\n        \"c-badge-tertiary\": \"rgba(132, 135, 65, 1)\"\n      }\n    }\n  },\n  {\n    \"id\": \"happy_new_year\",\n    \"name\": \"新年快乐\",\n    \"isDark\": false,\n    \"isCustom\": false,\n    \"config\": {\n      \"themeColors\": {\n        \"c-primary\": \"rgb(192, 57, 43)\",\n        \"c-primary-dark-100\": \"rgb(173,51,39)\",\n        \"c-primary-dark-100-alpha-100\": \"rgba(173, 51, 39, 0.90)\",\n        \"c-primary-alpha-100\": \"rgba(192, 57, 43, 0.90)\",\n        \"c-primary-dark-100-alpha-200\": \"rgba(173, 51, 39, 0.80)\",\n        \"c-primary-alpha-200\": \"rgba(192, 57, 43, 0.80)\",\n        \"c-primary-dark-100-alpha-300\": \"rgba(173, 51, 39, 0.70)\",\n        \"c-primary-alpha-300\": \"rgba(192, 57, 43, 0.70)\",\n        \"c-primary-dark-100-alpha-400\": \"rgba(173, 51, 39, 0.60)\",\n        \"c-primary-alpha-400\": \"rgba(192, 57, 43, 0.60)\",\n        \"c-primary-dark-100-alpha-500\": \"rgba(173, 51, 39, 0.50)\",\n        \"c-primary-alpha-500\": \"rgba(192, 57, 43, 0.50)\",\n        \"c-primary-dark-100-alpha-600\": \"rgba(173, 51, 39, 0.40)\",\n        \"c-primary-alpha-600\": \"rgba(192, 57, 43, 0.40)\",\n        \"c-primary-dark-100-alpha-700\": \"rgba(173, 51, 39, 0.30)\",\n        \"c-primary-alpha-700\": \"rgba(192, 57, 43, 0.30)\",\n        \"c-primary-dark-100-alpha-800\": \"rgba(173, 51, 39, 0.20)\",\n        \"c-primary-alpha-800\": \"rgba(192, 57, 43, 0.20)\",\n        \"c-primary-dark-100-alpha-900\": \"rgba(173, 51, 39, 0.10)\",\n        \"c-primary-alpha-900\": \"rgba(192, 57, 43, 0.10)\",\n        \"c-primary-dark-200\": \"rgb(156,46,35)\",\n        \"c-primary-dark-200-alpha-100\": \"rgba(156, 46, 35, 0.90)\",\n        \"c-primary-dark-200-alpha-200\": \"rgba(156, 46, 35, 0.80)\",\n        \"c-primary-dark-200-alpha-300\": \"rgba(156, 46, 35, 0.70)\",\n        \"c-primary-dark-200-alpha-400\": \"rgba(156, 46, 35, 0.60)\",\n        \"c-primary-dark-200-alpha-500\": \"rgba(156, 46, 35, 0.50)\",\n        \"c-primary-dark-200-alpha-600\": \"rgba(156, 46, 35, 0.40)\",\n        \"c-primary-dark-200-alpha-700\": \"rgba(156, 46, 35, 0.30)\",\n        \"c-primary-dark-200-alpha-800\": \"rgba(156, 46, 35, 0.20)\",\n        \"c-primary-dark-200-alpha-900\": \"rgba(156, 46, 35, 0.10)\",\n        \"c-primary-dark-300\": \"rgb(140,41,32)\",\n        \"c-primary-dark-300-alpha-100\": \"rgba(140, 41, 32, 0.90)\",\n        \"c-primary-dark-300-alpha-200\": \"rgba(140, 41, 32, 0.80)\",\n        \"c-primary-dark-300-alpha-300\": \"rgba(140, 41, 32, 0.70)\",\n        \"c-primary-dark-300-alpha-400\": \"rgba(140, 41, 32, 0.60)\",\n        \"c-primary-dark-300-alpha-500\": \"rgba(140, 41, 32, 0.50)\",\n        \"c-primary-dark-300-alpha-600\": \"rgba(140, 41, 32, 0.40)\",\n        \"c-primary-dark-300-alpha-700\": \"rgba(140, 41, 32, 0.30)\",\n        \"c-primary-dark-300-alpha-800\": \"rgba(140, 41, 32, 0.20)\",\n        \"c-primary-dark-300-alpha-900\": \"rgba(140, 41, 32, 0.10)\",\n        \"c-primary-dark-400\": \"rgb(126,37,29)\",\n        \"c-primary-dark-400-alpha-100\": \"rgba(126, 37, 29, 0.90)\",\n        \"c-primary-dark-400-alpha-200\": \"rgba(126, 37, 29, 0.80)\",\n        \"c-primary-dark-400-alpha-300\": \"rgba(126, 37, 29, 0.70)\",\n        \"c-primary-dark-400-alpha-400\": \"rgba(126, 37, 29, 0.60)\",\n        \"c-primary-dark-400-alpha-500\": \"rgba(126, 37, 29, 0.50)\",\n        \"c-primary-dark-400-alpha-600\": \"rgba(126, 37, 29, 0.40)\",\n        \"c-primary-dark-400-alpha-700\": \"rgba(126, 37, 29, 0.30)\",\n        \"c-primary-dark-400-alpha-800\": \"rgba(126, 37, 29, 0.20)\",\n        \"c-primary-dark-400-alpha-900\": \"rgba(126, 37, 29, 0.10)\",\n        \"c-primary-dark-500\": \"rgb(113,33,26)\",\n        \"c-primary-dark-500-alpha-100\": \"rgba(113, 33, 26, 0.90)\",\n        \"c-primary-dark-500-alpha-200\": \"rgba(113, 33, 26, 0.80)\",\n        \"c-primary-dark-500-alpha-300\": \"rgba(113, 33, 26, 0.70)\",\n        \"c-primary-dark-500-alpha-400\": \"rgba(113, 33, 26, 0.60)\",\n        \"c-primary-dark-500-alpha-500\": \"rgba(113, 33, 26, 0.50)\",\n        \"c-primary-dark-500-alpha-600\": \"rgba(113, 33, 26, 0.40)\",\n        \"c-primary-dark-500-alpha-700\": \"rgba(113, 33, 26, 0.30)\",\n        \"c-primary-dark-500-alpha-800\": \"rgba(113, 33, 26, 0.20)\",\n        \"c-primary-dark-500-alpha-900\": \"rgba(113, 33, 26, 0.10)\",\n        \"c-primary-dark-600\": \"rgb(102,30,23)\",\n        \"c-primary-dark-600-alpha-100\": \"rgba(102, 30, 23, 0.90)\",\n        \"c-primary-dark-600-alpha-200\": \"rgba(102, 30, 23, 0.80)\",\n        \"c-primary-dark-600-alpha-300\": \"rgba(102, 30, 23, 0.70)\",\n        \"c-primary-dark-600-alpha-400\": \"rgba(102, 30, 23, 0.60)\",\n        \"c-primary-dark-600-alpha-500\": \"rgba(102, 30, 23, 0.50)\",\n        \"c-primary-dark-600-alpha-600\": \"rgba(102, 30, 23, 0.40)\",\n        \"c-primary-dark-600-alpha-700\": \"rgba(102, 30, 23, 0.30)\",\n        \"c-primary-dark-600-alpha-800\": \"rgba(102, 30, 23, 0.20)\",\n        \"c-primary-dark-600-alpha-900\": \"rgba(102, 30, 23, 0.10)\",\n        \"c-primary-dark-700\": \"rgb(92,27,21)\",\n        \"c-primary-dark-700-alpha-100\": \"rgba(92, 27, 21, 0.90)\",\n        \"c-primary-dark-700-alpha-200\": \"rgba(92, 27, 21, 0.80)\",\n        \"c-primary-dark-700-alpha-300\": \"rgba(92, 27, 21, 0.70)\",\n        \"c-primary-dark-700-alpha-400\": \"rgba(92, 27, 21, 0.60)\",\n        \"c-primary-dark-700-alpha-500\": \"rgba(92, 27, 21, 0.50)\",\n        \"c-primary-dark-700-alpha-600\": \"rgba(92, 27, 21, 0.40)\",\n        \"c-primary-dark-700-alpha-700\": \"rgba(92, 27, 21, 0.30)\",\n        \"c-primary-dark-700-alpha-800\": \"rgba(92, 27, 21, 0.20)\",\n        \"c-primary-dark-700-alpha-900\": \"rgba(92, 27, 21, 0.10)\",\n        \"c-primary-dark-800\": \"rgb(83,24,19)\",\n        \"c-primary-dark-800-alpha-100\": \"rgba(83, 24, 19, 0.90)\",\n        \"c-primary-dark-800-alpha-200\": \"rgba(83, 24, 19, 0.80)\",\n        \"c-primary-dark-800-alpha-300\": \"rgba(83, 24, 19, 0.70)\",\n        \"c-primary-dark-800-alpha-400\": \"rgba(83, 24, 19, 0.60)\",\n        \"c-primary-dark-800-alpha-500\": \"rgba(83, 24, 19, 0.50)\",\n        \"c-primary-dark-800-alpha-600\": \"rgba(83, 24, 19, 0.40)\",\n        \"c-primary-dark-800-alpha-700\": \"rgba(83, 24, 19, 0.30)\",\n        \"c-primary-dark-800-alpha-800\": \"rgba(83, 24, 19, 0.20)\",\n        \"c-primary-dark-800-alpha-900\": \"rgba(83, 24, 19, 0.10)\",\n        \"c-primary-dark-900\": \"rgb(75,22,17)\",\n        \"c-primary-dark-900-alpha-100\": \"rgba(75, 22, 17, 0.90)\",\n        \"c-primary-dark-900-alpha-200\": \"rgba(75, 22, 17, 0.80)\",\n        \"c-primary-dark-900-alpha-300\": \"rgba(75, 22, 17, 0.70)\",\n        \"c-primary-dark-900-alpha-400\": \"rgba(75, 22, 17, 0.60)\",\n        \"c-primary-dark-900-alpha-500\": \"rgba(75, 22, 17, 0.50)\",\n        \"c-primary-dark-900-alpha-600\": \"rgba(75, 22, 17, 0.40)\",\n        \"c-primary-dark-900-alpha-700\": \"rgba(75, 22, 17, 0.30)\",\n        \"c-primary-dark-900-alpha-800\": \"rgba(75, 22, 17, 0.20)\",\n        \"c-primary-dark-900-alpha-900\": \"rgba(75, 22, 17, 0.10)\",\n        \"c-primary-dark-1000\": \"rgb(68,20,15)\",\n        \"c-primary-dark-1000-alpha-100\": \"rgba(68, 20, 15, 0.90)\",\n        \"c-primary-dark-1000-alpha-200\": \"rgba(68, 20, 15, 0.80)\",\n        \"c-primary-dark-1000-alpha-300\": \"rgba(68, 20, 15, 0.70)\",\n        \"c-primary-dark-1000-alpha-400\": \"rgba(68, 20, 15, 0.60)\",\n        \"c-primary-dark-1000-alpha-500\": \"rgba(68, 20, 15, 0.50)\",\n        \"c-primary-dark-1000-alpha-600\": \"rgba(68, 20, 15, 0.40)\",\n        \"c-primary-dark-1000-alpha-700\": \"rgba(68, 20, 15, 0.30)\",\n        \"c-primary-dark-1000-alpha-800\": \"rgba(68, 20, 15, 0.20)\",\n        \"c-primary-dark-1000-alpha-900\": \"rgba(68, 20, 15, 0.10)\",\n        \"c-primary-light-100\": \"rgb(205,97,85)\",\n        \"c-primary-light-100-alpha-100\": \"rgba(205, 97, 85, 0.90)\",\n        \"c-primary-light-100-alpha-200\": \"rgba(205, 97, 85, 0.80)\",\n        \"c-primary-light-100-alpha-300\": \"rgba(205, 97, 85, 0.70)\",\n        \"c-primary-light-100-alpha-400\": \"rgba(205, 97, 85, 0.60)\",\n        \"c-primary-light-100-alpha-500\": \"rgba(205, 97, 85, 0.50)\",\n        \"c-primary-light-100-alpha-600\": \"rgba(205, 97, 85, 0.40)\",\n        \"c-primary-light-100-alpha-700\": \"rgba(205, 97, 85, 0.30)\",\n        \"c-primary-light-100-alpha-800\": \"rgba(205, 97, 85, 0.20)\",\n        \"c-primary-light-100-alpha-900\": \"rgba(205, 97, 85, 0.10)\",\n        \"c-primary-light-200\": \"rgb(215,129,119)\",\n        \"c-primary-light-200-alpha-100\": \"rgba(215, 129, 119, 0.90)\",\n        \"c-primary-light-200-alpha-200\": \"rgba(215, 129, 119, 0.80)\",\n        \"c-primary-light-200-alpha-300\": \"rgba(215, 129, 119, 0.70)\",\n        \"c-primary-light-200-alpha-400\": \"rgba(215, 129, 119, 0.60)\",\n        \"c-primary-light-200-alpha-500\": \"rgba(215, 129, 119, 0.50)\",\n        \"c-primary-light-200-alpha-600\": \"rgba(215, 129, 119, 0.40)\",\n        \"c-primary-light-200-alpha-700\": \"rgba(215, 129, 119, 0.30)\",\n        \"c-primary-light-200-alpha-800\": \"rgba(215, 129, 119, 0.20)\",\n        \"c-primary-light-200-alpha-900\": \"rgba(215, 129, 119, 0.10)\",\n        \"c-primary-light-300\": \"rgb(223,154,146)\",\n        \"c-primary-light-300-alpha-100\": \"rgba(223, 154, 146, 0.90)\",\n        \"c-primary-light-300-alpha-200\": \"rgba(223, 154, 146, 0.80)\",\n        \"c-primary-light-300-alpha-300\": \"rgba(223, 154, 146, 0.70)\",\n        \"c-primary-light-300-alpha-400\": \"rgba(223, 154, 146, 0.60)\",\n        \"c-primary-light-300-alpha-500\": \"rgba(223, 154, 146, 0.50)\",\n        \"c-primary-light-300-alpha-600\": \"rgba(223, 154, 146, 0.40)\",\n        \"c-primary-light-300-alpha-700\": \"rgba(223, 154, 146, 0.30)\",\n        \"c-primary-light-300-alpha-800\": \"rgba(223, 154, 146, 0.20)\",\n        \"c-primary-light-300-alpha-900\": \"rgba(223, 154, 146, 0.10)\",\n        \"c-primary-light-400\": \"rgb(229,174,168)\",\n        \"c-primary-light-400-alpha-100\": \"rgba(229, 174, 168, 0.90)\",\n        \"c-primary-light-400-alpha-200\": \"rgba(229, 174, 168, 0.80)\",\n        \"c-primary-light-400-alpha-300\": \"rgba(229, 174, 168, 0.70)\",\n        \"c-primary-light-400-alpha-400\": \"rgba(229, 174, 168, 0.60)\",\n        \"c-primary-light-400-alpha-500\": \"rgba(229, 174, 168, 0.50)\",\n        \"c-primary-light-400-alpha-600\": \"rgba(229, 174, 168, 0.40)\",\n        \"c-primary-light-400-alpha-700\": \"rgba(229, 174, 168, 0.30)\",\n        \"c-primary-light-400-alpha-800\": \"rgba(229, 174, 168, 0.20)\",\n        \"c-primary-light-400-alpha-900\": \"rgba(229, 174, 168, 0.10)\",\n        \"c-primary-light-500\": \"rgb(234,190,185)\",\n        \"c-primary-light-500-alpha-100\": \"rgba(234, 190, 185, 0.90)\",\n        \"c-primary-light-500-alpha-200\": \"rgba(234, 190, 185, 0.80)\",\n        \"c-primary-light-500-alpha-300\": \"rgba(234, 190, 185, 0.70)\",\n        \"c-primary-light-500-alpha-400\": \"rgba(234, 190, 185, 0.60)\",\n        \"c-primary-light-500-alpha-500\": \"rgba(234, 190, 185, 0.50)\",\n        \"c-primary-light-500-alpha-600\": \"rgba(234, 190, 185, 0.40)\",\n        \"c-primary-light-500-alpha-700\": \"rgba(234, 190, 185, 0.30)\",\n        \"c-primary-light-500-alpha-800\": \"rgba(234, 190, 185, 0.20)\",\n        \"c-primary-light-500-alpha-900\": \"rgba(234, 190, 185, 0.10)\",\n        \"c-primary-light-600\": \"rgb(238,203,199)\",\n        \"c-primary-light-600-alpha-100\": \"rgba(238, 203, 199, 0.90)\",\n        \"c-primary-light-600-alpha-200\": \"rgba(238, 203, 199, 0.80)\",\n        \"c-primary-light-600-alpha-300\": \"rgba(238, 203, 199, 0.70)\",\n        \"c-primary-light-600-alpha-400\": \"rgba(238, 203, 199, 0.60)\",\n        \"c-primary-light-600-alpha-500\": \"rgba(238, 203, 199, 0.50)\",\n        \"c-primary-light-600-alpha-600\": \"rgba(238, 203, 199, 0.40)\",\n        \"c-primary-light-600-alpha-700\": \"rgba(238, 203, 199, 0.30)\",\n        \"c-primary-light-600-alpha-800\": \"rgba(238, 203, 199, 0.20)\",\n        \"c-primary-light-600-alpha-900\": \"rgba(238, 203, 199, 0.10)\",\n        \"c-primary-light-700\": \"rgb(241,213,210)\",\n        \"c-primary-light-700-alpha-100\": \"rgba(241, 213, 210, 0.90)\",\n        \"c-primary-light-700-alpha-200\": \"rgba(241, 213, 210, 0.80)\",\n        \"c-primary-light-700-alpha-300\": \"rgba(241, 213, 210, 0.70)\",\n        \"c-primary-light-700-alpha-400\": \"rgba(241, 213, 210, 0.60)\",\n        \"c-primary-light-700-alpha-500\": \"rgba(241, 213, 210, 0.50)\",\n        \"c-primary-light-700-alpha-600\": \"rgba(241, 213, 210, 0.40)\",\n        \"c-primary-light-700-alpha-700\": \"rgba(241, 213, 210, 0.30)\",\n        \"c-primary-light-700-alpha-800\": \"rgba(241, 213, 210, 0.20)\",\n        \"c-primary-light-700-alpha-900\": \"rgba(241, 213, 210, 0.10)\",\n        \"c-primary-light-800\": \"rgb(244,221,219)\",\n        \"c-primary-light-800-alpha-100\": \"rgba(244, 221, 219, 0.90)\",\n        \"c-primary-light-800-alpha-200\": \"rgba(244, 221, 219, 0.80)\",\n        \"c-primary-light-800-alpha-300\": \"rgba(244, 221, 219, 0.70)\",\n        \"c-primary-light-800-alpha-400\": \"rgba(244, 221, 219, 0.60)\",\n        \"c-primary-light-800-alpha-500\": \"rgba(244, 221, 219, 0.50)\",\n        \"c-primary-light-800-alpha-600\": \"rgba(244, 221, 219, 0.40)\",\n        \"c-primary-light-800-alpha-700\": \"rgba(244, 221, 219, 0.30)\",\n        \"c-primary-light-800-alpha-800\": \"rgba(244, 221, 219, 0.20)\",\n        \"c-primary-light-800-alpha-900\": \"rgba(244, 221, 219, 0.10)\",\n        \"c-primary-light-900\": \"rgb(246,228,226)\",\n        \"c-primary-light-900-alpha-100\": \"rgba(246, 228, 226, 0.90)\",\n        \"c-primary-light-900-alpha-200\": \"rgba(246, 228, 226, 0.80)\",\n        \"c-primary-light-900-alpha-300\": \"rgba(246, 228, 226, 0.70)\",\n        \"c-primary-light-900-alpha-400\": \"rgba(246, 228, 226, 0.60)\",\n        \"c-primary-light-900-alpha-500\": \"rgba(246, 228, 226, 0.50)\",\n        \"c-primary-light-900-alpha-600\": \"rgba(246, 228, 226, 0.40)\",\n        \"c-primary-light-900-alpha-700\": \"rgba(246, 228, 226, 0.30)\",\n        \"c-primary-light-900-alpha-800\": \"rgba(246, 228, 226, 0.20)\",\n        \"c-primary-light-900-alpha-900\": \"rgba(246, 228, 226, 0.10)\",\n        \"c-primary-light-1000\": \"rgb(255,255,255)\",\n        \"c-primary-light-1000-alpha-100\": \"rgba(255, 255, 255, 0.90)\",\n        \"c-primary-light-1000-alpha-200\": \"rgba(255, 255, 255, 0.80)\",\n        \"c-primary-light-1000-alpha-300\": \"rgba(255, 255, 255, 0.70)\",\n        \"c-primary-light-1000-alpha-400\": \"rgba(255, 255, 255, 0.60)\",\n        \"c-primary-light-1000-alpha-500\": \"rgba(255, 255, 255, 0.50)\",\n        \"c-primary-light-1000-alpha-600\": \"rgba(255, 255, 255, 0.40)\",\n        \"c-primary-light-1000-alpha-700\": \"rgba(255, 255, 255, 0.30)\",\n        \"c-primary-light-1000-alpha-800\": \"rgba(255, 255, 255, 0.20)\",\n        \"c-primary-light-1000-alpha-900\": \"rgba(255, 255, 255, 0.10)\",\n        \"c-theme\": \"rgb(192, 57, 43)\",\n        \"c-1000\": \"rgb(33, 33, 33)\",\n        \"c-950\": \"rgb(44,44,44)\",\n        \"c-900\": \"rgb(55,55,55)\",\n        \"c-850\": \"rgb(66,66,66)\",\n        \"c-800\": \"rgb(77,77,77)\",\n        \"c-750\": \"rgb(89,89,89)\",\n        \"c-700\": \"rgb(100,100,100)\",\n        \"c-650\": \"rgb(111,111,111)\",\n        \"c-600\": \"rgb(122,122,122)\",\n        \"c-550\": \"rgb(133,133,133)\",\n        \"c-500\": \"rgb(144,144,144)\",\n        \"c-450\": \"rgb(155,155,155)\",\n        \"c-400\": \"rgb(166,166,166)\",\n        \"c-350\": \"rgb(177,177,177)\",\n        \"c-300\": \"rgb(188,188,188)\",\n        \"c-250\": \"rgb(200,200,200)\",\n        \"c-200\": \"rgb(211,211,211)\",\n        \"c-150\": \"rgb(222,222,222)\",\n        \"c-100\": \"rgb(233,233,233)\",\n        \"c-050\": \"rgb(244,244,244)\",\n        \"c-000\": \"rgb(255,255,255)\"\n      },\n      \"extInfo\": {\n        \"c-app-background\": \"rgba(255, 255, 255, 0.15)\",\n        \"c-main-background\": \"rgba(255, 255, 255, 0.8)\",\n        \"bg-image\": \"xnkl.png\",\n        \"bg-image-position\": \"center\",\n        \"bg-image-size\": \"cover\",\n        \"c-badge-primary\": \"#7fb575\",\n        \"c-badge-secondary\": \"#dfbb6b\",\n        \"c-badge-tertiary\": \"var(c-primary-light-100)\"\n      }\n    }\n  }\n] as const"
  },
  {
    "path": "src/theme/themes/utils.js",
    "content": "const { RGB_Linear_Shade, RGB_Alpha_Shade } = require('./colorUtils')\n\nexports.createThemeColors = (rgbaColor, fontRgbaColor, isDark) => {\n  const colors = {\n    'c-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[`c-primary-dark-${i * 100}`] = preColor\n    for (let j = 1; j < 10; j += 1) {\n      colors[`c-primary-dark-${i * 100}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)\n      colors[`c-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[`c-primary-light-${i * 100}`] = preColor\n    for (let j = 1; j < 10; j += 1) {\n      colors[`c-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[`c-primary-light-${1000}`] = preColor\n  for (let j = 1; j < 10; j += 1) {\n    colors[`c-primary-light-${1000}-alpha-${j * 100}`] = RGB_Alpha_Shade(0.1 * j, preColor)\n  }\n\n  colors['c-theme'] = isDark ? colors['c-primary-light-900'] : rgbaColor\n\n  return { ...colors, ...createFontColors(fontRgbaColor, isDark) }\n}\n\nconst createFontColors = (rgbaColor, isDark) => {\n  // rgb(238, 238, 238)\n  // let prec = 'rgb(255, 255, 255)'\n  if (rgbaColor == null) rgbaColor = isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)'\n  if (isDark) return createFontDarkColors(rgbaColor)\n\n  let colors = {\n    'c-1000': rgbaColor,\n  }\n  let step = isDark ? -0.05 : 0.05\n  for (let i = 1; i < 21; i += 1) {\n    colors[`c-${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) => {\n  // rgb(238, 238, 238)\n  // let prec = 'rgb(255, 255, 255)'\n\n  let colors = {\n    'c-1000': rgbaColor,\n  }\n  const step = -0.05\n  let preColor = rgbaColor\n  for (let i = 1; i < 21; i += 1) {\n    preColor = RGB_Linear_Shade(step, preColor)\n    colors[`c-${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/types/app.d.ts",
    "content": "/* eslint-disable no-var */\nimport type { AppEventTypes } from '@/event/appEvent'\nimport type { ListEventTypes } from '@/event/listEvent'\nimport type { DislikeEventTypes } from '@/event/dislikeEvent'\nimport type { StateEventTypes } from '@/event/stateEvent'\nimport type { I18n } from '@/lang/i18n'\nimport type { Buffer as _Buffer } from 'buffer'\nimport type { SettingScreenIds } from '@/screens/Home/Views/Setting'\n\n// interface Process {\n//   env: {\n//     NODE_ENV: 'development' | 'production'\n//   }\n//   versions: {\n//     app: string\n//   }\n// }\ninterface GlobalData {\n  fontSize: number\n  gettingUrlId: string\n\n  // event_app: AppType\n  // event_list: ListType\n\n  playerStatus: {\n    isInitialized: boolean\n    isRegisteredService: boolean\n    isIniting: boolean\n  }\n  restorePlayInfo: LX.Player.SavedPlayInfo | null\n  isScreenKeepAwake: boolean\n  isPlayedStop: boolean\n  isEnableSyncLog: boolean\n  isEnableUserApiLog: boolean\n  playerTrackId: string\n\n  qualityList: LX.QualityList\n  apis: Partial<LX.UserApi.UserApiSources>\n  apiInitPromise: [Promise<boolean>, boolean, (success: boolean) => void]\n\n  jumpMyListPosition: boolean\n\n  settingActiveId: SettingScreenIds\n\n  /**\n   * 首页是否正在滚动中，用于防止意外误触播放歌曲\n   */\n  homePagerIdle: boolean\n\n  // windowInfo: {\n  //   screenW: number\n  //   screenH: number\n  //   fontScale: number\n  //   pixelRatio: number\n  //   screenPxW: number\n  //   screenPxH: number\n  // }\n\n  // syncKeyInfo: LX.Sync.KeyInfo\n}\n\n\ndeclare global {\n  var isDev: boolean\n  var lx: GlobalData\n  var i18n: I18n\n  var app_event: AppEventTypes\n  var list_event: ListEventTypes\n  var dislike_event: DislikeEventTypes\n  var state_event: StateEventTypes\n\n  var Buffer: typeof _Buffer\n\n  module NodeJS {\n    interface ProcessVersions {\n      app: string\n    }\n  }\n  // var process: Process\n}\n"
  },
  {
    "path": "src/types/app_setting.d.ts",
    "content": "import type { I18n } from '@/lang/i18n'\n\ndeclare global {\n  namespace LX {\n    type AddMusicLocationType = 'top' | 'bottom'\n\n    interface AppSetting {\n      version: string\n      /**\n       * 是否跟随系统切换亮暗主题\n       */\n      'common.isAutoTheme': 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.shareType': 'system' | 'clipboard'\n\n      /**\n       * 是否同意软件协议\n       */\n      'common.isAgreePact': boolean\n\n      /**\n       * 是否在键盘弹出时隐藏播放栏\n       */\n      'common.autoHidePlayBar': boolean\n\n      /**\n       * 抽屉组件弹出方向\n       */\n      'common.drawerLayoutPosition': 'left' | 'right'\n\n      /**\n       * 启用首页滑动\n       */\n      'common.homePageScroll': boolean\n\n      /**\n       * 允许通过底栏进度条调整进度\n       */\n      'common.allowProgressBarSeek': boolean\n\n      /**\n       * 是否显示返回按钮\n       */\n      'common.showBackBtn': boolean\n\n      /**\n       * 是否显示退出按钮\n       */\n      'common.showExitBtn': boolean\n\n      /**\n       * 使用系统文件选择器\n       */\n      'common.useSystemFileSelector': boolean\n\n      /**\n       * 总是保留状态栏高度\n       */\n      'common.alwaysKeepStatusbarHeight': 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      'theme.hideBgDark': boolean\n\n      /**\n       * 动态背景\n       */\n      'theme.dynamicBg': boolean\n\n      /**\n       * 字体阴影\n       */\n      'theme.fontShadow': boolean\n\n      /**\n       * 启动时自动播放歌曲\n       */\n      'player.startupAutoPlay': boolean\n\n      /**\n       * 启动后打开歌曲详细界面\n       */\n      'player.startupPushPlayDetailScreen': 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.isSavePlayTime': boolean\n\n      /**\n       * 音量大小\n       */\n      'player.volume': number\n\n      /**\n       * 播放速率\n       */\n      'player.playbackRate': number\n\n      /**\n       * 缓存大小设置 unit MB\n       */\n      'player.cacheSize': string\n\n      /**\n       * 定时暂停播放-倒计时时间\n       */\n      'player.timeoutExit': string\n\n      /**\n       * 定时暂停播放-是否等待歌曲播放完毕再暂停\n       */\n      'player.timeoutExitPlayed': boolean\n\n      /**\n       * 点击相同列表内的歌曲切歌时是否清空已播放列表（随机模式下列表内所有歌曲会重新参与随机）\n       */\n      'player.isAutoCleanPlayedList': boolean\n\n      /**\n       * 其他应用播放声音时是否自动暂停\n       */\n      'player.isHandleAudioFocus': boolean\n\n      /**\n       * 是否启用音频卸载功能（这可以节省耗电量，没有播放异常问题不建议关闭）\n       */\n      'player.isEnableAudioOffload': boolean\n\n      /**\n       * 是否显示歌词翻译\n       */\n      'player.isShowLyricTranslation': boolean\n\n      /**\n       * 是否显示歌词罗马音\n       */\n      'player.isShowLyricRoma': boolean\n\n      /**\n       * 是否在通知栏显示歌曲图片\n       */\n      'player.isShowNotificationImage': boolean\n\n      /**\n       * 是否将歌词从简体转换为繁体\n       */\n      'player.isS2t': boolean\n\n      /**\n       * 是否启用蓝牙歌词\n       */\n      'player.isShowBluetoothLyric': boolean\n\n      /**\n       * 播放详情页-是否缩放当前播放的歌词行\n       */\n      // 'playDetail.isZoomActiveLrc': boolean\n\n      /**\n       * 播放详情页-是否允许通过歌词调整播放进度\n       */\n      // 'playDetail.isShowLyricProgressSetting': boolean\n\n      /**\n       * 播放详情页-歌词对齐方式\n       */\n      'playDetail.style.align': 'center' | 'left' | 'right'\n\n      /**\n       * 竖屏歌词字体大小\n       */\n      'playDetail.vertical.style.lrcFontSize': number\n\n      /**\n       * 横屏歌词字体大小\n       */\n      'playDetail.horizontal.style.lrcFontSize': number\n\n      /**\n       * 播放详情页-是否允许通过歌词调整播放进度\n       */\n      'playDetail.isShowLyricProgressSetting': boolean\n\n      /**\n       * 是否启用桌面歌词\n       */\n      'desktopLyric.enable': boolean\n\n      /**\n       * 是否锁定桌面歌词\n       */\n      'desktopLyric.isLock': boolean\n\n      /**\n       * 桌面歌词窗口宽度\n       */\n      'desktopLyric.width': number\n\n      /**\n       * 桌面歌词最大行数\n       */\n      'desktopLyric.maxLineNum': number\n\n      /**\n       * 桌面歌词是否使用单行显示\n       */\n      'desktopLyric.isSingleLine': boolean\n\n      /**\n       * 桌面歌词是否启用歌词切换动画\n       */\n      'desktopLyric.showToggleAnima': boolean\n\n      /**\n       * 桌面歌词窗口x坐标\n       */\n      'desktopLyric.position.x': number\n\n      /**\n       * 桌面歌词窗口y坐标\n       */\n      'desktopLyric.position.y': number\n\n      /**\n       * 歌词水平对齐方式\n       */\n      'desktopLyric.textPosition.x': 'left' | 'center' | 'right'\n\n      /**\n       * 歌词垂直对齐方式\n       */\n      'desktopLyric.textPosition.y': 'top' | 'center' | 'bottom'\n\n      /**\n       * 桌面歌词字体大小\n       */\n      'desktopLyric.style.fontSize': number\n\n      /**\n       * 桌面歌词字体透明度\n       */\n      'desktopLyric.style.opacity': 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      'search.isShowHotSearch': boolean\n\n      /**\n       * 是否显示搜索历史\n       */\n      'search.isShowHistorySearch': boolean\n\n      /**\n       * 是否启用双击列表里的歌曲时自动切换到当前列表播放（仅对歌单、排行榜有效）\n       */\n      'list.isClickPlayList': boolean\n\n      /**\n       * 是否显示歌曲来源（仅对我的列表有效）\n       */\n      'list.isShowSource': boolean\n\n      /**\n       * 是否显示歌曲专辑名\n       */\n      'list.isShowAlbumName': boolean\n\n      /**\n       * 是否显示歌曲时长\n       */\n      'list.isShowInterval': boolean\n\n      /**\n       * 是否自动恢复列表滚动位置（仅对我的列表有效）\n       */\n      'list.isSaveScrollLocation': boolean\n\n      /**\n       * 添加歌曲到我的列表时的方式\n       */\n      'list.addMusicLocationType': AddMusicLocationType\n\n      /**\n       * 文件命名方式\n       */\n      'download.fileName': '歌名 - 歌手' | '歌手 - 歌名' | '歌名'\n\n      /**\n       * 是否启用同步\n       */\n      'sync.enable': boolean\n    }\n  }\n}\n\n"
  },
  {
    "path": "src/types/common.d.ts",
    "content": "// import './app_setting'\n\ndeclare namespace LX {\n  type OnlineSource = 'kw' | 'kg' | 'tx' | 'wy' | 'mg'\n  type Source = OnlineSource | 'local'\n  type Quality = '128k' | '320k' | 'flac' | 'flac24bit' | '192k' | 'ape' | 'wav'\n  type QualityList = Partial<Record<LX.Source, LX.Quality[]>>\n\n  type ShareType = 'system' | 'clipboard'\n\n  type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle'\n  interface VersionInfo {\n    version: string\n    desc: string\n  }\n}\n"
  },
  {
    "path": "src/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/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/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/types/download_list.d.ts",
    "content": "\n// interface DownloadList {\n\n// }\n\n\ndeclare 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    }\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?: string\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      metadata: {\n        musicInfo: LX.Music.MusicInfoOnline\n        url: string | null\n        quality: LX.Quality\n        ext: FileExt\n        fileName: string\n        filePath: string\n      }\n    }\n\n    interface saveDownloadMusicInfo {\n      list: ListItem[]\n      addMusicLocationType: LX.AddMusicLocationType\n    }\n  }\n}\n"
  },
  {
    "path": "src/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: '试听列表'\n      // list: LX.Music.MusicInfo[]\n    }\n\n    interface MyLoveListInfo {\n      id: '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    type ListMusics = LX.Music.MusicInfo[]\n  }\n}\n"
  },
  {
    "path": "src/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      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<'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  }\n}\n"
  },
  {
    "path": "src/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 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/types/player.d.ts",
    "content": "import type { Track as RNTrack } from 'react-native-track-player'\n\ndeclare global {\n  namespace LX {\n    namespace Player {\n      interface MusicInfo {\n        id: string | null\n        pic: string | null | undefined\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\n      interface LyricInfo extends LX.Music.LyricInfo {\n        rawlrcInfo: LX.Music.LyricInfo\n      }\n\n      type PlayMusic = LX.Music.MusicInfo | LX.Download.ListItem\n\n      type PlayMusicInfo = Readonly<{\n        /**\n         * 当前播放歌曲的列表 id\n         */\n        musicInfo: PlayMusic\n        /**\n          * 当前播放歌曲的列表 id\n          */\n        listId: string\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: PlayMusic\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      interface Track extends RNTrack {\n        musicId: string\n        // original: PlayMusic\n        // quality: LX.Quality\n      }\n\n    }\n  }\n}\n"
  },
  {
    "path": "src/types/sync.d.ts",
    "content": "declare global {\n  namespace LX {\n    namespace Sync {\n      interface Status {\n        status: boolean\n        message: string\n      }\n\n      interface KeyInfo {\n        clientId: string\n        key: string\n        serverName: string\n      }\n\n      interface Socket extends WebSocket {\n        isReady: boolean\n        data: {\n          keyInfo: KeyInfo\n          urlInfo: UrlInfo\n        }\n        moduleReadys: {\n          list: boolean\n          dislike: boolean\n        }\n\n        onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void\n\n        remote: LX.Sync.ServerSyncActions\n        remoteQueueList: LX.Sync.ServerSyncListActions\n        remoteQueueDislike: LX.Sync.ServerSyncDislikeActions\n      }\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      interface UrlInfo {\n        wsProtocol: string\n        httpProtocol: string\n        hostPath: string\n        href: string\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}\n\nexport {}\n"
  },
  {
    "path": "src/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/types/theme.d.ts",
    "content": "import type { ImageSourcePropType } from 'react-native'\n\ndeclare global {\n  namespace LX {\n    interface ThemeColors {\n      'c-000': string\n      'c-050': string\n      'c-100': string\n      'c-150': string\n      'c-200': string\n      'c-250': string\n      'c-300': string\n      'c-350': string\n      'c-400': string\n      'c-450': string\n      'c-500': string\n      'c-550': string\n      'c-600': string\n      'c-650': string\n      'c-700': string\n      'c-750': string\n      'c-800': string\n      'c-850': string\n      'c-900': string\n      'c-950': string\n      'c-1000': string\n\n\n      'c-theme': string\n\n      'c-primary': string\n      'c-primary-alpha-100': string\n      'c-primary-alpha-200': string\n      'c-primary-alpha-300': string\n      'c-primary-alpha-400': string\n      'c-primary-alpha-500': string\n      'c-primary-alpha-600': string\n      'c-primary-alpha-700': string\n      'c-primary-alpha-800': string\n      'c-primary-alpha-900': string\n\n      'c-primary-dark-100': string\n      'c-primary-dark-100-alpha-100': string\n      'c-primary-dark-100-alpha-200': string\n      'c-primary-dark-100-alpha-300': string\n      'c-primary-dark-100-alpha-400': string\n      'c-primary-dark-100-alpha-500': string\n      'c-primary-dark-100-alpha-600': string\n      'c-primary-dark-100-alpha-700': string\n      'c-primary-dark-100-alpha-800': string\n      'c-primary-dark-100-alpha-900': string\n\n      'c-primary-dark-200': string\n      'c-primary-dark-200-alpha-100': string\n      'c-primary-dark-200-alpha-200': string\n      'c-primary-dark-200-alpha-300': string\n      'c-primary-dark-200-alpha-400': string\n      'c-primary-dark-200-alpha-500': string\n      'c-primary-dark-200-alpha-600': string\n      'c-primary-dark-200-alpha-700': string\n      'c-primary-dark-200-alpha-800': string\n      'c-primary-dark-200-alpha-900': string\n\n      'c-primary-dark-300': string\n      'c-primary-dark-300-alpha-100': string\n      'c-primary-dark-300-alpha-200': string\n      'c-primary-dark-300-alpha-300': string\n      'c-primary-dark-300-alpha-400': string\n      'c-primary-dark-300-alpha-500': string\n      'c-primary-dark-300-alpha-600': string\n      'c-primary-dark-300-alpha-700': string\n      'c-primary-dark-300-alpha-800': string\n      'c-primary-dark-300-alpha-900': string\n\n      'c-primary-dark-400': string\n      'c-primary-dark-400-alpha-100': string\n      'c-primary-dark-400-alpha-200': string\n      'c-primary-dark-400-alpha-300': string\n      'c-primary-dark-400-alpha-400': string\n      'c-primary-dark-400-alpha-500': string\n      'c-primary-dark-400-alpha-600': string\n      'c-primary-dark-400-alpha-700': string\n      'c-primary-dark-400-alpha-800': string\n      'c-primary-dark-400-alpha-900': string\n\n      'c-primary-dark-500': string\n      'c-primary-dark-500-alpha-100': string\n      'c-primary-dark-500-alpha-200': string\n      'c-primary-dark-500-alpha-300': string\n      'c-primary-dark-500-alpha-400': string\n      'c-primary-dark-500-alpha-500': string\n      'c-primary-dark-500-alpha-600': string\n      'c-primary-dark-500-alpha-700': string\n      'c-primary-dark-500-alpha-800': string\n      'c-primary-dark-500-alpha-900': string\n\n      'c-primary-dark-600': string\n      'c-primary-dark-600-alpha-100': string\n      'c-primary-dark-600-alpha-200': string\n      'c-primary-dark-600-alpha-300': string\n      'c-primary-dark-600-alpha-400': string\n      'c-primary-dark-600-alpha-500': string\n      'c-primary-dark-600-alpha-600': string\n      'c-primary-dark-600-alpha-700': string\n      'c-primary-dark-600-alpha-800': string\n      'c-primary-dark-600-alpha-900': string\n\n      'c-primary-dark-700': string\n      'c-primary-dark-700-alpha-100': string\n      'c-primary-dark-700-alpha-200': string\n      'c-primary-dark-700-alpha-300': string\n      'c-primary-dark-700-alpha-400': string\n      'c-primary-dark-700-alpha-500': string\n      'c-primary-dark-700-alpha-600': string\n      'c-primary-dark-700-alpha-700': string\n      'c-primary-dark-700-alpha-800': string\n      'c-primary-dark-700-alpha-900': string\n\n      'c-primary-dark-800': string\n      'c-primary-dark-800-alpha-100': string\n      'c-primary-dark-800-alpha-200': string\n      'c-primary-dark-800-alpha-300': string\n      'c-primary-dark-800-alpha-400': string\n      'c-primary-dark-800-alpha-500': string\n      'c-primary-dark-800-alpha-600': string\n      'c-primary-dark-800-alpha-700': string\n      'c-primary-dark-800-alpha-800': string\n      'c-primary-dark-800-alpha-900': string\n\n      'c-primary-dark-900': string\n      'c-primary-dark-900-alpha-100': string\n      'c-primary-dark-900-alpha-200': string\n      'c-primary-dark-900-alpha-300': string\n      'c-primary-dark-900-alpha-400': string\n      'c-primary-dark-900-alpha-500': string\n      'c-primary-dark-900-alpha-600': string\n      'c-primary-dark-900-alpha-700': string\n      'c-primary-dark-900-alpha-800': string\n      'c-primary-dark-900-alpha-900': string\n\n      'c-primary-dark-1000': string\n      'c-primary-dark-1000-alpha-100': string\n      'c-primary-dark-1000-alpha-200': string\n      'c-primary-dark-1000-alpha-300': string\n      'c-primary-dark-1000-alpha-400': string\n      'c-primary-dark-1000-alpha-500': string\n      'c-primary-dark-1000-alpha-600': string\n      'c-primary-dark-1000-alpha-700': string\n      'c-primary-dark-1000-alpha-800': string\n      'c-primary-dark-1000-alpha-900': string\n\n      'c-primary-light-100': string\n      'c-primary-light-100-alpha-100': string\n      'c-primary-light-100-alpha-200': string\n      'c-primary-light-100-alpha-300': string\n      'c-primary-light-100-alpha-400': string\n      'c-primary-light-100-alpha-500': string\n      'c-primary-light-100-alpha-600': string\n      'c-primary-light-100-alpha-700': string\n      'c-primary-light-100-alpha-800': string\n      'c-primary-light-100-alpha-900': string\n\n      'c-primary-light-200': string\n      'c-primary-light-200-alpha-100': string\n      'c-primary-light-200-alpha-200': string\n      'c-primary-light-200-alpha-300': string\n      'c-primary-light-200-alpha-400': string\n      'c-primary-light-200-alpha-500': string\n      'c-primary-light-200-alpha-600': string\n      'c-primary-light-200-alpha-700': string\n      'c-primary-light-200-alpha-800': string\n      'c-primary-light-200-alpha-900': string\n\n      'c-primary-light-300': string\n      'c-primary-light-300-alpha-100': string\n      'c-primary-light-300-alpha-200': string\n      'c-primary-light-300-alpha-300': string\n      'c-primary-light-300-alpha-400': string\n      'c-primary-light-300-alpha-500': string\n      'c-primary-light-300-alpha-600': string\n      'c-primary-light-300-alpha-700': string\n      'c-primary-light-300-alpha-800': string\n      'c-primary-light-300-alpha-900': string\n\n      'c-primary-light-400': string\n      'c-primary-light-400-alpha-100': string\n      'c-primary-light-400-alpha-200': string\n      'c-primary-light-400-alpha-300': string\n      'c-primary-light-400-alpha-400': string\n      'c-primary-light-400-alpha-500': string\n      'c-primary-light-400-alpha-600': string\n      'c-primary-light-400-alpha-700': string\n      'c-primary-light-400-alpha-800': string\n      'c-primary-light-400-alpha-900': string\n\n      'c-primary-light-500': string\n      'c-primary-light-500-alpha-100': string\n      'c-primary-light-500-alpha-200': string\n      'c-primary-light-500-alpha-300': string\n      'c-primary-light-500-alpha-400': string\n      'c-primary-light-500-alpha-500': string\n      'c-primary-light-500-alpha-600': string\n      'c-primary-light-500-alpha-700': string\n      'c-primary-light-500-alpha-800': string\n      'c-primary-light-500-alpha-900': string\n\n      'c-primary-light-600': string\n      'c-primary-light-600-alpha-100': string\n      'c-primary-light-600-alpha-200': string\n      'c-primary-light-600-alpha-300': string\n      'c-primary-light-600-alpha-400': string\n      'c-primary-light-600-alpha-500': string\n      'c-primary-light-600-alpha-600': string\n      'c-primary-light-600-alpha-700': string\n      'c-primary-light-600-alpha-800': string\n      'c-primary-light-600-alpha-900': string\n\n      'c-primary-light-700': string\n      'c-primary-light-700-alpha-100': string\n      'c-primary-light-700-alpha-200': string\n      'c-primary-light-700-alpha-300': string\n      'c-primary-light-700-alpha-400': string\n      'c-primary-light-700-alpha-500': string\n      'c-primary-light-700-alpha-600': string\n      'c-primary-light-700-alpha-700': string\n      'c-primary-light-700-alpha-800': string\n      'c-primary-light-700-alpha-900': string\n\n      'c-primary-light-800': string\n      'c-primary-light-800-alpha-100': string\n      'c-primary-light-800-alpha-200': string\n      'c-primary-light-800-alpha-300': string\n      'c-primary-light-800-alpha-400': string\n      'c-primary-light-800-alpha-500': string\n      'c-primary-light-800-alpha-600': string\n      'c-primary-light-800-alpha-700': string\n      'c-primary-light-800-alpha-800': string\n      'c-primary-light-800-alpha-900': string\n\n      'c-primary-light-900': string\n      'c-primary-light-900-alpha-100': string\n      'c-primary-light-900-alpha-200': string\n      'c-primary-light-900-alpha-300': string\n      'c-primary-light-900-alpha-400': string\n      'c-primary-light-900-alpha-500': string\n      'c-primary-light-900-alpha-600': string\n      'c-primary-light-900-alpha-700': string\n      'c-primary-light-900-alpha-800': string\n      'c-primary-light-900-alpha-900': string\n\n      'c-primary-light-1000': string\n      'c-primary-light-1000-alpha-100': string\n      'c-primary-light-1000-alpha-200': string\n      'c-primary-light-1000-alpha-300': string\n      'c-primary-light-1000-alpha-400': string\n      'c-primary-light-1000-alpha-500': string\n      'c-primary-light-1000-alpha-600': string\n      'c-primary-light-1000-alpha-700': string\n      'c-primary-light-1000-alpha-800': string\n      'c-primary-light-1000-alpha-900': string\n    }\n\n    type ActiveTheme = ThemeColors & Omit<Theme['config']['extInfo'], 'bg-image'> & Pick<Theme, 'id' | 'name' | 'isDark'> & {\n      'c-font': string\n      'c-font-label': string\n      'c-primary-font': string\n      'c-primary-font-hover': string\n      'c-primary-font-active': string\n      'c-primary-background': string\n      'c-primary-background-hover': string\n      'c-primary-background-active': string\n      'c-primary-input-background': string\n      'c-button-font': string\n      'c-button-font-selected': string\n      'c-button-background': string\n      'c-button-background-selected': string\n      'c-button-background-hover': string\n      'c-button-background-active': string\n      'c-list-header-border-bottom': string\n      'c-content-background': string\n      'c-border-background': string\n      'bg-image'?: ImageSourcePropType\n    }\n\n    interface Theme {\n      id: string\n      name: string\n      isDark: boolean\n      isCustom: boolean\n      config: {\n        themeColors: ThemeColors\n        extInfo: {\n          'c-app-background': string\n          'c-main-background': string\n          'bg-image': string\n          'bg-image-position': string\n          'bg-image-size': string\n\n          // 徽章颜色\n          'c-badge-primary': string\n          'c-badge-secondary': string\n          'c-badge-tertiary': string\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}\n"
  },
  {
    "path": "src/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 UserApiInfo {\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    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    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/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\ntype MakeArrayItemReadOnly<T extends any[]> = { [K in keyof T]: Readonly<T[K]> }\n\ntype ForwardRefFn<R> = <P = {}>(p: React.PropsWithChildren<P> & React.RefAttributes<R>) => React.ReactNode | null\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/utils/bootLog.ts",
    "content": "const logs: string[] = []\n\n\nexport const bootLog = (...msgs: any[]) => {\n  logs.push(msgs.map(m => typeof m == 'string' ? m : JSON.stringify(m)).join(' '))\n}\n\nexport const getBootLog = () => {\n  return logs.join('\\n')\n}\n\n"
  },
  {
    "path": "src/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?: number | string | Date): 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: number | string | Date, 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\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\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\n\n// export const freezeListItem = <T extends any[]>(list: T): MakeArrayItemReadOnly<T> => {\n//   for (let i = 0; i < list.length; i++) {\n//     Object.freeze(list[i])\n//   }\n//   return list\n// }\n\nexport const b64DecodeUnicode = (str: string) => {\n  // Going backwards: from bytestream, to percent-encoding, to original string.\n  return Buffer.from(str, 'base64').toString()\n}\n"
  },
  {
    "path": "src/utils/data.ts",
    "content": "import { getData, saveData, getAllKeys, removeDataMultiple, saveDataMultiple, removeData, getDataMultiple } from '@/plugins/storage'\nimport { DEFAULT_SETTING, LIST_IDS, storageDataPrefix, type NAV_ID_Type } from '@/config/constant'\nimport { throttle } from './common'\n// import { gzip, ungzip } from '@/utils/nativeModules/gzip'\n// import { readFile, writeFile, temporaryDirectoryPath, unlink } from '@/utils/fs'\n// import { isNotificationsEnabled, openNotificationPermissionActivity, shareText } from '@/utils/nativeModules/utils'\n// import { i18n } from '@/plugins/i18n'\n// import musicSdk from '@/utils/musicSdk'\n\nconst fontSizeKey = storageDataPrefix.fontSize\nconst themeKey = storageDataPrefix.theme\nconst playInfoStorageKey = storageDataPrefix.playInfo\nconst userListKey = storageDataPrefix.userList\nconst viewPrevStateKey = storageDataPrefix.viewPrevState\nconst listScrollPositionKey = storageDataPrefix.listScrollPosition\nconst listUpdateInfoKey = storageDataPrefix.listUpdateInfo\nconst ignoreVersionKey = storageDataPrefix.ignoreVersion\nconst ignoreVersionFailTipTimeKey = storageDataPrefix.ignoreVersionFailTipTimeKey\nconst searchSettingKey = storageDataPrefix.searchSetting\nconst searchHistoryListKey = storageDataPrefix.searchHistoryList\nconst songListSettingKey = storageDataPrefix.songListSetting\nconst leaderboardSettingKey = storageDataPrefix.leaderboardSetting\nconst listPrevSelectIdKey = storageDataPrefix.listPrevSelectId\nconst syncAuthKeyPrefix = storageDataPrefix.syncAuthKey\nconst syncHostPrefix = storageDataPrefix.syncHost\nconst syncHostHistoryPrefix = storageDataPrefix.syncHostHistory\nconst listPrefix = storageDataPrefix.list\nconst dislikeListPrefix = storageDataPrefix.dislikeList\nconst userApiPrefix = storageDataPrefix.userApi\nconst openStoragePathPrefix = storageDataPrefix.openStoragePath\nconst selectedManagedFolderPrefix = storageDataPrefix.selectedManagedFolder\n\n// const defaultListKey = listPrefix + 'default'\n// const loveListKey = listPrefix + 'love'\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']\nlet searchHistoryList: string[]\n\nconst saveListPositionThrottle = throttle(() => {\n  void saveData(listScrollPositionKey, listPosition)\n}, 1000)\nconst saveSearchSettingThrottle = throttle(() => {\n  void saveData(searchSettingKey, searchSetting)\n}, 1000)\nconst saveSearchHistoryThrottle = throttle(() => {\n  void saveData(searchHistoryListKey, searchHistoryList)\n}, 1000)\nconst saveSongListSettingThrottle = throttle(() => {\n  void saveData(songListSettingKey, songListSetting)\n}, 1000)\nconst saveLeaderboardSettingThrottle = throttle(() => {\n  void saveData(leaderboardSettingKey, leaderboardSetting)\n}, 1000)\nconst saveViewPrevStateThrottle = throttle((state) => {\n  void saveData(viewPrevStateKey, state)\n}, 1000)\n\nexport const getFontSize = async() => (await getData<number>(fontSizeKey) ?? 1)\nexport const saveFontSize = async(size: number) => {\n  await saveData(fontSizeKey, size)\n}\n\nexport const getUserTheme = async() => (await getData<LX.Theme[]>(themeKey) ?? [])\nexport const saveUserTheme = async(themes: LX.Theme[]) => {\n  await saveData(themeKey, themes)\n}\n\n\nconst initPosition = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  listPosition ??= await getData(listScrollPositionKey) ?? {}\n}\nexport const getListPosition = async(id: string): Promise<number> => {\n  await initPosition()\n  return listPosition[id] ?? 0\n}\nexport const saveListPosition = 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  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  void saveData(listPrevSelectIdKey, listPrevSelectId)\n}, 200)\nexport const getListPrevSelectId = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  listPrevSelectId ??= await getData(listPrevSelectIdKey) ?? LIST_IDS.DEFAULT\n  return listPrevSelectId || LIST_IDS.DEFAULT\n}\nexport const saveListPrevSelectId = (id: string) => {\n  listPrevSelectId = id\n  saveListPrevSelectIdThrottle()\n}\n\nconst saveListUpdateInfoThrottle = throttle(() => {\n  void saveData(listUpdateInfoKey, listUpdateInfo)\n}, 1000)\n\nconst initListUpdateInfo = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  listUpdateInfo ??= await getData(listUpdateInfoKey) ?? {}\n}\nexport const getListUpdateInfo = async() => {\n  await initListUpdateInfo()\n  return listUpdateInfo\n}\nexport const saveListUpdateInfo = async(info: LX.List.ListUpdateInfo) => {\n  await initListUpdateInfo()\n  listUpdateInfo = info\n  saveListUpdateInfoThrottle()\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  saveListUpdateInfoThrottle()\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  saveListUpdateInfoThrottle()\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  delete listUpdateInfo[id]\n  saveListUpdateInfoThrottle()\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  saveListUpdateInfoThrottle()\n}\n\nlet ignoreVersion: string | null\nexport const saveIgnoreVersion = (version: string | null) => {\n  ignoreVersion = version\n  if (version == null) {\n    void removeData(ignoreVersionKey)\n  } else {\n    void saveData(ignoreVersionKey, version)\n  }\n}\n// 获取忽略更新的版本号\nexport const getIgnoreVersion = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  if (ignoreVersion === undefined) ignoreVersion = (await getData<string | null>(ignoreVersionKey)) ?? null\n  return ignoreVersion\n}\n\nlet ignoreVersionFailTipTime: number | null\nexport const saveIgnoreVersionFailTipTime = (time: number | null) => {\n  ignoreVersionFailTipTime = time\n  if (time == null) {\n    void removeData(ignoreVersionFailTipTimeKey)\n  } else {\n    void saveData(ignoreVersionFailTipTimeKey, time)\n  }\n}\n// 获取忽略更新的版本号\nexport const getIgnoreVersionFailTipTime = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  if (ignoreVersionFailTipTime === undefined) ignoreVersionFailTipTime = (await getData<number | null>(ignoreVersionFailTipTimeKey))\n  return ignoreVersionFailTipTime ?? 0\n}\n\nlet openStoragePath: string | null = ''\nexport const saveOpenStoragePath = async(path: string) => {\n  if (path) {\n    openStoragePath = path\n    await saveData(openStoragePathPrefix, path)\n  } else {\n    if (!openStoragePath) return\n    openStoragePath = null\n    await removeData(openStoragePathPrefix)\n  }\n}\n// 获取上次打开的存储路径\nexport const getOpenStoragePath = async() => {\n  if (openStoragePath === '') {\n    // eslint-disable-next-line require-atomic-updates\n    openStoragePath = await getData<string | null>(openStoragePathPrefix)\n  }\n  return openStoragePath\n}\n\nexport const getSearchSetting = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  searchSetting ??= await getData(searchSettingKey) ?? { ...DEFAULT_SETTING.search }\n  return { ...searchSetting }\n}\nexport const saveSearchSetting = 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 getSearchHistory = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  searchHistoryList ??= await getData(searchHistoryListKey) ?? []\n  return [...searchHistoryList]\n}\nexport const saveSearchHistory = async(historyList: typeof searchHistoryList) => {\n  // if (!searchHistoryList) await getSearchHistory()\n  searchHistoryList = historyList\n  saveSearchHistoryThrottle()\n}\n\nexport const getSongListSetting = async() => {\n  // eslint-disable-next-line require-atomic-updates\n  songListSetting ??= await getData(songListSettingKey) ?? { ...DEFAULT_SETTING.songList }\n  return { ...songListSetting }\n}\nexport const saveSongListSetting = 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 getData(leaderboardSettingKey) ?? { ...DEFAULT_SETTING.leaderboard }\n  return { ...leaderboardSetting }\n}\nexport const saveLeaderboardSetting = async(setting: Partial<typeof DEFAULT_SETTING['leaderboard']>) => {\n  if (!leaderboardSetting) await getLeaderboardSetting()\n  leaderboardSetting = Object.assign(leaderboardSetting, setting)\n  saveLeaderboardSettingThrottle()\n}\n\nexport const getViewPrevState = async() => {\n  return (await getData<{ id: NAV_ID_Type }>(viewPrevStateKey)) ?? { ...DEFAULT_SETTING.viewPrevState }\n}\nexport const saveViewPrevState = (state: { id: NAV_ID_Type }) => {\n  saveViewPrevStateThrottle(state)\n}\n\n\nconst idFixRxp = /\\.0$/\n/**\n * 获取用户列表\n */\nexport const getUserLists = async(): Promise<LX.List.UserListInfo[]> => {\n  const list = await getData<LX.List.UserListInfo[]>(userListKey) ?? []\n  for (const info of list) {\n    // 兼容v2.3.0之前版本PC端插入数字类型的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 listInfo\n */\nexport const saveUserList = async(listInfo: LX.List.UserListInfo[]) => {\n  await saveData(userListKey, listInfo)\n}\n\n/**\n * 获取列表内歌曲\n * @param listId 列表id\n * @returns\n */\nexport const getListMusics = async(listId: string): Promise<LX.Music.MusicInfo[]> => {\n  const list = await getData<LX.Music.MusicInfo[]>(listPrefix + listId)\n  return list ?? []\n}\n\n/**\n * 保存列表内歌曲\n * @param listData 列表数据\n */\nexport const saveListMusics = async(listData: Array<{ id: string, musics: LX.Music.MusicInfo[] }>) => {\n  if (listData.length > 1) {\n    await saveDataMultiple(listData.map(list => ([listPrefix + list.id, list.musics])))\n  } else {\n    const list = listData[0]\n    await saveData(listPrefix + list.id, list.musics)\n  }\n}\n\n/**\n * 移除歌曲列表\n * @param ids\n */\nexport const removeListMusics = async(ids: string[]): Promise<void> => {\n  if (ids.length > 1) {\n    await removeDataMultiple(ids.map(id => {\n      // delete global.lx.listScrollPosition[id]\n      // delete global.lx.listSort[id]\n      return listPrefix + id\n    }))\n  } else {\n    await removeData(listPrefix + ids[0])\n  }\n  // await saveData(listSortPrefix, global.lx.listSort)\n  // delaySaveListScrollPosition(global.lx.listScrollPosition)\n}\n\n\nexport const getMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality) => getData<string>(`${storageDataPrefix.musicUrl}${musicInfo.id}_${type}`).then((url) => url ?? '')\nexport const saveMusicUrl = async(musicInfo: LX.Music.MusicInfo, type: LX.Quality, url: string) => saveData(`${storageDataPrefix.musicUrl}${musicInfo.id}_${type}`, url)\nexport const clearMusicUrl = async(keys?: string[]) => {\n  if (!keys) keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.musicUrl))\n  await removeDataMultiple(keys)\n}\n\nexport const getLyric = async(musicInfo: LX.Music.MusicInfo) => getData<LX.Music.LyricInfo>(`${storageDataPrefix.lyric}${musicInfo.id}`).then(lrcInfo => lrcInfo ?? { lyric: '' })\nexport const saveLyric = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo) => saveData(`${storageDataPrefix.lyric}${musicInfo.id}`, lyricInfo)\nexport const clearLyric = async(keys?: string[]) => {\n  if (!keys) keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.lyric))\n  await removeDataMultiple(keys)\n}\nexport const saveEditedLyric = async(musicInfo: LX.Music.MusicInfo, lyricInfo: LX.Music.LyricInfo) => saveData(`${storageDataPrefix.lyric}${musicInfo.id}_edited`, lyricInfo)\nexport const clearEditedLyric = async() => {\n  let keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.lyric) && key.endsWith('_edited'))\n  await removeDataMultiple(keys)\n}\nexport const getPlayerLyric = async(musicInfo: LX.Music.MusicInfo): Promise<LX.Player.LyricInfo> => {\n  return getDataMultiple([\n    `${storageDataPrefix.lyric}${musicInfo.id}`,\n    `${storageDataPrefix.lyric}${musicInfo.id}_edited`,\n  ]).then(([lrcInfo, lrcInfo_edited]) => {\n    const lyricInfo: LX.Music.LyricInfo = lrcInfo_edited[1] as LX.Music.LyricInfo | null ?? {\n      lyric: '',\n    }\n    let rawLyricInfo: LX.Music.LyricInfo = lrcInfo[1] as LX.Music.LyricInfo | null ?? {\n      lyric: '',\n    }\n    return lyricInfo.lyric ? {\n      ...lyricInfo,\n      rawlrcInfo: rawLyricInfo,\n    } : {\n      ...rawLyricInfo,\n      rawlrcInfo: rawLyricInfo,\n    }\n  })\n}\n\nexport const getOtherSource = async(id: string) => getData<LX.Music.MusicInfoOnline[]>(`${storageDataPrefix.musicOtherSource}${id}`).then((url) => url ?? [])\nexport const saveOtherSource = async(id: string, sourceInfo: LX.Music.MusicInfoOnline[]) => saveData(`${storageDataPrefix.musicOtherSource}${id}`, sourceInfo)\nexport const clearOtherSource = async(keys?: string[]) => {\n  if (!keys) keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.musicOtherSource))\n  await removeDataMultiple(keys)\n}\n\n/**\n * 获取不喜欢列表信息\n * @returns 不喜欢列表信息\n */\nexport const getDislikeListRules = async() => {\n  return await getData<string>(dislikeListPrefix) ?? ''\n}\n/**\n * 保存列表信息\n * @param rules 规则信息\n */\nexport const saveDislikeListRules = async(rules: string) => {\n  await saveData(dislikeListPrefix, rules)\n}\n\n// export const clearMusicUrlAndLyric = async() => {\n//   let keys = (await getAllKeys()).filter(key => key.startsWith(storageDataPrefix.musicUrl) || key.startsWith(storageDataPrefix.lyric))\n//   await removeDataMultiple(keys)\n// }\n\nexport const getMetaCache = async() => {\n  const keys = await getAllKeys()\n  const info = {\n    otherSourceKeys: [] as string[],\n    // musicUrlKeys: [] as string[],\n    lyricKeys: [] as string[],\n  }\n\n  for (const key of keys) {\n    if (key.startsWith(storageDataPrefix.musicOtherSource)) info.otherSourceKeys.push(key)\n    else if (key.startsWith(storageDataPrefix.lyric)) info.lyricKeys.push(key)\n  }\n\n  return info\n}\n\nexport const savePlayInfo = async(playInfo: LX.Player.SavedPlayInfo) => {\n  return saveData(playInfoStorageKey, playInfo)\n}\n// 获取上次关闭时的当前歌曲播放信息\nexport const getPlayInfo = async() => {\n  return getData<LX.Player.SavedPlayInfo | null>(playInfoStorageKey)\n}\n\nlet selectedManagedFolder: string | null = ''\nexport const setSelectedManagedFolder = async(uri: string) => {\n  selectedManagedFolder = uri\n  return saveData(selectedManagedFolderPrefix, uri)\n}\nexport const getSelectedManagedFolder = async() => {\n  if (selectedManagedFolder != '') return selectedManagedFolder\n  let uri = await getData<string>(selectedManagedFolderPrefix)\n  if (selectedManagedFolder != uri) selectedManagedFolder = uri\n  return selectedManagedFolder\n}\n\nexport const getSyncAuthKey = async(serverId: string) => {\n  const keys = await getData<Record<string, LX.Sync.KeyInfo>>(syncAuthKeyPrefix)\n  if (!keys) return null\n  return keys[serverId] ?? null\n}\nexport const setSyncAuthKey = async(serverId: string, info: LX.Sync.KeyInfo) => {\n  let keys = await getData<Record<string, LX.Sync.KeyInfo>>(syncAuthKeyPrefix) ?? {}\n  keys[serverId] = info\n  await saveData(syncAuthKeyPrefix, keys)\n}\n\nlet syncHostInfo: string\nexport const getSyncHost = async() => {\n  if (syncHostInfo === undefined) {\n    // eslint-disable-next-line require-atomic-updates\n    syncHostInfo = await getData(syncHostPrefix) ?? ''\n\n    // 清空1.0.0之前版本的同步主机\n    if (typeof syncHostInfo == 'object') syncHostInfo = ''\n  }\n  return syncHostInfo\n}\nexport const setSyncHost = async(host: string) => {\n  // let hostInfo = await getData(syncHostPrefix) || {}\n  // hostInfo.host = host\n  // hostInfo.port = port\n  syncHostInfo = host\n  await saveData(syncHostPrefix, syncHostInfo)\n}\nlet syncHostHistory: string[]\nexport const getSyncHostHistory = async() => {\n  if (syncHostHistory === undefined) {\n    // eslint-disable-next-line require-atomic-updates\n    syncHostHistory = await getData(syncHostHistoryPrefix) ?? []\n\n    // 清空1.0.0之前版本的同步历史\n    if (syncHostHistory.length && typeof syncHostHistory[0] !== 'string') syncHostHistory = []\n  }\n  return syncHostHistory\n}\nexport 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  await saveData(syncHostHistoryPrefix, syncHostHistory)\n}\nexport const removeSyncHostHistory = async(index: number) => {\n  syncHostHistory.splice(index, 1)\n  await saveData(syncHostHistoryPrefix, syncHostHistory)\n}\n\nlet userApis: LX.UserApi.UserApiInfo[] = []\nexport const getUserApiList = async(): Promise<LX.UserApi.UserApiInfo[]> => {\n  userApis = await getData<LX.UserApi.UserApiInfo[]>(userApiPrefix) ?? []\n\n  // 移除 1.7.1 及之前版本的脚本数据被意外存储到列表中的问题\n  let updated = false\n  for (const info of userApis) {\n    if ((info as LX.UserApi.UserApiInfo & { script?: string }).script != null) {\n      delete (info as LX.UserApi.UserApiInfo & { script?: string }).script\n      updated = true\n    }\n  }\n  if (updated) void saveData(userApiPrefix, userApis)\n\n  return [...userApis]\n}\nexport const getUserApiScript = async(id: string): Promise<string> => {\n  const script = await getData<string>(`${userApiPrefix}${id}`) ?? ''\n  return script\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}\nexport const addUserApi = async(script: string): Promise<LX.UserApi.UserApiInfo> => {\n  const result = /^\\/\\*[\\S|\\s]+?\\*\\//.exec(script)\n  if (!result) throw new Error(global.i18n.t('user_api_add_failed_tip'))\n\n  let scriptInfo = matchInfo(result[0])\n\n  scriptInfo.name ||= `user_api_${new Date().toLocaleString()}`\n  const apiInfo: LX.UserApi.UserApiInfo = {\n    id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,\n    ...scriptInfo,\n    allowShowUpdateAlert: true,\n  }\n  userApis.push(apiInfo)\n  await saveDataMultiple([\n    [userApiPrefix, userApis],\n    [`${userApiPrefix}${apiInfo.id}`, script],\n  ])\n  return apiInfo\n}\nexport const removeUserApi = async(ids: string[]) => {\n  if (!userApis) return []\n  const _ids: string[] = []\n  for (let index = userApis.length - 1; index > -1; index--) {\n    if (ids.includes(userApis[index].id)) {\n      _ids.push(`${userApiPrefix}${userApis[index].id}`)\n      userApis.splice(index, 1)\n      ids.splice(index, 1)\n    }\n  }\n  await saveData(userApiPrefix, userApis)\n  if (_ids.length) await removeDataMultiple(_ids)\n  return [...userApis]\n}\nexport const setUserApiAllowShowUpdateAlert = async(id: string, enable: boolean) => {\n  const targetApi = userApis?.find(api => api.id == id)\n  if (!targetApi) return\n  targetApi.allowShowUpdateAlert = enable\n  await saveData(userApiPrefix, userApis)\n}\n"
  },
  {
    "path": "src/utils/dislikeManage.ts",
    "content": "import { SPLIT_CHAR } from '@/config/constant'\nimport { getDislikeListRules } from './data'\n\nconst dislikeInfo: LX.Dislike.DislikeInfo = {\n  names: new Set(),\n  musicNames: new Set(),\n  singerNames: new Set(),\n  rules: '',\n}\n\nexport const getDislikeInfo = async() => {\n  updateDislikeInfo(await getDislikeListRules())\n  return dislikeInfo\n}\n\nconst updateDislikeInfo = (rules: string) => {\n  dislikeInfo.names.clear()\n  dislikeInfo.musicNames.clear()\n  dislikeInfo.singerNames.clear()\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        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}\n\nexport const addDislikeInfo = async(infos: LX.Dislike.DislikeMusicInfo[]) => {\n  updateDislikeInfo(dislikeInfo.rules + '\\n' + infos.map(info => `${info.name ?? ''}${SPLIT_CHAR.DISLIKE_NAME}${info.singer ?? ''}`).join('\\n'))\n  return dislikeInfo\n}\n\nexport const overwirteDislikeInfo = async(rules: string) => {\n  updateDislikeInfo(rules)\n  return dislikeInfo\n}\n\nexport const clearDislikeInfo = async() => {\n  updateDislikeInfo('')\n  return dislikeInfo\n}\n"
  },
  {
    "path": "src/utils/errorHandle.ts",
    "content": "import { Alert } from 'react-native'\n// import { exitApp } from '@/utils/common'\nimport { setJSExceptionHandler, setNativeExceptionHandler } from 'react-native-exception-handler'\nimport { log } from '@/utils/log'\nimport { toast } from './tools'\n\nconst errorHandler = (e: Error, isFatal: boolean) => {\n  const excludedErrors = [\n    'Failed to construct \\'Response\\'',\n  ]\n  if (isFatal) {\n    if (excludedErrors.includes(e.message)) {\n      toast('应用遇到了错误，如果你有固定的重现方式，请截图并在 GitHub 反馈（并附上刚才你进行了什么操作，以及“设置-错误日志”的内容）')\n    } else {\n      Alert.alert(\n        '💥Unexpected error occurred💥',\n        `\n  应用出 bug 了😭，以下是错误异常信息。请截图并在 GitHub 反馈（并附上刚才你进行了什么操作，以及附上“设置-错误日志”的内容）。现在应用可能会出现异常，若出现异常请尝试强制结束应用后重新启动！\n\n  Error:\n  ${isFatal ? 'Fatal:' : ''} ${e.name} ${e.message}\n  `,\n        [{\n          text: '关闭 (Close)',\n          onPress: () => {\n            // exitApp()\n          },\n        }],\n      )\n    }\n  }\n  log.error(e.stack)\n}\n\nif (process.env.NODE_ENV !== 'development') {\n  setJSExceptionHandler(errorHandler)\n\n  setNativeExceptionHandler((errorString) => {\n    log.error(errorString)\n    console.log('+++++', errorString, '+++++')\n  }, false)\n}\n"
  },
  {
    "path": "src/utils/fs.ts",
    "content": "import RNFS from 'react-native-fs'\nimport {\n  Dirs,\n  FileSystem,\n  AndroidScoped,\n  type OpenDocumentOptions,\n  type Encoding,\n  type HashAlgorithm,\n  getExternalStoragePaths as _getExternalStoragePaths,\n} from 'react-native-file-system'\n\nexport type {\n  FileType,\n} from 'react-native-file-system'\n\n// export const externalDirectoryPath = RNFS.ExternalDirectoryPath\n\nexport const extname = (name: string) => name.lastIndexOf('.') > 0 ? name.substring(name.lastIndexOf('.') + 1) : ''\n\nexport const temporaryDirectoryPath = Dirs.CacheDir\nexport const externalStorageDirectoryPath = Dirs.SDCardDir\nexport const privateStorageDirectoryPath = Dirs.DocumentDir\n\nexport const getExternalStoragePaths = async(is_removable?: boolean) => _getExternalStoragePaths(is_removable)\n\nexport const selectManagedFolder = async(isPersist: boolean = false) => AndroidScoped.openDocumentTree(isPersist)\nexport const selectFile = async(options: OpenDocumentOptions) => AndroidScoped.openDocument(options)\nexport const removeManagedFolder = async(path: string) => AndroidScoped.releasePersistableUriPermission(path)\nexport const getManagedFolders = async() => AndroidScoped.getPersistedUriPermissions()\n\nexport const getPersistedUriList = async() => AndroidScoped.getPersistedUriPermissions()\n\n\nexport const readDir = async(path: string) => FileSystem.ls(path)\n\nexport const unlink = async(path: string) => FileSystem.unlink(path)\n\nexport const mkdir = async(path: string) => FileSystem.mkdir(path)\n\nexport const stat = async(path: string) => FileSystem.stat(path)\nexport const hash = async(path: string, algorithm: HashAlgorithm) => FileSystem.hash(path, algorithm)\n\nexport const readFile = async(path: string, encoding?: Encoding) => FileSystem.readFile(path, encoding)\n\n\n// export const copyFile = async(fromPath: string, toPath: string) => FileSystem.cp(fromPath, toPath)\n\nexport const moveFile = async(fromPath: string, toPath: string) => FileSystem.mv(fromPath, toPath)\nexport const gzipFile = async(fromPath: string, toPath: string) => FileSystem.gzipFile(fromPath, toPath)\nexport const unGzipFile = async(fromPath: string, toPath: string) => FileSystem.unGzipFile(fromPath, toPath)\nexport const gzipString = async(data: string, encoding?: Encoding) => FileSystem.gzipString(data, encoding)\nexport const unGzipString = async(data: string, encoding?: Encoding) => FileSystem.unGzipString(data, encoding)\n\nexport const existsFile = async(path: string) => FileSystem.exists(path)\n\nexport const rename = async(path: string, name: string) => FileSystem.rename(path, name)\n\nexport const writeFile = async(path: string, data: string, encoding?: Encoding) => FileSystem.writeFile(path, data, encoding)\n\nexport const appendFile = async(path: string, data: string, encoding?: Encoding) => FileSystem.appendFile(path, data, encoding)\n\nexport const downloadFile = (url: string, path: string, options: Omit<RNFS.DownloadFileOptions, 'fromUrl' | 'toFile'> = {}) => {\n  if (!options.headers) {\n    options.headers = {\n      'User-Agent': 'Mozilla/5.0 (Linux; Android 10; Pixel 3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Mobile Safari/537.36',\n    }\n  }\n  return RNFS.downloadFile({\n    fromUrl: url, // URL to download file from\n    toFile: path, // Local filesystem path to save the file to\n    ...options,\n    // headers: options.headers, // An object of headers to be passed to the server\n    // // background?: boolean;     // Continue the download in the background after the app terminates (iOS only)\n    // // discretionary?: boolean;  // Allow the OS to control the timing and speed of the download to improve perceived performance  (iOS only)\n    // // cacheable?: boolean;      // Whether the download can be stored in the shared NSURLCache (iOS only, defaults to true)\n    // progressInterval: options.progressInterval,\n    // progressDivider: options.progressDivider,\n    // begin: (res: DownloadBeginCallbackResult) => void;\n    // progress?: (res: DownloadProgressCallbackResult) => void;\n    // // resumable?: () => void;    // only supported on iOS yet\n    // connectionTimeout?: number // only supported on Android yet\n    // readTimeout?: number       // supported on Android and iOS\n    // // backgroundTimeout?: number // Maximum time (in milliseconds) to download an entire resource (iOS only, useful for timing out background downloads)\n  })\n}\n\nexport const stopDownload = (jobId: number) => {\n  RNFS.stopDownload(jobId)\n}\n"
  },
  {
    "path": "src/utils/hooks/index.js",
    "content": "export { default as useLayout } from './useLayout'\nexport { default as useKeyboard } from './useKeyboard'\nexport { default as useWindowSize } from './useWindowSize'\nexport { default as useHorizontalMode } from './useHorizontalMode'\nexport { default as useDeviceOrientation } from './useDeviceOrientation'\n\n// export { default as usePlayTime } from './usePlayTime'\nexport { default as useAssertApiSupport } from './useAssertApiSupport'\nexport { useDrag } from './useDrag'\nexport { useUnmounted } from './useUnmounted'\n"
  },
  {
    "path": "src/utils/hooks/useAnimateColor.ts",
    "content": "import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Animated } from 'react-native'\n\n\nconst ANIMATION_DURATION = 800\n\nexport const useAnimateColor = (color: string) => {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const anim = useMemo(() => new Animated.Value(0), [color])\n  const [finished, setFinished] = useState(true)\n  const currentColor = useRef(color)\n  const nextColor = useMemo(() => color, [color])\n\n  const animColor = anim.interpolate({\n    inputRange: [0, 1],\n    outputRange: [currentColor.current, nextColor],\n  })\n\n  useEffect(() => {\n    setFinished(false)\n    Animated.timing(anim, {\n      toValue: 1,\n      duration: ANIMATION_DURATION,\n      useNativeDriver: false,\n    }).start((finished) => {\n      if (!finished) return\n      // currentColor.current = nextColor\n      setFinished(true)\n    })\n    requestAnimationFrame(() => {\n      currentColor.current = nextColor\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [nextColor])\n\n  return [animColor, finished] as const\n}\n"
  },
  {
    "path": "src/utils/hooks/useAnimateNumber.ts",
    "content": "import { useEffect, useMemo, useRef, useState } from 'react'\nimport { Animated } from 'react-native'\n\n\nexport const DEFAULT_DURATION = 800\n\nexport const useAnimateNumber = (val: number, duration = DEFAULT_DURATION, useNativeDriver = true) => {\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const anim = useMemo(() => new Animated.Value(0), [val])\n  const [finished, setFinished] = useState(true)\n  const currentNumber = useRef(val)\n  const nextNumber = useMemo(() => val, [val])\n\n  const animNumber = anim.interpolate({\n    inputRange: [0, 1],\n    outputRange: [currentNumber.current, nextNumber],\n  })\n\n  useEffect(() => {\n    setFinished(false)\n    Animated.timing(anim, {\n      toValue: 1,\n      duration,\n      useNativeDriver,\n    }).start((finished) => {\n      if (!finished) return\n      // currentNumber.current = nextNumber\n      setFinished(true)\n    })\n    requestAnimationFrame(() => {\n      currentNumber.current = nextNumber\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [nextNumber])\n\n  return [animNumber, finished] as const\n}\n\nexport const useAnimateOnecNumber = (val: number, toVal: number, duration = DEFAULT_DURATION, useNativeDriver = true) => {\n  const anim = useMemo(() => new Animated.Value(0), [])\n  const [finished, setFinished] = useState(true)\n\n  const animNumber = anim.interpolate({\n    inputRange: [0, 1],\n    outputRange: [val, toVal],\n  })\n\n  useEffect(() => {\n    setFinished(false)\n    Animated.timing(anim, {\n      toValue: 1,\n      duration,\n      useNativeDriver,\n    }).start((finished) => {\n      if (!finished) return\n      // currentNumber.current = nextNumber\n      setFinished(true)\n    })\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return [animNumber, finished] as const\n}\n"
  },
  {
    "path": "src/utils/hooks/useAssertApiSupport.js",
    "content": "import { useGetter } from '@/store'\n\nexport default source => {\n  const supportQualitys = useGetter('common', 'supportQualitys')\n\n  return Boolean(supportQualitys[source])\n}\n"
  },
  {
    "path": "src/utils/hooks/useBackHandler.ts",
    "content": "import { useEffect } from 'react'\nimport { BackHandler } from 'react-native'\n\nexport function useBackHandler(handler: () => boolean) {\n  useEffect(() => {\n    BackHandler.addEventListener('hardwareBackPress', handler)\n\n    return () => {\n      BackHandler.removeEventListener('hardwareBackPress', handler)\n    }\n  }, [handler])\n}\n"
  },
  {
    "path": "src/utils/hooks/useDeviceOrientation.js",
    "content": "import { useEffect, useState, useCallback } from 'react'\nimport { windowSizeTools } from '../windowSizeTools'\n\n\nexport default () => {\n  const isOrientationPortrait = ({\n    width,\n    height,\n  }) => height >= width\n  const isOrientationLandscape = ({\n    width,\n    height,\n  }) => width >= height\n\n  const size = windowSizeTools.getSize()\n  const [orientation, setOrientation] = useState({\n    portrait: isOrientationPortrait(size),\n    landscape: isOrientationLandscape(size),\n  })\n\n  const onChange = useCallback((size) => {\n    setOrientation({\n      portrait: isOrientationPortrait(size),\n      landscape: isOrientationLandscape(size),\n    })\n  }, [])\n\n  useEffect(() => {\n    const changeEvent = windowSizeTools.onSizeChanged(onChange)\n\n    return () => {\n      changeEvent.remove()\n    }\n  }, [orientation.portrait, orientation.landscape, onChange])\n\n  return orientation\n}\n"
  },
  {
    "path": "src/utils/hooks/useDrag.ts",
    "content": "import { useCallback, useRef } from 'react'\nimport { type LayoutChangeEvent } from 'react-native'\n\nexport const useDrag = (onSetProgress: (progress: number) => void, onDragState: (drag: boolean) => void, setDragProgress: (progress: number) => void) => {\n  const info = useRef({\n    isDraging: false,\n    dragStartX: 0,\n    dragStartProgress: 0,\n    dragProgress: 0,\n    progressWidth: 0,\n  })\n\n  const onDragStart = useCallback((offsetX: number, locationX: number) => {\n    info.current.isDraging = true\n    info.current.dragStartX = offsetX\n\n    let val = locationX / info.current.progressWidth\n    if (val < 0) val = 0\n    if (val > 1) val = 1\n\n    setDragProgress(info.current.dragStartProgress = info.current.dragProgress = val)\n    // dragProgress.value = msEvent.msDownProgress = val\n    onDragState(true)\n  }, [onDragState, setDragProgress])\n  const onDragEnd = useCallback(() => {\n    if (info.current.isDraging) onSetProgress(info.current.dragProgress)\n    info.current.isDraging = false\n    onDragState(false)\n  }, [onDragState, onSetProgress])\n  const onDrag = useCallback((offsetX: number) => {\n    if (!info.current.isDraging) return\n    // dragging.value ||= true\n\n    let progress = info.current.dragStartProgress + (offsetX - info.current.dragStartX) / info.current.progressWidth\n    if (progress > 1) progress = 1\n    else if (progress < 0) progress = 0\n    setDragProgress(info.current.dragProgress = progress)\n  }, [setDragProgress])\n\n  const onLayout = useCallback((e: LayoutChangeEvent) => {\n    info.current.progressWidth = e.nativeEvent.layout.width\n  }, [])\n\n  // const onPress = useCallback((locationX: number) => {\n  //   onSetProgress(locationX / info.current.progressWidth)\n  // }, [onSetProgress])\n\n  return {\n    onLayout,\n    onDragStart,\n    onDragEnd,\n    onDrag,\n  }\n}\n"
  },
  {
    "path": "src/utils/hooks/useHorizontalMode.ts",
    "content": "import { useWindowSize } from '@/utils/hooks'\nimport { isHorizontalMode } from '../tools'\n\n\nexport default () => {\n  const windowSize = useWindowSize()\n\n  return isHorizontalMode(windowSize.width, windowSize.height)\n}\n"
  },
  {
    "path": "src/utils/hooks/useKeyboard.js",
    "content": "import { useEffect, useState } from 'react'\nimport { Keyboard } from 'react-native'\n\nexport default () => {\n  const [shown, setShown] = useState(false)\n  const [keyboardHeight, setKeyboardHeight] = useState(0)\n\n  const handleKeyboardDidShow = e => {\n    // const isShow = e.endCoordinates.height > 115\n    // setShown(isShow)\n    // setKeyboardHeight(isShow ? e.endCoordinates.height : 0)\n    setShown(true)\n    setKeyboardHeight(e.endCoordinates.height)\n  }\n\n  const handleKeyboardDidHide = () => {\n    setShown(false)\n    setKeyboardHeight(0)\n  }\n\n  useEffect(() => {\n    const keyboardDidShow = Keyboard.addListener('keyboardDidShow', handleKeyboardDidShow)\n    const keyboardDidHide = Keyboard.addListener('keyboardDidHide', handleKeyboardDidHide)\n\n    return () => {\n      keyboardDidShow.remove()\n      keyboardDidHide.remove()\n    }\n  }, [])\n\n  return {\n    keyboardShown: shown,\n    keyboardHeight,\n  }\n}\n"
  },
  {
    "path": "src/utils/hooks/useLayout.tsx",
    "content": "import { useState, useCallback } from 'react'\nimport { type LayoutChangeEvent } from 'react-native'\n\nexport default () => {\n  const [layout, setLayout] = useState({\n    x: 0,\n    y: 0,\n    width: 0,\n    height: 0,\n  })\n  const onLayout = useCallback((e: LayoutChangeEvent) => { setLayout(e.nativeEvent.layout) }, [])\n\n  return {\n    onLayout,\n    ...layout,\n  }\n}\n"
  },
  {
    "path": "src/utils/hooks/usePlayTime.js",
    "content": "// import { useMemo } from 'react'\n// import { useProgress } from '@/plugins/player/utils'\n// import { formatPlayTime2 } from '@/utils'\n// // import { useGetter } from '@/store'\n// // import { STATE_PLAYING, STATE_BUFFERING } from 'react-native-track-player'\n\n// export default () => {\n//   const { position, buffered, duration } = useProgress(250)\n//   // const isGettingUrl = useGetter('player', 'isGettingUrl')\n\n//   return {\n//     curTimeStr: formatPlayTime2(position),\n//     maxTimeStr: formatPlayTime2(duration),\n//     time: position,\n//     duration,\n//     bufferedProgress: duration ? buffered / duration * 100 : 100,\n//     progress: duration ? position / duration * 100 : 0,\n//   }\n// }\n"
  },
  {
    "path": "src/utils/hooks/useUnmounted.tsx",
    "content": "import { useEffect, useRef } from 'react'\n\nexport function useUnmounted() {\n  const isUnmountedRef = useRef(false)\n  useEffect(() => {\n    isUnmountedRef.current = false\n    return () => {\n      isUnmountedRef.current = true\n    }\n  }, [])\n\n  return isUnmountedRef\n}\n"
  },
  {
    "path": "src/utils/hooks/useWindowSize.ts",
    "content": "import { useEffect, useState } from 'react'\nimport { type SizeHandler, windowSizeTools } from '@/utils/windowSizeTools'\n\nexport default () => {\n  const [size, setSize] = useState(windowSizeTools.getSize())\n\n  useEffect(() => {\n    const onChange: SizeHandler = (size) => {\n      setSize(size)\n    }\n\n    const remove = windowSizeTools.onSizeChanged(onChange)\n    return () => {\n      remove()\n    }\n  }, [])\n\n  return size\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "import { dateFormat } from './common'\nimport he from 'he'\n\nexport { tranditionalize as langS2T } from '@/utils/simplify-chinese-main'\n\nexport * from './common'\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 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 as string}_${oldMusicInfo.songmid as string}`,\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 as string)?.[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.albumMid = oldMusicInfo.albumMid\n        meta.id = oldMusicInfo.songId\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): any => {\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\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\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\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 global.i18n.t('date_format_second', { num: differ })\n  } else if (differ < 3600) {\n    return global.i18n.t('date_format_minute', { num: Math.trunc(differ / 60) })\n  } else if (differ < 86400) {\n    return global.i18n.t('date_format_hour', { num: Math.trunc(differ / 3600) })\n  } else {\n    return dateFormat(time)\n  }\n}\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\nexport const decodeName = (str: string | null = '') => {\n  if (!str) return ''\n  return he.decode(str)\n}\n"
  },
  {
    "path": "src/utils/listManage.ts",
    "content": "import {\n  getUserLists as getUserListsFromStore,\n  getListMusics as getListMusicsFromStore,\n  // saveListMusics as saveListMusicsFromStore,\n  // removeListMusics as removeListMusicsFromStore,\n  overwriteListPosition,\n  overwriteListUpdateInfo,\n  removeListPosition,\n  removeListUpdateInfo,\n} from '@/utils/data'\nimport { arrPush, arrPushByPosition, arrUnshift } from '@/utils/common'\nimport { LIST_IDS } from '@/config/constant'\n\nexport const userLists: LX.List.UserListInfo[] = []\nexport const allMusicList = new Map<string, LX.Music.MusicInfo[]>()\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[]): LX.Music.MusicInfo[] => {\n  allMusicList.set(listId, musicList)\n  return musicList\n}\nexport const removeMusicList = (id: string) => {\n  allMusicList.delete(id)\n}\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 index\n  switch (id) {\n    case LIST_IDS.DEFAULT:\n    case LIST_IDS.LOVE:\n      break\n    case LIST_IDS.TEMP:\n    //   tempList.meta = meta ?? {}\n      // break\n    default:\n      index = userLists.findIndex(l => l.id == id)\n      if (index < 0) return\n      userLists.splice(index, 1, { ...userLists[index], name, source, sourceListId, 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/**\n * 获取用户列表\n * @returns 所有用户列表\n */\nexport const getUserLists = async() => {\n  const lists = await getUserListsFromStore()\n  return setUserLists(lists)\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    if (allMusicList.has(listInfo.id)) updatedListIds.push(listInfo.id)\n    newUserIds.push(listInfo.id)\n    setMusicList(listInfo.id, list)\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)) updatedListIds.push(LIST_IDS.DEFAULT)\n  setMusicList(LIST_IDS.DEFAULT, defaultList)\n  setMusicList(LIST_IDS.LOVE, loveList)\n  updatedListIds.push(LIST_IDS.LOVE)\n\n  if (tempList && allMusicList.has(LIST_IDS.TEMP)) {\n    setMusicList(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    if (!allMusicList.has(id)) continue\n    removeMusicList(id)\n    void removeListPosition(id)\n    void removeListUpdateInfo(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 getListMusicSync = (id: string | null) => {\n  return id ? allMusicList.get(id) ?? [] : []\n}\n\n/**\n * 获取列表内的歌曲\n * @param listId\n */\nexport const getListMusics = async(listId: string): Promise<LX.Music.MusicInfo[]> => {\n  if (!listId) return []\n  if (allMusicList.has(listId)) return allMusicList.get(listId)!\n  const list = await getListMusicsFromStore(listId)\n  return setMusicList(listId, list)\n}\n\nexport const listMusicOverwrite = async(listId: string, musicInfos: LX.Music.MusicInfo[]): Promise<string[]> => {\n  setMusicList(listId, musicInfos)\n  return [listId]\n}\n\nexport const listMusicAdd = async(id: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): Promise<string[]> => {\n  const targetList = await getListMusics(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    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  setMusicList(id, targetList)\n\n  return [id]\n}\n\nexport const listMusicRemove = async(listId: string, ids: string[]): Promise<string[]> => {\n  let targetList = await getListMusics(listId)\n\n  const listSet = new Set<string>()\n  for (const item of targetList) listSet.add(item.id)\n  for (const id of ids) listSet.delete(id)\n  const newList = targetList.filter(mInfo => listSet.has(mInfo.id))\n  targetList.splice(0, targetList.length)\n  arrPush(targetList, newList)\n\n  return [listId]\n}\n\nexport const listMusicMove = async(fromId: string, toId: string, musicInfos: LX.Music.MusicInfo[], addMusicLocationType: LX.AddMusicLocationType): Promise<string[]> => {\n  return [\n    ...await listMusicRemove(fromId, musicInfos.map(musicInfo => musicInfo.id)),\n    ...await listMusicAdd(toId, musicInfos, addMusicLocationType),\n  ]\n}\n\nexport const listMusicUpdateInfo = async(musicInfos: LX.List.ListActionMusicUpdate): Promise<string[]> => {\n  const updateListIds = new Set<string>()\n  for (const { id, musicInfo } of musicInfos) {\n    const targetList = await getListMusics(id)\n    if (!targetList.length) 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, info)\n    updateListIds.add(id)\n  }\n  return Array.from(updateListIds)\n}\n\n\nexport const listMusicUpdatePosition = async(listId: string, position: number, ids: string[]): Promise<string[]> => {\n  let targetList = await getListMusics(listId)\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 = createSortedList(targetList, position, ids)\n  const infos: LX.Music.MusicInfo[] = []\n  const map = new Map<string, LX.Music.MusicInfo>()\n  for (const item of targetList) map.set(item.id, item)\n  for (const id of ids) {\n    infos.push(map.get(id)!)\n    map.delete(id)\n  }\n  const list = targetList.filter(mInfo => map.has(mInfo.id))\n  arrPushByPosition(list, infos, Math.min(position, list.length))\n\n  targetList.splice(0, targetList.length)\n  arrPush(targetList, list)\n\n  // console.timeEnd('ts')\n  return [listId]\n}\n\n\nexport const listMusicClear = async(ids: string[]): Promise<string[]> => {\n  const changedIds: string[] = []\n  for (const id of ids) {\n    const list = await getListMusics(id)\n    if (!list.length) continue\n    setMusicList(id, [])\n    changedIds.push(id)\n  }\n  return changedIds\n}\n"
  },
  {
    "path": "src/utils/localMediaMetadata.ts",
    "content": "import { temporaryDirectoryPath, readDir, unlink, extname } from '@/utils/fs'\nimport { readPic as _readPic } from 'react-native-local-media-metadata'\nexport {\n  type MusicMetadata,\n  type MusicMetadataFull,\n  readMetadata,\n  writeMetadata,\n  writePic,\n  readLyric,\n  writeLyric,\n} from 'react-native-local-media-metadata'\n\nlet cleared = false\nconst picCachePath = temporaryDirectoryPath + '/local-media-metadata'\n\nexport const scanAudioFiles = async(dirPath: string) => {\n  const files = await readDir(dirPath)\n  return files.filter(file => {\n    if (file.mimeType?.startsWith('audio/')) return true\n    if (extname(file?.name ?? '') === 'ogg') return true\n    return false\n  }).map(file => file)\n}\n\nconst clearPicCache = async() => {\n  await unlink(picCachePath)\n  cleared = true\n}\n\nexport const readPic = async(dirPath: string): Promise<string> => {\n  if (!cleared) await clearPicCache()\n  return _readPic(dirPath, picCachePath)\n}\n\n// export interface MusicMetadata {\n//   type: 'mp3' | 'flac' | 'ogg' | 'wav'\n//   bitrate: string\n//   interval: number\n//   size: number\n//   ext: 'mp3' | 'flac' | 'ogg' | 'wav'\n//   albumName: string\n//   singer: string\n//   name: string\n// }\n// export const readMetadata = async(filePath: string): Promise<MusicMetadata | null> => {\n//   return LocalMediaModule.readMetadata(filePath)\n// }\n\n// export const readPic = async(filePath: string): Promise<string> => {\n//   return LocalMediaModule.readPic(filePath)\n// }\n\n// export const readLyric = async(filePath: string): Promise<string> => {\n//   return LocalMediaModule.readLyric(filePath)\n// }\n\n\n"
  },
  {
    "path": "src/utils/log.ts",
    "content": "// import { requestStoragePermission } from '@/utils/common'\nimport { temporaryDirectoryPath, existsFile, appendFile, unlink, writeFile, readFile } from '@/utils/fs'\n\nconst logPath = temporaryDirectoryPath + '/error.log'\n\nconst logTools = {\n  tempLog: [] as Array<{ time: string, type: 'LOG' | 'WARN' | 'ERROR', text: string }> | null,\n  writeLog(msg: string) {\n    console.log(msg)\n    void appendFile(logPath, '\\n----lx log----\\n' + msg)\n  },\n  async initLogFile() {\n    try {\n      let isExists = await existsFile(logPath)\n      // console.log(isExists)\n      if (!isExists) await writeFile(logPath, '')\n      if (this.tempLog?.length) this.writeLog(this.tempLog.map(m => `${m.time} ${m.type} ${m.text}`).join('\\n----lx log----\\n'))\n      this.tempLog = null\n    } catch (err) {\n      console.log(err)\n    }\n  },\n}\n\nexport const init = async() => {\n  return logTools.initLogFile()\n}\n\nexport const getLogs = async() => {\n  return readFile(logPath)\n}\n\nexport const clearLogs = async() => {\n  return unlink(logPath).then(async() => writeFile(logPath, ''))\n}\n\nexport const log = {\n  info(...msgs: any[]) {\n    // console.info(...msgs)\n    const msg = msgs.map(m => typeof m == 'string' ? m : m instanceof Error ? m.stack ?? m.message : JSON.stringify(m)).join(' ')\n    if (msg.startsWith('%c')) return\n    const time = new Date().toLocaleString()\n    if (logTools.tempLog) {\n      logTools.tempLog.push({ type: 'LOG', time, text: msg })\n    } else logTools.writeLog(`${time} LOG ${msg}`)\n  },\n  warn(...msgs: any[]) {\n    // console.warn(...msgs)\n    const msg = msgs.map(m => typeof m == 'string' ? m : m instanceof Error ? m.stack ?? m.message : JSON.stringify(m)).join(' ')\n    const time = new Date().toLocaleString()\n    if (logTools.tempLog) {\n      logTools.tempLog.push({ type: 'WARN', time, text: msg })\n    } else logTools.writeLog(`${time} WARN ${msg}`)\n  },\n  error(...msgs: any[]) {\n    const msg = msgs.map(m => typeof m == 'string' ? m : m instanceof Error ? m.stack ?? m.message : JSON.stringify(m)).join(' ')\n    const time = new Date().toLocaleString()\n    if (logTools.tempLog) {\n      logTools.tempLog.push({ type: 'ERROR', time, text: msg })\n    } else {\n      logTools.writeLog(`${time} ERROR ${msg}`)\n    }\n  },\n}\n/*\nif (process.env.NODE_ENV !== 'development') {\n  const logPath = externalDirectoryPath + '/debug.log'\n\n  let tempLog = []\n\n  const log = window.console.log\n  const error = window.console.error\n  const warn = window.console.warn\n\n  const writeLog = msg => appendFile(logPath, '\\n' + msg)\n\n  window.console.log = (...msgs) => {\n    log(...msgs)\n    const msg = msgs.map(m => typeof m == 'string' ? m : JSON.stringify(m)).join(' ')\n    if (msg.startsWith('%c')) return\n    const time = new Date().toLocaleString()\n    if (tempLog) {\n      tempLog({ type: 'LOG', time, text: msg })\n    } else writeLog(`${time} LOG ${msg}`)\n  }\n  window.console.error = (...msgs) => {\n    error(...msgs)\n    const msg = msgs.map(m => typeof m == 'string' ? m : JSON.stringify(m)).join(' ')\n    const time = new Date().toLocaleString()\n    if (tempLog) {\n      tempLog({ type: 'ERROR', time, text: msg })\n    } else writeLog(`${time} ERROR ${msg}`)\n  }\n  window.console.warn = (...msgs) => {\n    warn(...msgs)\n    const msg = msgs.map(m => typeof m == 'string' ? m : JSON.stringify(m)).join(' ')\n    const time = new Date().toLocaleString()\n    if (tempLog) {\n      tempLog({ type: 'WARN', time, text: msg })\n    } else writeLog(`${time} WARN ${msg}`)\n  }\n\n  const init = async() => {\n    try {\n      let result = await requestStoragePermission()\n      if (!result) return\n      let isExists = await existsFile(logPath)\n      console.log(logPath, isExists)\n      if (!isExists) await writeFile(logPath, '')\n      writeLog(tempLog(m => `${m.time} ${m.type} ${m.text}`).join('\\n'))\n      tempLog = null\n    } catch (err) {\n      console.error(err)\n    }\n  }\n\n\n  init()\n}\n\n */\n"
  },
  {
    "path": "src/utils/lrcTools.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/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/utils/music.ts",
    "content": "import { existsFile } from './fs'\n\n\nexport const getLocalFilePath = async(musicInfo: LX.Music.MusicInfoLocal): Promise<string> => {\n  if (await existsFile(musicInfo.meta.filePath)) return musicInfo.meta.filePath\n  // 直接从应用外 intent 调用打开的文件，ogg等类型无法判断文件是否存在，但这类文件路径为纯数字\n  return /\\/\\d+$/.test(musicInfo.meta.filePath) ? musicInfo.meta.filePath : ''\n}\n"
  },
  {
    "path": "src/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\nexport default sources\n"
  },
  {
    "path": "src/utils/musicSdk/api-source.js",
    "content": "import apiSourceInfo from './api-source-info'\n\n// import temp_api_kw from './kw/api-temp'\n// import test_api_kg from './kg/api-test'\n// import test_api_kw from './kw/api-test'\n// import test_api_tx from './tx/api-test'\n// import test_api_wy from './wy/api-test'\n// import test_api_mg from './mg/api-test'\n\n// import direct_api_kg from './kg/api-direct'\n// import direct_api_kw from './kw/api-direct'\n// import direct_api_tx from './tx/api-direct'\n// import direct_api_wy from './wy/api-direct'\n// import direct_api_mg from './mg/api-direct'\n\nimport settingState from '@/store/setting/state'\n\n\nconst apiList = {\n  // temp_api_kw,\n  // // test_api_bd: require('./bd/api-test'),\n  // test_api_kg,\n  // test_api_kw,\n  // test_api_tx,\n  // test_api_wy,\n  // test_api_mg,\n  // direct_api_kg,\n  // direct_api_kw,\n  // direct_api_tx,\n  // direct_api_wy,\n  // direct_api_mg,\n  // test_api_tx: require('./tx/api-test'),\n  // test_api_wy: require('./wy/api-test'),\n  // test_api_xm: require('./xm/api-test'),\n}\nconst supportQuality = {}\n\nfor (const api of apiSourceInfo) {\n  supportQuality[api.id] = api.supportQualitys\n  // for (const source of Object.keys(api.supportQualitys)) {\n  //   const path = `./${source}/api-${api.id}`\n  //   console.log(path)\n  //   apiList[`${api.id}_api_${source}`] = path\n  // }\n}\n\nconst getAPI = source => apiList[`${settingState.setting['common.apiSource']}_api_${source}`]\n\nconst apis = source => {\n  if (/^user_api/.test(settingState.setting['common.apiSource'])) return global.lx.apis[source]\n  const api = getAPI(source)\n  if (api) return api\n  throw new Error('Api is not found')\n}\n\nexport { apis, supportQuality }\n"
  },
  {
    "path": "src/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/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/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/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/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/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      tid: 'hot',\n      id: '1',\n    },\n    {\n      name: '最新',\n      tid: 'new',\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/utils/musicSdk/index.js",
    "content": "import kw from './kw'\nimport kg from './kg'\nimport tx from './tx'\nimport wy from './wy'\nimport mg from './mg'\n// import bd from './bd'\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: 'bd',\n    // },\n  ],\n  kw,\n  kg,\n  tx,\n  wy,\n  mg,\n  // bd,\n  xm,\n}\nexport default {\n  ...sources,\n  supportQuality,\n}\n\nexport const 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\n\nexport const searchMusic = async({ 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\nexport const findMusic = async(musicInfo) => {\n  const { name, singer, albumName, interval, source: s } = musicInfo\n\n  const lists = await searchMusic({ name, singer, source: s, limit: 25 })\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"
  },
  {
    "path": "src/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/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      cache: 'default',\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      cache: 'default',\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/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      cache: null,\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/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/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/utils/musicSdk/kg/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeLyric } from './util'\nimport { decodeName } from '../../index'\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\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 decodeLyric(body.content).then(result => parseLyric(result))\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/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/utils/musicSdk/kg/musicSearch.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate } from '../../index'\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 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/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/utils/musicSdk/kg/singer.js",
    "content": "import { httpFetch } from '../../request'\n// import { decodeName, formatPlayTime, sizeFormate } from '../../index'\n// import { signatureParams } from './util'\nimport { getMusicInfosByList } from './musicInfo'\n\nexport default {\n  filterAlbum(rawList) {\n    let returnList = []\n    rawList.forEach((albumInfo) => {\n      returnList.push({\n        name: albumInfo.albumname,\n        author: albumInfo.singername,\n        img: albumInfo.replaceAll('{size}', '480'),\n        album_id: albumInfo.albumid,\n      })\n    })\n  },\n  async getSingerInfo(singerid) {\n    if (singerid == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0\n    const requestObj = httpFetch(`http://mobiles.kugou.com/api/v5/singer/info?singerid=${singerid}`)\n    let { body, statusCode } = await requestObj.promise\n    if (statusCode !== 200) throw new Error('获取歌手信息失败')\n    return {\n      source: 'kg',\n      singerid,\n      info: {\n        name: body.data.singername,\n        desc: body.data.intro,\n        img: body.data.imgurl.replace('{size}', '480'),\n      },\n    }\n  },\n  async getSingerSongList(singerid, page, limit) {\n    if (singerid == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0\n    const requestObj = httpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${singerid}&page=${page}&pagesize=${limit}`)\n    let { body, statusCode } = await requestObj.promise\n    if (statusCode !== 200) throw new Error('获取歌手歌曲列表失败')\n    let listData = await getMusicInfosByList(body.data.info)\n    return {\n      source: 'kg',\n      list: listData,\n      id: `kg__singer_${singerid}`,\n      singerid,\n      total: body.data.total,\n      allPage: Math.ceil(body.data.total / limit),\n    }\n  },\n  async getSingerAlbumList(singerid, page, limit) {\n    if (singerid == 0) throw new Error('歌手不存在') // kg源某些歌曲在歌手没被kg收录时返回的歌手id为0\n    const requestObj = httpFetch(`http://mobiles.kugou.com/api/v5/singer/song?singerid=${singerid}&page=${page}&pagesize=${limit}`)\n    let { body, statusCode } = await requestObj.promise\n    if (statusCode !== 200) throw new Error('获取歌手专辑列表失败')\n    return {\n      source: 'kg',\n      albums: this.filterAlbum(body.data.info),\n      singerid,\n    }\n  },\n}\n"
  },
  {
    "path": "src/utils/musicSdk/kg/songList.js",
    "content": "import { httpFetch } from '../../request'\nimport { decodeName, formatPlayTime, sizeFormate, dateFormat, formatPlayCount } from '../../index'\nimport infSign from './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      tid: 'recommend',\n      id: '5',\n    },\n    {\n      name: '最热',\n      tid: 'hot',\n      id: '6',\n    },\n    {\n      name: '最新',\n      tid: 'new',\n      id: '7',\n    },\n    {\n      name: '热藏',\n      tid: 'hot_collect',\n      id: '3',\n    },\n    {\n      name: '飙升',\n      tid: 'rise',\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    options.cache = 'default'\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 { url: 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.split('?')[0] != link.split('?')[0]) {\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(link, page, ++retryNum)\n    }\n    if (typeof body == 'string') return this.getUserListDetail2(body.replace(/^[\\s\\S]+?\"global_collection_id\":\"(\\w+)\"[\\s\\S]+?$/, '$1'))\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/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/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/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/utils/musicSdk/kg/util.js",
    "content": "import pako from 'pako'\nimport { Buffer } from 'buffer'\nimport { toMD5 } from '../utils'\nimport { httpFetch } from '../../request'\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')\nexport const decodeLyric = str => new Promise((resolve, reject) => {\n  if (!str.length) return\n  const buf_str = Buffer.from(str, 'base64').slice(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  const result = pako.inflate(buf_str, { to: 'string' })\n  resolve(result)\n  // (err, result) => {\n  //   if (err) return reject(err)\n  //   resolve(result.toString())\n  // }\n})\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  options.cache = 'default'\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/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/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/utils/musicSdk/kw/decodeLyric.js",
    "content": "const { inflate } = require('pako')\nconst iconv = require('iconv-lite')\n\nconst handleInflate = data => new Promise((resolve, reject) => {\n  resolve(Buffer.from(inflate(data)))\n})\n\nconst buf_key = Buffer.from('yeelion')\nconst buf_key_len = buf_key.length\n\nconst decodeLyric = async(buf, isGetLyricx) => {\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  // console.log(buf)\n  // const index = buf.indexOf('\\r\\n\\r\\n') + 4\n  const lrcData = await handleInflate(buf.slice(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 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_str[i] ^ buf_key[j]\n      i++\n      j++\n    }\n  }\n\n  return iconv.decode(Buffer.from(output), 'gb18030')\n}\nexport default async({ lrcBuffer, isGetLyricx }) => {\n  const lrc = await decodeLyric(lrcBuffer, isGetLyricx)\n  // console.log(lrc)\n  return Buffer.from(lrc).toString('base64')\n}\n"
  },
  {
    "path": "src/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/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/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, { cache: 'default' }).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/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      cache: false,\n      binary: true,\n    })\n    requestObj.promise = requestObj.promise.then(({ statusCode, body }) => {\n      if (statusCode != 200) return Promise.reject(new Error(JSON.stringify(body)))\n      return decodeLyric({ lrcBuffer: body, 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/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/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/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      tid: 'new',\n      id: 'new',\n    },\n    {\n      name: '最热',\n      tid: 'hot',\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: 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/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/utils/musicSdk/kw/util.js",
    "content": "// import BackgroundTimer from 'react-native-background-timer'\n// import { httpGet, httpFetch } from '../../request'\nimport { toMD5 } from '../utils'\nimport { aesEncryptSync, aesDecryptSync, AES_MODE } from '@/utils/nativeModules/crypto'\nexport { default as decodeLyric } from './decodeLyric'\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'].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\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\n// const createAesEncrypt = (buffer, mode, key, iv) => {\n//   const cipher = createCipheriv(mode, key, iv)\n//   return Buffer.concat([cipher.update(buffer), cipher.final()])\n// }\n\n// const 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  aesKey: 'cFcnPcf6Kb85RC1y3V6M5A==',\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    const data = decodeURIComponent(base64Result)\n    return JSON.parse(aesDecryptSync(data, this.aesKey, this.aesIv, AES_MODE.ECB_128_NoPadding))\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 data = Buffer.from(JSON.stringify(jsonData)).toString('base64')\n    const time = Date.now()\n\n    const encodeData = aesEncryptSync(data, this.aesKey, this.aesIv, AES_MODE.ECB_128_NoPadding)\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/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/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/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/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/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/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/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\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/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/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/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/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: 30,\n  successCode: '000000',\n  cachedDetailInfo: {},\n  cachedUrl: {},\n  sortList: [\n    {\n      name: '推荐',\n      id: '15127315',\n      tid: 'recommend',\n      // id: '1',\n    },\n    // {\n    //   name: '最新',\n    //   id: '15127272',\n    //   tid: 'new',\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/MIGUM3.0/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 { url: location, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(body, location)\n    if (statusCode > 400) return this.getDetailUrl(link, page, ++retryNum)\n    if (location.split('?')[0] != link.split('?')[0]) {\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    //   if (body.retCode !== '100000') return this.getList(sortId, tagId, page, ++tryNum)\n    //   // if (body.code !== '000000') return this.getList(sortId, tagId, page, ++tryNum)\n    //   console.log(body)\n    //   // return {\n    //   //   list: this.filterList(body.data.contentItemList[0].itemList),\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/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/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/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/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/utils/musicSdk/options.js",
    "content": "export const bHh = '624868746c'\n\nexport const headers = {\n  'User-Agent': 'lx-music mobile request',\n  [bHh]: [bHh],\n}\n\n\nexport const timeout = 15000\n"
  },
  {
    "path": "src/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/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/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.songmid)\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/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/utils/musicSdk/tx/lyric.js",
    "content": "import { httpFetch } from '../../request'\nimport { b64DecodeUnicode, decodeName } from '../../index'\n\nexport 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/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/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/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      tid: 'hot',\n      id: 5,\n    },\n    {\n      name: '最新',\n      tid: 'new',\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 { url, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(headers)\n    if (statusCode > 400) return this.handleParseId(link, ++retryNum)\n    return url\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/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/utils/musicSdk/utils.js",
    "content": "import { stringMd5 } from 'react-native-quick-md5'\nimport { decodeName } from '../index'\n\n/**\n * 获取音乐音质\n * @param {*} info\n * @param {*} type\n */\n\nexport const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k']\nexport const getMusicType = (info, type) => {\n  const list = global.lx.qualityList[info.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 (info._types[type]) return type\n  }\n  return '128k'\n}\n\nexport const toMD5 = str => stringMd5(str)\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/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\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 - 1),\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/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/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/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/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/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/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/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\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/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      tid: 'hot',\n      id: 'hot',\n    },\n    // {\n    //   name: '最新',\n    //   tid: 'new',\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 { url, statusCode } = await requestObj_listDetailLink.promise\n    // console.log(headers)\n    if (statusCode > 400) return this.handleParseId(link, ++retryNum)\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      credentials: 'omit',\n      cache: 'default',\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/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/utils/musicSdk/wy/utils/crypto.js",
    "content": "// https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js\nimport { btoa } from 'react-native-quick-base64'\nimport { aesEncryptSync, aesDecryptSync, rsaEncryptSync, AES_MODE, RSA_PADDING } from '@/utils/nativeModules/crypto'\nimport { toMD5 } from '../../utils'\nconst iv = btoa('0102030405060708')\nconst presetKey = btoa('0CoJUm6Qyw8W8jud')\nconst linuxapiKey = btoa('rFgB&h#%2?^eDg:Q')\n// const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\nconst publicKey = '-----BEGIN PUBLIC KEY-----\\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\\n-----END PUBLIC KEY-----'\nconst eapiKey = btoa('e82ckenh8dichen8')\n\n\nconst aesEncrypt = (b64, mode, key, iv) => {\n  // console.log(b64, mode, key, iv)\n  // const cipher = createCipheriv(mode, key, iv)\n  // return Buffer.concat([cipher.update(buffer), cipher.final()])\n  return aesEncryptSync(b64, key, iv, mode)\n}\n\nconst aesDecrypt = (b64, mode, key, iv) => {\n  // let decipher = createDecipheriv(mode, key, iv)\n  // return Buffer.concat([decipher.update(b64), decipher.final()])\n  return aesDecryptSync(b64, key, iv, mode)\n}\n\nconst rsaEncrypt = (buffer, key) => {\n  buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])\n  return Buffer.from(rsaEncryptSync(buffer.toString('base64'), key, RSA_PADDING.NoPadding), 'base64')\n}\n\nexport const weapi = object => {\n  const text = JSON.stringify(object)\n  const secretKey = String(Math.random()).substring(2, 18)\n  return {\n    params: aesEncrypt(btoa(aesEncrypt(Buffer.from(text).toString('base64'), AES_MODE.CBC_128_PKCS7Padding, presetKey, iv)), AES_MODE.CBC_128_PKCS7Padding, btoa(secretKey), iv),\n    encSecKey: rsaEncrypt(Buffer.from(secretKey).reverse(), publicKey).toString('hex'),\n  }\n}\n\nexport const linuxapi = object => {\n  const text = JSON.stringify(object)\n  return {\n    eparams: Buffer.from(aesEncrypt(Buffer.from(text).toString('base64'), AES_MODE.ECB_128_NoPadding, linuxapiKey, ''), 'base64').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 = toMD5(message)\n  const data = `${url}-36cd479b6b5-${text}-36cd479b6b5-${digest}`\n  return {\n    params: Buffer.from(aesEncrypt(Buffer.from(data).toString('base64'), AES_MODE.ECB_128_NoPadding, eapiKey, ''), 'base64').toString('hex').toUpperCase(),\n  }\n}\n\nexport const eapiDecrypt = cipherBuffer => {\n  return aesDecrypt(cipherBuffer, AES_MODE.ECB_128_NoPadding, eapiKey, '').toString()\n}\n"
  },
  {
    "path": "src/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/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/utils/nativeModules/cache.ts",
    "content": "import { NativeModules } from 'react-native'\n\nconst { CacheModule } = NativeModules\n\nexport const getAppCacheSize = async(): Promise<number> => CacheModule.getAppCacheSize().then((size: number) => Math.trunc(size))\nexport const clearAppCache = CacheModule.clearAppCache as () => Promise<void>\n"
  },
  {
    "path": "src/utils/nativeModules/crypto.ts",
    "content": "import { NativeModules } from 'react-native'\n\nconst { CryptoModule } = NativeModules\n\n// export const testRsa = (text: string, key: string) => {\n//   // console.log(sourceFilePath, targetFilePath)\n//   return CryptoModule.testRsa()\n// }\n\nenum KEY_PREFIX {\n  publicKeyStart = '-----BEGIN PUBLIC KEY-----',\n  publicKeyEnd = '-----END PUBLIC KEY-----',\n  privateKeyStart = '-----BEGIN PRIVATE KEY-----',\n  privateKeyEnd = '-----END PRIVATE KEY-----',\n}\n\nexport enum RSA_PADDING {\n  OAEPWithSHA1AndMGF1Padding = 'RSA/ECB/OAEPWithSHA1AndMGF1Padding',\n  NoPadding = 'RSA/ECB/NoPadding',\n}\n\nexport enum AES_MODE {\n  CBC_128_PKCS7Padding = 'AES/CBC/PKCS7Padding',\n  ECB_128_NoPadding = 'AES',\n}\n\nexport const generateRsaKey = async() => {\n  // console.log(sourceFilePath, targetFilePath)\n  const key = await CryptoModule.generateRsaKey() as { publicKey: string, privateKey: string }\n  return {\n    publicKey: `${KEY_PREFIX.publicKeyStart}\\n${key.publicKey}${KEY_PREFIX.publicKeyEnd}`,\n    privateKey: `${KEY_PREFIX.privateKeyStart}\\n${key.privateKey}${KEY_PREFIX.privateKeyEnd}`,\n  }\n}\n\nexport const rsaEncrypt = async(text: string, key: string, padding: RSA_PADDING): Promise<string> => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.rsaEncrypt(text, key\n    .replace(KEY_PREFIX.publicKeyStart, '')\n    .replace(KEY_PREFIX.publicKeyEnd, ''),\n  padding)\n}\n\nexport const rsaDecrypt = async(text: string, key: string, padding: RSA_PADDING): Promise<string> => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.rsaDecrypt(text, key\n    .replace(KEY_PREFIX.privateKeyStart, '')\n    .replace(KEY_PREFIX.privateKeyEnd, ''),\n  padding)\n}\n\nexport const rsaEncryptSync = (text: string, key: string, padding: RSA_PADDING): string => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.rsaEncryptSync(text, key\n    .replace(KEY_PREFIX.publicKeyStart, '')\n    .replace(KEY_PREFIX.publicKeyEnd, ''),\n  padding)\n}\n\nexport const rsaDecryptSync = (text: string, key: string, padding: RSA_PADDING): string => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.rsaDecryptSync(text, key\n    .replace(KEY_PREFIX.privateKeyStart, '')\n    .replace(KEY_PREFIX.privateKeyEnd, ''),\n  padding)\n}\n\n\nexport const aesEncrypt = async(text: string, key: string, vi: string, mode: AES_MODE): Promise<string> => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.aesEncrypt(text, key, vi, mode)\n}\n\nexport const aesDecrypt = async(text: string, key: string, vi: string, mode: AES_MODE): Promise<string> => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.aesDecrypt(text, key, vi, mode)\n}\n\nexport const aesEncryptSync = (text: string, key: string, vi: string, mode: AES_MODE): string => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.aesEncryptSync(text, key, vi, mode)\n}\n\nexport const aesDecryptSync = (text: string, key: string, vi: string, mode: AES_MODE): string => {\n  // console.log(sourceFilePath, targetFilePath)\n  return CryptoModule.aesDecryptSync(text, key, vi, mode)\n}\n"
  },
  {
    "path": "src/utils/nativeModules/cryptoTest.ts",
    "content": "import {\n  rsaEncrypt,\n  rsaDecrypt,\n  RSA_PADDING,\n  generateRsaKey,\n  AES_MODE,\n  aesEncrypt,\n  aesDecrypt,\n} from '@/utils/nativeModules/crypto'\n\n\nconst publicKey = `\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4U2m4fBhTkQOOeAlEusFCDa28UI3xZqv\n5EGiOZCJ2bH1LfBjwG5dL3Zk2vT6XLaAn7vyXwVYNmdDn4Fa3l8fZndCty1aUAkpxZehZVy/0I+z\nQ7QwSvzQpv2yHPQ76Kcuc3E7VEMSPZkx71dQpsDBtE/F04TW6zOxomFcbqUA97QsjNwU8KKSKKJR\n2FhjEX0WhJpvDrkAKQBEujwf3pQDa8iUuF4F0v+oCKiSEf6tuWYx5iBpOvXUmZDLPeBnVZuvJM0e\n2yXaIYeZorDaosIvCEqVcDPT3gvePZp6eTyffRJmqk7OkyG2epWM1XPXynu85BYK91pZ03YRNBrp\nOkdU7wIDAQAB\n-----END PUBLIC KEY-----\n`\nconst privateKey = `\n-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDhTabh8GFORA454CUS6wUINrbx\nQjfFmq/kQaI5kInZsfUt8GPAbl0vdmTa9PpctoCfu/JfBVg2Z0OfgVreXx9md0K3LVpQCSnFl6Fl\nXL/Qj7NDtDBK/NCm/bIc9Dvopy5zcTtUQxI9mTHvV1CmwMG0T8XThNbrM7GiYVxupQD3tCyM3BTw\nopIoolHYWGMRfRaEmm8OuQApAES6PB/elANryJS4XgXS/6gIqJIR/q25ZjHmIGk69dSZkMs94GdV\nm68kzR7bJdohh5misNqiwi8ISpVwM9PeC949mnp5PJ99EmaqTs6TIbZ6lYzVc9fKe7zkFgr3WlnT\ndhE0Guk6R1TvAgMBAAECggEAOTnF94Fc1cpHar/Z6tIy9wEeumy9Sb2ei3V4RPLHcLnYspBqZcgi\ndxm1SEAND1tzlB7i0uvCmh7keDEc6XpzuUz1bx1f4RBSwdNftSU3uzukpr+vvHw2axPpF52ZUeCU\n1dGe5iobCfZNTqN44sH28VuJvc3x4M/CgKIGHjxe4IsyxFCIBpitjk829ymWqlUp/xdVxYfY+WFQ\n7/SgA48MU2ASyQVzBA4Q3MQ1d8Fn7Ogd+nYdCGaMfRvO0MI9DcB6uj6KoNZ2VxZkT6eXNEkbzCJR\nmbsHfWUx39HVGmlKvZefvryYKJoui1jAZw24F2h8WtBkeGIZ3DgyR9QLQaVT4QKBgQD/HkZUcYXw\nI8To/YDtO7i0UZ+vj95PHkfYsWizW1pUiFMHc2jxsyXcjYoKebf8gogKAYnwMxs9iZNkQ4V6zAHi\nzeE9C3SwMvh4l6MGJo10+/VmD3SaGZpHEs38HPyXqqqIsQEQq/WDiOMeacTY06AzeIAbW1lVHcTA\nXa8N3TOVuQKBgQDiFP9PAy0trTE8lozQHINytluXlsJap3WcRkGxOTR0v8YLHWYVXMQYo+s4s2Qx\nt48nwtBeDEI7OVMA2ip7mAC1IwNObYarLztyB1Vz2FJgVpyj63TdTUaxsiOeAbkLzo4r0TCZnuqi\nwdkhAWGu4i3hRrnXe6sbb2Dv4zYysNKT5wKBgQCTjN8AV+gvS4DHgFbg3nmlUNAaqgrZl5nWKkVz\n9pH38iCTXpyDrilntjTwehV/Zb9oihtNYUGQBdHJW4QH0ZYFpy1uMQH8Jn6uwIT5ObL2xgLYVHgL\n6GLiWG3qMpmk3oBjLnx/N/V3beRt4p6HCV7OZhMxv1Obduwklgp46ka7gQKBgQDDGbOpj+gw/sD6\ntEEYZ0LYf55TFvrqGJFaJxcRxXgLOGPDu78YuFFRokOfTtAsR2f2vBvszU9qpHGIzrzSo74YkvqL\nd+E7YSs/oCySKCAOmy/aFZtoTwOu3Tf3Zy01jy8JiSETsRxzEC48WWDe9rj5K3u9BTAIIPnaio1+\n+TEACQKBgQCfdfmP/05Q4Yc2wEtLfiuHatiobIBzdrem0lXS3ZsRsabnddkeGoQ2QvoMo1+D1/CA\nBL/KT6V2h9E8eNQVIOpwjxjR9wPBeHVSLhRV0Rh0Lkog4tGwvWVOh+W+ICr+s6Xn9xxvMUiL3Uw6\n9qebfBzruW5Gzke5E5/k3K6aCvFm0Q==\n-----END PRIVATE KEY-----\n`\n\n// void rsaEncrypt(Buffer.from('hello').toString('base64'), publicKey, RSA_PADDING.OAEPWithSHA1AndMGF1Padding).then((text) => {\n//   console.log(text)\n//   void rsaDecrypt(text, privateKey, RSA_PADDING.OAEPWithSHA1AndMGF1Padding).then((text) => {\n//     console.log(text)\n//   })\n// })\n// void generateRsaKey().then((key) => {\n//   console.log(key.publicKey)\n//   console.log(key.privateKey)\n// })\n\nconst aesKey = Buffer.from('123456789abcdefg').toString('base64')\nconst vi = Buffer.from('012345678901234a').toString('base64')\n\n// void aesEncrypt(Buffer.from('hello').toString('base64'), aesKey, vi, AES_MODE.CBC_PKCS7Padding).then((text) => {\n//   console.log('hello', text)\n//   void aesDecrypt(text, aesKey, vi, AES_MODE.CBC_PKCS7Padding).then((text) => {\n//     console.log(text)\n//   })\n// })\n\n// void aesEncrypt(Buffer.from('hello2').toString('base64'), aesKey, '', AES_MODE.ECB_NoPadding).then((text) => {\n//   console.log('hello2', text)\n//   void aesDecrypt(text, aesKey, '', AES_MODE.ECB_NoPadding).then((text) => {\n//     console.log(text)\n//   })\n// })\n"
  },
  {
    "path": "src/utils/nativeModules/lyricDesktop.ts",
    "content": "import { NativeModules, NativeEventEmitter } from 'react-native'\n\nconst { LyricModule } = NativeModules\n\n// export const themes = [\n//   { id: 'green', value: '#07c556' },\n//   { id: 'yellow', value: '#fffa12' },\n//   { id: 'blue', value: '#19b5fe' },\n//   { id: 'red', value: '#ff1222' },\n//   { id: 'pink', value: '#f1828d' },\n//   { id: 'purple', value: '#c851d4' },\n//   { id: 'orange', value: '#ffad12' },\n//   { id: 'grey', value: '#bdc3c7' },\n//   { id: 'black', value: '#333333' },\n//   { id: 'white', value: '#ffffff' },\n// ]\n\n// export const textPositionX = [\n//   { id: 'left', value: 'LEFT' },\n//   { id: 'center', value: 'CENTER' },\n//   { id: 'right', value: 'RIGHT' },\n// ]\n// export const textPositionY = [\n//   { id: 'top', value: 'TOP' },\n//   { id: 'center', value: 'CENTER' },\n//   { id: 'bottom', value: 'BOTTOM' },\n// ]\n\n// const getThemeColor = themeId => (themes.find(t => t.id == themeId) || themes[0]).value\n// const getTextPositionX = x => (textPositionX.find(t => t.id == x) || textPositionX[0]).value\n// const getTextPositionY = y => (textPositionY.find(t => t.id == y) || textPositionY[0]).value\nconst getAlpha = (num: number) => num / 100\nconst getTextSize = (num: number) => num / 10\n\n/**\n * 发送歌词事件\n * @param isShow\n * @returns\n */\nexport const setSendLyricTextEvent = async(isSend: boolean) => {\n  return LyricModule.setSendLyricTextEvent(isSend)\n}\n\n/**\n * show lyric\n */\nexport const showDesktopLyricView = async({\n  isShowToggleAnima,\n  isSingleLine,\n  width,\n  maxLineNum,\n  isLock,\n  unplayColor,\n  playedColor,\n  shadowColor,\n  opacity,\n  textSize,\n  positionX,\n  positionY,\n  textPositionX,\n  textPositionY,\n}: {\n  isShowToggleAnima: boolean\n  isSingleLine: boolean\n  width: number\n  maxLineNum: number\n  isLock: boolean\n  unplayColor: string\n  playedColor: string\n  shadowColor: string\n  opacity: number\n  textSize: number\n  positionX: number\n  positionY: number\n  textPositionX: LX.AppSetting['desktopLyric.textPosition.x']\n  textPositionY: LX.AppSetting['desktopLyric.textPosition.y']\n}): Promise<void> => {\n  return LyricModule.showDesktopLyric({\n    isSingleLine,\n    isShowToggleAnima,\n    isLock,\n    unplayColor,\n    playedColor,\n    shadowColor,\n    alpha: getAlpha(opacity),\n    textSize: getTextSize(textSize),\n    lyricViewX: positionX,\n    lyricViewY: positionY,\n    textX: textPositionX.toUpperCase(),\n    textY: textPositionY.toUpperCase(),\n    width,\n    maxLineNum,\n  })\n}\n\n/**\n * hide lyric\n */\nexport const hideDesktopLyricView = async(): Promise<void> => {\n  return LyricModule.hideDesktopLyric()\n}\n\n\n/**\n * play lyric\n * @param {Number} time play time\n * @returns {Promise} Promise\n */\nexport const play = async(time: number): Promise<void> => {\n  return LyricModule.play(time)\n}\n\n/**\n * pause lyric\n */\nexport const pause = async(): Promise<void> => {\n  return LyricModule.pause()\n}\n\n/**\n * set lyric\n * @param lyric lyric str\n * @param translation lyric translation\n * @param romalrc lyric translation\n */\nexport const setLyric = async(lyric: string, translation: string, romalrc: string): Promise<void> => {\n  return LyricModule.setLyric(lyric, translation || '', romalrc || '')\n}\n\nexport const setPlaybackRate = async(rate: number): Promise<void> => {\n  return LyricModule.setPlaybackRate(rate)\n}\n\n/**\n * toggle show translation\n * @param isShowTranslation is show translation\n */\nexport const toggleTranslation = async(isShowTranslation: boolean): Promise<void> => {\n  return LyricModule.toggleTranslation(isShowTranslation)\n}\n\n/**\n * toggle show roma lyric\n * @param isShowRoma is show roma lyric\n */\nexport const toggleRoma = async(isShowRoma: boolean): Promise<void> => {\n  return LyricModule.toggleRoma(isShowRoma)\n}\n\n/**\n * toggle is lock lyric window\n * @param isLock is lock lyric window\n */\nexport const toggleLock = async(isLock: boolean): Promise<void> => {\n  return LyricModule.toggleLock(isLock)\n}\n\n/**\n * set color\n * @param unplayColor\n * @param playedColor\n * @param shadowColor\n */\nexport const setColor = async(unplayColor: string, playedColor: string, shadowColor: string): Promise<void> => {\n  return LyricModule.setColor(unplayColor, playedColor, shadowColor)\n}\n\n/**\n * set text alpha\n * @param alpha text alpha\n */\nexport const setAlpha = async(alpha: number): Promise<void> => {\n  return LyricModule.setAlpha(getAlpha(alpha))\n}\n\n/**\n * set text size\n * @param size text size\n */\nexport const setTextSize = async(size: number): Promise<void> => {\n  return LyricModule.setTextSize(getTextSize(size))\n}\n\nexport const setShowToggleAnima = async(isShowToggleAnima: boolean): Promise<void> => {\n  return LyricModule.setShowToggleAnima(isShowToggleAnima)\n}\n\nexport const setSingleLine = async(isSingleLine: boolean): Promise<void> => {\n  return LyricModule.setSingleLine(isSingleLine)\n}\n\nexport const setPosition = async(x: number, y: number): Promise<void> => {\n  return LyricModule.setPosition(x, y)\n}\n\nexport const setMaxLineNum = async(maxLineNum: number): Promise<void> => {\n  return LyricModule.setMaxLineNum(maxLineNum)\n}\n\nexport const setWidth = async(width: number): Promise<void> => {\n  return LyricModule.setWidth(width)\n}\n\n// export const fixViewPosition = async(): Promise<void> => {\n//   return LyricModule.fixViewPosition()\n// }\n\nexport const setLyricTextPosition = async(textX: LX.AppSetting['desktopLyric.textPosition.x'], textY: LX.AppSetting['desktopLyric.textPosition.y']): Promise<void> => {\n  return LyricModule.setLyricTextPosition(textX.toUpperCase(), textY.toUpperCase())\n}\n\nexport const checkOverlayPermission = async(): Promise<void> => {\n  return LyricModule.checkOverlayPermission()\n}\n\nexport const openOverlayPermissionActivity = async(): Promise<void> => {\n  return LyricModule.openOverlayPermissionActivity()\n}\n\nexport const onPositionChange = (handler: (position: { x: number, y: number }) => void): () => void => {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  const eventEmitter = new NativeEventEmitter(LyricModule)\n  const eventListener = eventEmitter.addListener('set-position', event => {\n    handler(event as { x: number, y: number })\n  })\n\n  return () => {\n    eventListener.remove()\n  }\n}\n\nexport const onLyricLinePlay = (handler: (lineInfo: { text: string, extendedLyrics: string[] }) => void): () => void => {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  const eventEmitter = new NativeEventEmitter(LyricModule)\n  const eventListener = eventEmitter.addListener('lyric-line-play', event => {\n    handler(event as { text: string, extendedLyrics: string[] })\n  })\n\n  return () => {\n    eventListener.remove()\n  }\n}\n\n"
  },
  {
    "path": "src/utils/nativeModules/userApi.ts",
    "content": "import { NativeEventEmitter, NativeModules } from 'react-native'\n\nconst { UserApiModule } = NativeModules\n\nlet loadScriptInfo: LX.UserApi.UserApiInfo | null = null\nexport const loadScript = (info: LX.UserApi.UserApiInfo & { script: string }) => {\n  loadScriptInfo = info\n  UserApiModule.loadScript({\n    id: info.id,\n    name: info.name,\n    description: info.description,\n    version: info.version ?? '',\n    author: info.author ?? '',\n    homepage: info.homepage ?? '',\n    script: info.script,\n  })\n}\n\nexport interface SendResponseParams {\n  requestKey: string\n  error: string | null\n  response: {\n    statusCode: number\n    statusMessage: string\n    headers: Record<string, string>\n    body: any\n  } | null\n}\nexport interface SendActions {\n  request: LX.UserApi.UserApiRequestParams\n  response: SendResponseParams\n}\nexport const sendAction = <T extends keyof SendActions>(action: T, data: SendActions[T]) => {\n  UserApiModule.sendAction(action, JSON.stringify(data))\n}\n\n// export const clearAppCache = CacheModule.clearAppCache as () => Promise<void>\n\nexport interface InitParams {\n  status: boolean\n  errorMessage: string\n  info: LX.UserApi.UserApiInfo\n}\n\nexport interface ResponseParams {\n  status: boolean\n  errorMessage?: string\n  requestKey: string\n  result: any\n}\nexport interface UpdateInfoParams {\n  name: string\n  log: string\n  updateUrl: string\n}\nexport interface RequestParams {\n  requestKey: string\n  url: string\n  options: {\n    method: string\n    data: any\n    timeout: number\n    headers: any\n    binary: boolean\n  }\n}\nexport type CancelRequestParams = string\n\nexport interface Actions {\n  init: InitParams\n  request: RequestParams\n  cancelRequest: CancelRequestParams\n  response: ResponseParams\n  showUpdateAlert: UpdateInfoParams\n  log: string\n}\nexport type ActionsEvent = { [K in keyof Actions]: { action: K, data: Actions[K] } }[keyof Actions]\n\nexport const onScriptAction = (handler: (event: ActionsEvent) => void): () => void => {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  const eventEmitter = new NativeEventEmitter(UserApiModule)\n  const eventListener = eventEmitter.addListener('api-action', event => {\n    if (event.data) event.data = JSON.parse(event.data as string)\n    if (event.action == 'init') {\n      if (event.data.info) event.data.info = { ...loadScriptInfo, ...event.data.info }\n      else event.data.info = { ...loadScriptInfo }\n    } else if (event.action == 'showUpdateAlert') {\n      if (!loadScriptInfo?.allowShowUpdateAlert) return\n    }\n    handler(event as ActionsEvent)\n  })\n\n  return () => {\n    eventListener.remove()\n  }\n}\n\nexport const destroy = () => {\n  UserApiModule.destroy()\n}\n"
  },
  {
    "path": "src/utils/nativeModules/utils.ts",
    "content": "import { AppState, NativeEventEmitter, NativeModules } from 'react-native'\n\nconst { UtilsModule } = NativeModules\n\nexport const exitApp = UtilsModule.exitApp\n\nexport const getSupportedAbis = UtilsModule.getSupportedAbis\n\nexport const installApk = (filePath: string, fileProviderAuthority: string) => UtilsModule.installApk(filePath, fileProviderAuthority)\n\n\nexport const screenkeepAwake = () => {\n  if (global.lx.isScreenKeepAwake) return\n  global.lx.isScreenKeepAwake = true\n  UtilsModule.screenkeepAwake()\n}\nexport const screenUnkeepAwake = () => {\n  // console.log('screenUnkeepAwake')\n  if (!global.lx.isScreenKeepAwake) return\n  global.lx.isScreenKeepAwake = false\n  UtilsModule.screenUnkeepAwake()\n}\n\nexport const getWIFIIPV4Address = UtilsModule.getWIFIIPV4Address as () => Promise<string>\n\nexport const getDeviceName = async(): Promise<string> => {\n  return UtilsModule.getDeviceName().then((deviceName: string) => deviceName || 'Unknown')\n}\n\nexport const isNotificationsEnabled = UtilsModule.isNotificationsEnabled as () => Promise<boolean>\n\nexport const requestNotificationPermission = async() => new Promise<boolean>((resolve) => {\n  let subscription = AppState.addEventListener('change', (state) => {\n    if (state != 'active') return\n    subscription.remove()\n    setTimeout(() => {\n      void isNotificationsEnabled().then(resolve)\n    }, 1000)\n  })\n  UtilsModule.openNotificationPermissionActivity().then((result: boolean) => {\n    if (result) return\n    subscription.remove()\n    resolve(false)\n  })\n})\n\nexport const shareText = async(shareTitle: string, title: string, text: string): Promise<void> => {\n  UtilsModule.shareText(shareTitle, title, text)\n}\n\nexport const getSystemLocales = async(): Promise<string> => {\n  return UtilsModule.getSystemLocales()\n}\n\nexport const onScreenStateChange = (handler: (state: 'ON' | 'OFF') => void): () => void => {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  const eventEmitter = new NativeEventEmitter(UtilsModule)\n  const eventListener = eventEmitter.addListener('screen-state', event => {\n    handler(event.state as 'ON' | 'OFF')\n  })\n\n  return () => {\n    eventListener.remove()\n  }\n}\n\nexport const getWindowSize = async(): Promise<{ width: number, height: number }> => {\n  return UtilsModule.getWindowSize()\n}\n\nexport const onWindowSizeChange = (handler: (size: { width: number, height: number }) => void): () => void => {\n  UtilsModule.listenWindowSizeChanged()\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n  const eventEmitter = new NativeEventEmitter(UtilsModule)\n  const eventListener = eventEmitter.addListener('screen-size-changed', event => {\n    handler(event as { width: number, height: number })\n  })\n\n  return () => {\n    eventListener.remove()\n  }\n}\n\nexport const isIgnoringBatteryOptimization = async(): Promise<boolean> => {\n  return UtilsModule.isIgnoringBatteryOptimization()\n}\n\nexport const requestIgnoreBatteryOptimization = async() => new Promise<boolean>((resolve) => {\n  let subscription = AppState.addEventListener('change', (state) => {\n    if (state != 'active') return\n    subscription.remove()\n    setTimeout(() => {\n      void isIgnoringBatteryOptimization().then(resolve)\n    }, 1000)\n  })\n  UtilsModule.requestIgnoreBatteryOptimization().then((result: boolean) => {\n    if (result) return\n    subscription.remove()\n    resolve(false)\n  })\n})\n"
  },
  {
    "path": "src/utils/pixelRatio.ts",
    "content": "/**\n * Created by qianxin on 17/6/1.\n * 屏幕工具类\n * ui设计基准,iphone 6\n * width:375\n * height:667\n */\nimport { PixelRatio } from 'react-native'\nimport { windowSizeTools } from './windowSizeTools'\n\n// 高保真的宽度和高度\nconst designWidth = 375.0\nconst designHeight = 667.0\n\n// 获取屏幕的dp\nconst size = windowSizeTools.getSize()\n// console.log('size', size)\nlet screenW = size.width\nlet screenH = size.height\nif (screenW > screenH) {\n  const temp = screenW\n  screenW = screenH\n  screenH = temp\n}\nlet fontScale = PixelRatio.getFontScale()\nlet pixelRatio = PixelRatio.get()\n// 根据dp获取屏幕的px\nlet screenPxW = PixelRatio.getPixelSizeForLayoutSize(screenW)\nlet screenPxH = PixelRatio.getPixelSizeForLayoutSize(screenH)\n// console.log(screenPxW, screenPxH)\n\nconst scaleW = screenPxW / designWidth\nconst scaleH = screenPxH / designHeight\nconst scale = Math.min(scaleW, scaleH, 3.1)\n// console.log(scale)\n\n/**\n * 设置text\n * @param size  px\n * @returns dp\n */\nexport function getTextSize(size: number) {\n  // console.log('screenW======' + screenW)\n  // console.log('screenPxW======' + screenPxW)\n  let scaleWidth = screenW / designWidth\n  let scaleHeight = screenH / designHeight\n  // console.log(scaleWidth, scaleHeight)\n  let scale = Math.min(scaleWidth, scaleHeight, 1.3)\n  size = Math.floor(size * scale / fontScale)\n  // console.log(size)\n  return size\n}\nexport function setSpText(size: number) {\n  return getTextSize(size) * global.lx.fontSize\n}\n\n/**\n * 设置高度\n * @param size  px\n * @returns dp\n */\nexport function scaleSizeH(size: number) {\n  // console.log(screenPxH / designHeight)\n  // let scaleHeight = size * Math.min(screenPxH / designHeight, 3.1)\n  let scaleHeight = size * scale\n  size = Math.floor(scaleHeight / pixelRatio)\n  return size * global.lx.fontSize\n}\n\n/**\n * 设置宽度\n * @param size  px\n * @returns dp\n */\nexport function scaleSizeW(size: number) {\n  // console.log(screenPxW / designWidth)\n  // let scaleWidth = size * Math.min(screenPxW / designWidth, 3.1)\n  let scaleWidth = size * scale\n  size = Math.floor(scaleWidth / pixelRatio)\n  return size * global.lx.fontSize\n}\n\n\nexport const scaleSizeWR = (size: number) => {\n  return size * 2 - scaleSizeW(size)\n}\n\nexport const scaleSizeHR = (size: number) => {\n  return size * 2 - scaleSizeH(size)\n}\n\nexport const scaleSizeAbsHR = (size: number) => {\n  let scaleHeight = size * scale\n  return size * 2 - Math.floor(scaleHeight / pixelRatio)\n}\n"
  },
  {
    "path": "src/utils/request.js",
    "content": "// import needle from 'needle'\n// import progress from 'request-progress'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { requestMsg } from './message'\nimport { bHh } from './musicSdk/options'\nimport { deflateRaw } from 'pako'\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/**\n * 请求超时自动重试\n * @param {*} url\n * @param {*} options\n */\nexport const httpFetch = (url, options = { method: 'get' }) => {\n  const requestObj = fetchData(url, options)\n  return {\n    promise: requestObj.request.catch(err => {\n      console.log('出错', err.message)\n      switch (err.message) {\n        case 'socket hang up':\n          return Promise.reject(new Error(requestMsg.unachievable))\n        case 'Aborted':\n          return Promise.reject(new Error(requestMsg.timeout))\n        case 'Network request failed':\n          return Promise.reject(new Error(requestMsg.notConnectNetwork))\n        default:\n          return Promise.reject(err)\n      }\n    }),\n    cancelHttp() {\n      requestObj.abort()\n    },\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  const requestObj = fetchData(url, { ...options, method: 'get' })\n  requestObj.request.then(resp => {\n    callback(null, resp, resp.body)\n  }).catch(err => {\n    // debugRequest && console.log(JSON.stringify(err))\n    callback(err, null, null)\n  })\n\n  return () => {\n    requestObj.abort()\n  }\n}\n\n/*\nconst fetchWithTimeout = (resource, options) => {\n  const { timeout = 8000 } = options\n\n  const controller = new global.AbortController()\n  const id = BackgroundTimer.setTimeout(() => controller.abort(), timeout)\n\n  return {\n    request: global.fetch(resource, {\n      ...options,\n      signal: controller.signal,\n    }).then(response => {\n      BackgroundTimer.clearTimeout(id)\n      return response\n    }),\n    abort() {\n      controller.abort()\n    },\n  }\n} */\n\n\nconst handleDeflateRaw = data => new Promise((resolve, reject) => {\n  resolve(Buffer.from(deflateRaw(data)))\n  // deflateRaw(data, (err, buf) => {\n  //   if (err) return reject(err)\n  //   resolve(buf)\n  // })\n})\n\nconst regx = /(?:\\d\\w)+/g\n\nconst handleRequestData = async(url, {\n  method = 'get',\n  headers = {},\n  format = 'json',\n  cache = 'no-store',\n  ...options\n}) => {\n  // console.log(url, options)\n  headers = Object.assign({\n    Accept: 'application/json',\n  }, headers)\n  options.cache = cache\n  if (method.toLocaleLowerCase() === 'post' && !headers['Content-Type']) {\n    if (options.form) {\n      headers['Content-Type'] = 'application/x-www-form-urlencoded'\n      const formBody = []\n      for (let [key, value] of Object.entries(options.form)) {\n        let encodedKey = encodeURIComponent(key)\n        let encodedValue = encodeURIComponent(value)\n        formBody.push(`${encodedKey}=${encodedValue}`)\n      }\n      options.body = formBody.join('&')\n      delete options.form\n    } else if (options.formData) {\n      headers['Content-Type'] = 'multipart/form-data'\n      const formBody = []\n      for (let [key, value] of Object.entries(options.form)) {\n        let encodedKey = encodeURIComponent(key)\n        let encodedValue = encodeURIComponent(value)\n        formBody.push(`${encodedKey}=${encodedValue}`)\n      }\n      options.body = options.formData\n      delete options.formData\n    } else {\n      headers['Content-Type'] = 'application/json'\n    }\n  }\n  if (headers['Content-Type'] === 'application/json' && options.body) {\n    options.body = JSON.stringify(options.body)\n  }\n  if (headers[bHh]) {\n    let s = Buffer.from(bHh, 'hex').toString()\n    s = s.replace(s.substr(-1), '')\n    s = Buffer.from(s, 'base64').toString()\n    const v = process.versions.app.split('-')[0].split('.').map(n => n.length < 3 ? n.padStart(3, '0') : n).join('')\n    const v2 = process.versions.app.split('-')[1] || ''\n    headers[s] = !s || `${(await handleDeflateRaw(Buffer.from(JSON.stringify(`${url}${v}`.match(regx), null, 1).concat(v)).toString('base64'))).toString('hex')}&${parseInt(v)}${v2}`\n    delete headers[bHh]\n  }\n\n  return {\n    ...options,\n    method,\n    headers: Object.assign({}, defaultHeaders, headers),\n  }\n}\n\n// https://stackoverflow.com/a/64945178\nconst blobToBuffer = (blob) => {\n  return new Promise((resolve, reject) => {\n    const reader = new global.FileReader()\n    reader.onerror = reject\n    reader.onload = () => {\n      const data = reader.result.slice(reader.result.indexOf('base64,') + 7)\n      resolve(Buffer.from(data, 'base64'))\n    }\n    reader.readAsDataURL(blob)\n  })\n}\n\nconst fetchData = (url, { timeout = 15000, ...options }) => {\n  console.log('---start---', url)\n\n  const controller = new global.AbortController()\n  let id = BackgroundTimer.setTimeout(() => {\n    id = null\n    controller.abort()\n  }, timeout)\n\n  return {\n    request: handleRequestData(url, options).then(options => {\n      return global.fetch(url, {\n        ...options,\n        signal: controller.signal,\n      }).then(resp => (options.binary ? resp.blob() : resp.text()).then(text => {\n        // console.log(options, headers, text)\n        return {\n          headers: resp.headers.map,\n          body: text,\n          statusCode: resp.status,\n          statusMessage: resp.statusText,\n          url: resp.url,\n          ok: resp.ok,\n        }\n      })).then(resp => {\n        if (options.binary) {\n          return blobToBuffer(resp.body).then(buffer => {\n            resp.body = buffer\n            return resp\n          })\n        } else {\n          try {\n            resp.body = JSON.parse(resp.body)\n          } catch {}\n          return resp\n        }\n      }).catch(err => {\n        // console.log(err, err.code, err.message)\n        return Promise.reject(err)\n      }).finally(() => {\n        if (id == null) return\n        BackgroundTimer.clearTimeout(id)\n      })\n    }),\n    abort() {\n      controller.abort()\n    },\n  }\n}\n\nexport const checkUrl = async(url, options = {}) => {\n  return fetchData(url, { method: 'head', ...options }).request.then(resp => {\n    if (resp.statusCode === 200) {\n      return Promise.resolve()\n    } else {\n      throw new Error(resp.statusCode)\n    }\n  })\n}\n"
  },
  {
    "path": "src/utils/scroll.ts",
    "content": "import { type NativeScrollEvent, type FlatList, type NativeSyntheticEvent } from 'react-native'\n\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 = () => {}\n\ntype ScrollElement<T> = {\n  lx_scrollLockKey?: number\n  lx_scrollNextParams?: [ScrollElement<FlatList<any>>, NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'], number, number, Noop]\n  lx_scrollTimeout?: NodeJS.Timeout\n  lx_scrollDelayTimeout?: NodeJS.Timeout\n} & T\n\nconst handleScrollY = (element: ScrollElement<FlatList<any>>, info: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'], to: number, duration = 300, fn = noop): Noop => {\n  if (element == null) {\n    fn()\n    return noop\n  }\n\n  const clean = () => {\n    element.lx_scrollLockKey = undefined\n    element.lx_scrollNextParams = undefined\n    if (element.lx_scrollTimeout) clearTimeout(element.lx_scrollTimeout)\n    element.lx_scrollTimeout = undefined\n  }\n  if (element.lx_scrollLockKey) {\n    element.lx_scrollNextParams = [element, info, to, duration, fn]\n    element.lx_scrollLockKey = -1\n    return clean\n  }\n\n\n  let start = info.contentOffset.y\n  if (to > start) {\n    let maxScrollTop = info.contentSize.height - info.layoutMeasurement.height\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  let 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, info, _to, duration, fn] = element.lx_scrollNextParams\n    //   if (to > _to && info.contentOffset.y < val) info.contentOffset.y = val\n    //   clean()\n    //   handleScrollY(_element, info, _to, duration, fn)\n    //   return\n    // }\n    // if (element.lx_scrollLockKey != key) {\n    //   if (element.lx_scrollNextParams) {\n    //     // element.lx_scrollNextParams[1].contentOffset.y = val\n    //     // handleScrollY(element.lx_scrollNextParams[0], element.lx_scrollNextParams[1], element.lx_scrollNextParams[2], element.lx_scrollNextParams[3], element.lx_scrollNextParams[4])\n    //     start = val\n    //     info = element.lx_scrollNextParams[1]\n    //     to = element.lx_scrollNextParams[2]\n    //     fn = element.lx_scrollNextParams[4]\n    //     if (to > start) {\n    //       let maxScrollTop = info.contentSize.height - info.layoutMeasurement.height\n    //       if (to > maxScrollTop) to = maxScrollTop\n    //     } else if (to < start) {\n    //       if (to < 0) to = 0\n    //     } else {\n    //       clean()\n    //       fn()\n    //       return\n    //     }\n    //     change = to - val\n\n    //     element.lx_scrollLockKey = key\n    //   } else {\n    //     fn()\n    //     return\n    //   }\n    // }\n    currentTime += increment\n    val = Math.trunc(easeInOutQuad(currentTime, start, change, duration))\n    element.scrollToOffset({ offset: val, animated: false })\n    if (currentTime < duration) {\n      element.lx_scrollTimeout = setTimeout(animateScroll, increment)\n    } else {\n      if (element.lx_scrollNextParams) {\n        const [_element, info, _to, duration, fn] = element.lx_scrollNextParams\n        if (to > _to && info.contentOffset.y < val) info.contentOffset.y = val\n        info.contentOffset.y = val\n        clean()\n        handleScrollY(_element, info, _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<FlatList<any>>, info: NativeSyntheticEvent<NativeScrollEvent>['nativeEvent'], to: number, duration = 300, fn = () => {}, delay = 0): () => void => {\n  let cancelFn: () => void\n  if (element.lx_scrollDelayTimeout != null) {\n    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        clearTimeout(element.lx_scrollDelayTimeout)\n        element.lx_scrollDelayTimeout = undefined\n      }\n    }\n    element.lx_scrollDelayTimeout = setTimeout(() => {\n      element.lx_scrollDelayTimeout = undefined\n      scrollCancelFn = handleScrollY(element, info, to, duration, fn)\n    }, delay)\n  } else {\n    cancelFn = handleScrollY(element, info, to, duration, fn) ?? noop\n  }\n  return cancelFn\n}\n\n"
  },
  {
    "path": "src/utils/simplify-chinese-main/.gitignore",
    "content": "todo.md\nnode_modules/\nnpm-debug.log\nyarn-debug.log\nyarn-error.log\npackage-lock.json\ntsconfig.tsbuildinfo\nreport.*.json\n\n.eslintcache\n.DS_Store\n.idea\n.vscode\n.yarn\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n"
  },
  {
    "path": "src/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/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/utils/simplify-chinese-main/chinese.js",
    "content": "// exports.simplified = '万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤呼啧啬啭啮啰啴啸喷喽喾嗫嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾鼋鼌鼍鼗鼹齄齐齑齿龀龁龂龃龄龅龆龇龈龉龊龋龌龙龚龛龟志制咨系范尝准闲拼'\n// exports.traditional = '萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚唿嘖嗇囀齧囉嘽嘯噴嘍嚳囁噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽黿鼂鼉鞀鼴齇齊齏齒齔齕齗齟齡齙齠齜齦齬齪齲齷龍龔龕龜誌製谘係範嘗準閒拚'\nexport default {\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  伥: '倀',\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  凄: '淒',\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  卺: '巹',\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  哒: '噠',\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  垅: '壟',\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  婶: '嬸',\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  崃: '崍',\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  忆: '憶',\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  扦: '扡',\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  携: '攜',\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  枧: '梘',\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  橼: '櫞',\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  浆: '漿',\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  滨: '濱',\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  狝: '獮',\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  疴: '屙',\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  硚: '礄',\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  箦: '簀',\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  绁: '絏',\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  绻: '綣',\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  缵: '纘',\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  膑: '臏',\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  莲: '蓮',\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  蛳: '螄',\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  誊: '謄',\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  诚: '誠',\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  谕: '諭',\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  贳: '貰',\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  跞: '躒',\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  辍: '輟',\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  酝: '醞',\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  钵: '缽',\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  铵: '銨',\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  锴: '鍇',\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  镴: '鑞',\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  阶: '階',\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  颊: '頰',\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  饰: '飾',\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  骀: '駘',\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  鲉: '鮋',\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  鳃: '鰓',\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  鸹: '鴰',\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  鹾: '鹺',\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"
  },
  {
    "path": "src/utils/simplify-chinese-main/index.d.ts",
    "content": "export function simplify(source: string): string\nexport function tranditionalize(source: string): string\n"
  },
  {
    "path": "src/utils/simplify-chinese-main/index.js",
    "content": "// const { simplified, traditional } = require('./chinese')\nimport stMap from './chinese'\n\n// const stMap = {}\n// const tsMap = new Map()\n\n// simplified.split('').forEach((char, index) => {\n//   stMap[char] = traditional[index]\n//   // stMap.set(char, traditional[index])\n//   // tsMap.set(traditional[index], char)\n// })\n// console.log(JSON.stringify(stMap))\n// function 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[char] || char)\n  }\n  return result.join('')\n}\n\n// module.exports = {\n//   // simplify,\n//   tranditionalize,\n// }\nexport {\n  tranditionalize,\n}\n"
  },
  {
    "path": "src/utils/simplify-chinese-main/package.json",
    "content": "{\n  \"name\": \"simplify-chinese\",\n  \"description\": \"Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具\",\n  \"version\": \"1.1.0\",\n  \"main\": \"index.js\",\n  \"typings\": \"index.d.ts\",\n  \"repository\": \"https://github.com/koishijs/simplify-chinese.git\",\n  \"author\": \"Shigma <1700011071@pku.edu.cn>\",\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "src/utils/tools.ts",
    "content": "import { Platform, ToastAndroid, BackHandler, Linking, Dimensions, Alert, Appearance, PermissionsAndroid, AppState, StyleSheet, type ScaledSize } from 'react-native'\n// import ExtraDimensions from 'react-native-extra-dimensions-android'\nimport Clipboard from '@react-native-clipboard/clipboard'\nimport { storageDataPrefix } from '@/config/constant'\nimport { gzipFile, readFile, temporaryDirectoryPath, unGzipFile, unlink, writeFile } from '@/utils/fs'\nimport { getSystemLocales, isIgnoringBatteryOptimization, isNotificationsEnabled, requestNotificationPermission, requestIgnoreBatteryOptimization, shareText } from '@/utils/nativeModules/utils'\nimport musicSdk from '@/utils/musicSdk'\nimport { getData, removeData, saveData } from '@/plugins/storage'\nimport BackgroundTimer from 'react-native-background-timer'\nimport { scaleSizeH, scaleSizeW, setSpText } from './pixelRatio'\nimport { toOldMusicInfo } from './index'\nimport { stringMd5 } from 'react-native-quick-md5'\nimport { windowSizeTools } from '@/utils/windowSizeTools'\n\n\n// https://stackoverflow.com/a/47349998\nexport const getDeviceLanguage = async() => {\n  // let deviceLanguage = Platform.OS === 'ios'\n  //   ? NativeModules.SettingsManager.settings.AppleLocale ||\n  //     NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13\n  //   : await getSystemLocales()\n  // deviceLanguage = typeof deviceLanguage === 'string' ? deviceLanguage.substring(0, 5).toLocaleLowerCase() : ''\n  return getSystemLocales()\n}\n\n\nexport const isAndroid = Platform.OS === 'android'\n// @ts-expect-error\nexport const osVer = Platform.constants.Release as string\n\nexport const isActive = () => AppState.currentState == 'active'\n\nexport const TEMP_FILE_PATH = temporaryDirectoryPath + '/tempFile'\n\n// fix https://github.com/facebook/react-native/issues/4934\n// export const getWindowSise = (windowDimensions?: ReturnType<(typeof Dimensions)['get']>) => {\n//   return windowSizeTools.getSize()\n//   // windowDimensions ??= Dimensions.get('window')\n//   // if (Platform.OS === 'ios') return windowDimensions\n//   // return windowDimensions\n//   // const windowSize = {\n//   //   width: ExtraDimensions.getRealWindowWidth(),\n//   //   height: ExtraDimensions.getRealWindowHeight(),\n//   // }\n//   // if (\n//   //   (windowDimensions.height > windowDimensions.width && windowSize.height < windowSize.width) ||\n//   //   (windowDimensions.width > windowDimensions.height && windowSize.width < windowSize.height)\n//   // ) {\n//   //   windowSize.height = windowSize.width\n//   // }\n//   // windowSize.width = windowDimensions.width\n\n//   // if (ExtraDimensions.isSoftMenuBarEnabled()) {\n//   //   windowSize.height -= ExtraDimensions.getSoftMenuBarHeight()\n//   // }\n//   // return windowSize\n// }\n\nexport const checkStoragePermissions = async() => PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE)\n\nexport const requestStoragePermission = async() => {\n  const isGranted = await checkStoragePermissions()\n  if (isGranted) return isGranted\n\n  try {\n    const granted = await PermissionsAndroid.requestMultiple(\n      [\n        PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,\n        PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,\n      ],\n      // {\n      //   title: '存储读写权限申请',\n      //   message:\n      //     '洛雪音乐助手需要使用存储读写权限才能下载歌曲.',\n      //   buttonNeutral: '一会再问我',\n      //   buttonNegative: '取消',\n      //   buttonPositive: '确定',\n      // },\n    )\n    // console.log(granted)\n    // console.log(Object.values(granted).every(r => r === PermissionsAndroid.RESULTS.GRANTED))\n    // console.log(PermissionsAndroid.RESULTS)\n    const granteds = Object.values(granted)\n    return granteds.every(r => r === PermissionsAndroid.RESULTS.GRANTED)\n      ? true\n      : granteds.includes(PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN)\n        ? null\n        : false\n    // if (granted === PermissionsAndroid.RESULTS.GRANTED) {\n    //   console.log('You can use the storage')\n    // } else {\n    //   console.log('Storage permission denied')\n    // }\n  } catch (err: any) {\n    // console.warn(err)\n    return false\n  }\n}\n\n\n/**\n * 显示toast\n * @param message 消息\n * @param duration 时长\n * @param position 位置\n */\nexport const toast = (message: string, duration: 'long' | 'short' = 'short', position: 'top' | 'center' | 'bottom' = 'bottom') => {\n  let _duration\n  switch (duration) {\n    case 'long':\n      _duration = ToastAndroid.LONG\n      break\n    case 'short':\n    default:\n      _duration = ToastAndroid.SHORT\n      break\n  }\n  let _position\n  let offset: number\n  switch (position) {\n    case 'top':\n      _position = ToastAndroid.TOP\n      offset = 120\n      break\n    case 'center':\n      _position = ToastAndroid.CENTER\n      offset = 0\n      break\n    case 'bottom':\n    default:\n      _position = ToastAndroid.BOTTOM\n      offset = 120\n      break\n  }\n  ToastAndroid.showWithGravityAndOffset(message, _duration, _position, 0, offset)\n}\n\nexport const openUrl = async(url: string): Promise<void> => Linking.canOpenURL(url).then(async() => Linking.openURL(url))\n\nexport const assertApiSupport = (source: LX.Source): boolean => {\n  return source == 'local' || global.lx.qualityList[source] != null\n}\n\n// const handleRemoveDataMultiple = async keys => {\n//   await removeDataMultiple(keys.splice(0, 500))\n//   if (keys.length) return handleRemoveDataMultiple(keys)\n// }\n\nexport const exitApp = () => {\n  BackHandler.exitApp()\n}\n\nexport const handleSaveFile = async(path: string, data: any) => {\n  // if (!path.endsWith('.json')) path += '.json'\n  // const buffer = gzip(data)\n  const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`\n  await writeFile(tempFilePath, JSON.stringify(data))\n  await gzipFile(tempFilePath, path)\n  await unlink(tempFilePath)\n}\nexport const handleReadFile = async<T = unknown>(path: string): Promise<T> => {\n  let isJSON = path.endsWith('.json')\n  let data\n  if (isJSON) {\n    data = await readFile(path)\n  } else {\n    const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`\n    await unGzipFile(path, tempFilePath)\n    data = await readFile(tempFilePath)\n    await unlink(tempFilePath)\n  }\n  data = JSON.parse(data)\n\n  // 修复PC v1.14.0出现的导出数据被序列化两次的问题\n  if (typeof data != 'object') {\n    try {\n      data = JSON.parse(data as string)\n    } catch (err) {\n      return data\n    }\n  }\n\n  return data\n}\n\nexport const confirmDialog = async({\n  title = '',\n  message = '',\n  cancelButtonText = global.i18n.t('dialog_cancel'),\n  confirmButtonText = global.i18n.t('dialog_confirm'),\n  bgClose = true,\n}) => {\n  return new Promise<boolean>(resolve => {\n    Alert.alert(title, message, [\n      {\n        text: cancelButtonText,\n        onPress() {\n          resolve(false)\n        },\n      },\n      {\n        text: confirmButtonText,\n        onPress() {\n          resolve(true)\n        },\n      },\n    ], {\n      cancelable: bgClose,\n      onDismiss() {\n        resolve(false)\n      },\n    })\n  })\n}\n\nexport const tipDialog = async({\n  title = '',\n  message = '',\n  btnText = global.i18n.t('dialog_confirm'),\n  bgClose = true,\n}) => {\n  return new Promise<void>(resolve => {\n    Alert.alert(title, message, [\n      {\n        text: btnText,\n        onPress() {\n          resolve()\n        },\n      },\n    ], {\n      cancelable: bgClose,\n      onDismiss() {\n        resolve()\n      },\n    })\n  })\n}\n\nexport const clipboardWriteText = (str: string) => {\n  Clipboard.setString(str)\n}\n\n\nexport const checkNotificationPermission = async() => {\n  const isHide = await getData(storageDataPrefix.notificationTipEnable)\n  if (isHide != null) return\n  const enabled = await isNotificationsEnabled()\n  if (enabled) return\n  return new Promise<void>((resolve) => {\n    Alert.alert(\n      global.i18n.t('notifications_check_title'),\n      global.i18n.t('notifications_check_tip'),\n      [\n        {\n          text: global.i18n.t('never_show'),\n          onPress: () => {\n            void saveData(storageDataPrefix.notificationTipEnable, '1')\n            toast(global.i18n.t('disagree_tip'))\n            resolve()\n          },\n        },\n        {\n          text: global.i18n.t('disagree'),\n          onPress: () => {\n            toast(global.i18n.t('disagree_tip'))\n            resolve()\n          },\n        },\n        {\n          text: global.i18n.t('agree_go'),\n          onPress: () => {\n            requestAnimationFrame(() => {\n              void requestNotificationPermission().then((result) => {\n                if (!result) toast(global.i18n.t('disagree_tip'))\n                resolve()\n              })\n            })\n          },\n        },\n      ],\n    )\n  })\n}\n\n\nexport const checkIgnoringBatteryOptimization = async() => {\n  const isHide = await getData(storageDataPrefix.ignoringBatteryOptimizationTipEnable)\n  if (isHide != null) return\n  const enabled = await isIgnoringBatteryOptimization()\n  if (enabled) return\n  return new Promise<void>((resolve) => {\n    Alert.alert(\n      global.i18n.t('ignoring_battery_optimization_check_title'),\n      global.i18n.t('ignoring_battery_optimization_check_tip'),\n      [\n        {\n          text: global.i18n.t('never_show'),\n          onPress: () => {\n            void saveData(storageDataPrefix.ignoringBatteryOptimizationTipEnable, '1')\n            toast(global.i18n.t('disagree_tip'))\n            resolve()\n          },\n        },\n        {\n          text: global.i18n.t('disagree'),\n          onPress: () => {\n            toast(global.i18n.t('disagree_tip'))\n            resolve()\n          },\n        },\n        {\n          text: global.i18n.t('agree_to'),\n          onPress: () => {\n            requestAnimationFrame(() => {\n              void requestIgnoreBatteryOptimization().then((result) => {\n                if (!result) toast(global.i18n.t('disagree_tip'))\n                resolve()\n              })\n            })\n          },\n        },\n      ],\n    )\n  })\n}\nexport const resetNotificationPermissionCheck = async() => {\n  return removeData(storageDataPrefix.notificationTipEnable)\n}\nexport const resetIgnoringBatteryOptimizationCheck = async() => {\n  return removeData(storageDataPrefix.ignoringBatteryOptimizationTipEnable)\n}\n\nexport const shareMusic = (shareType: LX.ShareType, downloadFileName: LX.AppSetting['download.fileName'], musicInfo: LX.Music.MusicInfo) => {\n  const name = musicInfo.name\n  const singer = musicInfo.singer\n  const detailUrl = musicInfo.source == 'local' ? '' : musicSdk[musicInfo.source]?.getMusicDetailPageUrl(toOldMusicInfo(musicInfo)) ?? ''\n  const musicTitle = downloadFileName.replace('歌名', name).replace('歌手', singer)\n  switch (shareType) {\n    case 'system':\n      void shareText(global.i18n.t('share_card_title_music', { name }), global.i18n.t('share_title_music'), `${musicTitle.replace(/\\s/g, '')}${detailUrl ? '\\n' + detailUrl : ''}`)\n      break\n    case 'clipboard':\n      clipboardWriteText(`${musicTitle}${detailUrl ? '\\n' + detailUrl : ''}`)\n      toast(global.i18n.t('copy_name_tip'))\n      break\n  }\n}\n\nexport const onDimensionChange = (handler: (info: { window: ScaledSize, screen: ScaledSize }) => void) => {\n  return Dimensions.addEventListener('change', handler)\n}\n\n\nexport const getAppearance = () => {\n  return Appearance.getColorScheme() ?? 'light'\n}\n\nexport const onAppearanceChange = (callback: (colorScheme: Parameters<Parameters<typeof Appearance['addChangeListener']>[0]>[0]['colorScheme']) => void) => {\n  return Appearance.addChangeListener(({ colorScheme }) => {\n    callback(colorScheme)\n  })\n}\n\nlet isSupportedAutoTheme: boolean | null = null\nexport const getIsSupportedAutoTheme = () => {\n  if (isSupportedAutoTheme == null) {\n    const osVerNum = parseInt(osVer)\n    isSupportedAutoTheme = isAndroid\n      ? osVerNum >= 5\n      : osVerNum >= 13\n  }\n  return isSupportedAutoTheme\n}\n\n\nexport const showImportTip = (type: string) => {\n  let message\n  switch (type) {\n    case 'defautlList':\n    case 'playList':\n    case 'playList_v2':\n      message = global.i18n.t('list_import_tip__playlist')\n      break\n    case 'setting':\n    case 'setting_v2':\n      message = global.i18n.t('list_import_tip__setting')\n      break\n    case 'allData':\n    case 'allData_v2':\n      message = global.i18n.t('list_import_tip__alldata')\n      break\n    case 'playListPart':\n    case 'playListPart_v2':\n      message = global.i18n.t('list_import_tip__playlist_part')\n      break\n\n    default:\n      message = global.i18n.t('list_import_tip__unknown')\n      break\n  }\n  void tipDialog({\n    title: global.i18n.t('list_import_tip__failed'),\n    message,\n    btnText: global.i18n.t('ok'),\n  })\n}\n\n\n/**\n * 生成节流函数\n * @param fn 回调\n * @param delay 延迟\n * @returns\n */\nexport function throttleBackgroundTimer<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {\n  let timer: number | null = null\n  let _args: Args\n  return (...args: Args) => {\n    _args = args\n    if (timer) return\n    timer = BackgroundTimer.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 debounceBackgroundTimer<Args extends any[]>(fn: (...args: Args) => void | Promise<void>, delay = 100) {\n  let timer: number | null = null\n  let _args: Args\n  return (...args: Args) => {\n    _args = args\n    if (timer) BackgroundTimer.clearTimeout(timer)\n    timer = BackgroundTimer.setTimeout(() => {\n      timer = null\n      void fn(..._args)\n    }, delay)\n  }\n}\n\n// eslint-disable-next-line @typescript-eslint/ban-types\ntype Styles = StyleSheet.NamedStyles<Record<string, {}>>\ntype Style = Styles[keyof Styles]\nconst trasformeProps: Array<keyof Style> = [\n  // @ts-expect-error\n  'fontSize',\n  // @ts-expect-error\n  'lineHeight',\n  // 'margin',\n  // 'marginLeft',\n  // 'marginRight',\n  // 'marginTop',\n  // 'marginBottom',\n  // 'padding',\n  // 'paddingLeft',\n  // 'paddingRight',\n  // 'paddingTop',\n  // 'paddingBottom',\n  'left',\n  'right',\n  'top',\n  'bottom',\n]\nexport const trasformeStyle = <T extends Style>(styles: T): T => {\n  const newStyle: T = { ...styles }\n\n  for (const [p, v] of Object.entries(newStyle) as Array<[keyof Style, Style[keyof Style]]>) {\n    if (typeof v != 'number') continue\n    switch (p) {\n      case 'height':\n      case 'minHeight':\n      case 'marginTop':\n      case 'marginBottom':\n      case 'paddingTop':\n      case 'paddingBottom':\n      case 'paddingVertical':\n        newStyle[p] = scaleSizeH(v)\n        break\n      case 'width':\n      case 'minWidth':\n      case 'marginLeft':\n      case 'marginRight':\n      case 'paddingLeft':\n      case 'paddingRight':\n      case 'paddingHorizontal':\n      case 'gap':\n        newStyle[p] = scaleSizeW(v)\n        break\n      case 'padding':\n        newStyle.paddingRight = newStyle.paddingLeft = scaleSizeW(v)\n        newStyle.paddingBottom = newStyle.paddingTop = scaleSizeH(v)\n        break\n      case 'margin':\n        newStyle.marginRight = newStyle.marginLeft = scaleSizeW(v)\n        newStyle.marginBottom = newStyle.marginTop = scaleSizeH(v)\n        break\n      default:\n        // @ts-expect-error\n        if (trasformeProps.includes(p)) newStyle[p] = setSpText(v)\n        break\n    }\n  }\n  return newStyle\n}\n\nexport const createStyle = <T extends StyleSheet.NamedStyles<T>>(styles: T | StyleSheet.NamedStyles<T>): T => {\n  const newStyle: Record<string, Style> = { ...styles }\n  for (const [n, s] of Object.entries(newStyle)) {\n    newStyle[n] = trasformeStyle(s)\n  }\n  // @ts-expect-error\n  return StyleSheet.create(newStyle as StyleSheet.NamedStyles<T>)\n}\n\nexport const isHorizontalMode = (width: number, height: number): boolean => {\n  return width / height > 1.2\n}\n\n\nexport interface RowInfo {\n  rowNum: number | undefined\n  rowWidth: `${number}%`\n}\n\nexport type RowInfoType = 'full' | 'medium'\n\nexport const getRowInfo = (type: RowInfoType = 'full'): RowInfo => {\n  const win = windowSizeTools.getSize()\n  let isMultiRow = isHorizontalMode(win.width, win.height)\n  if (type == 'medium' && win.width / win.height < 1.8) isMultiRow = false\n  // console.log('getRowInfo')\n  return {\n    rowNum: isMultiRow ? 2 : undefined,\n    rowWidth: isMultiRow ? '50%' : '100%',\n  }\n}\n\nexport const toMD5 = stringMd5\n\n\nexport const cheatTip = async() => {\n  const isRead = await getData<boolean>(storageDataPrefix.cheatTip)\n  if (isRead) return\n\n  return tipDialog({\n    title: '谨防被骗提示',\n    message: `1. 本项目无微信公众号之类的所谓「官方账号」，也未在小米、华为、vivo 等应用商店发布应用，商店内的「LX Music」「洛雪音乐」相关的应用全部属于假冒应用，谨防被骗！\\n\n2. 本软件完全无广告且无引流（如需要加群、关注公众号之类才能使用或者升级）的行为，若你使用过程中遇到广告或者引流的信息，则表明你当前运行的软件是第三方修改版。\\n\n3. 目前本项目的原始发布地址只有 GitHub，其他渠道均为第三方转载发布，可信度请自行鉴别。`,\n    btnText: '我知道了 (Close)',\n    bgClose: true,\n  }).then(() => {\n    void saveData(storageDataPrefix.cheatTip, true)\n  })\n}\n\nexport const remoteLyricTip = async() => {\n  const isRead = await getData<boolean>(storageDataPrefix.remoteLyricTip)\n  if (isRead) return\n\n  return tipDialog({\n    title: '有点温馨的提示',\n    message: '若你将本功能用于汽车，请记住这个：\\n道路千万条，安全第一条！\\n道路千万条，安全第一条！！\\n道路千万条，安全第一条！！！',\n    btnText: '我知道了 (Close)',\n    bgClose: true,\n  }).then(() => {\n    void saveData(storageDataPrefix.remoteLyricTip, true)\n  })\n}\n"
  },
  {
    "path": "src/utils/version.js",
    "content": "import { httpGet } from '@/utils/request'\nimport { author, name } from '../../package.json'\nimport { downloadFile, stopDownload, temporaryDirectoryPath } from '@/utils/fs'\nimport { getSupportedAbis, installApk } from '@/utils/nativeModules/utils'\nimport { APP_PROVIDER_NAME } from '@/config/constant'\n\nconst abis = [\n  'arm64-v8a',\n  'armeabi-v7a',\n  'x86_64',\n  'x86',\n  'universal',\n]\n\nconst address = [\n  [`https://raw.githubusercontent.com/${author.name}/${name}/master/publish/version.json`, 'direct'],\n  ['https://registry.npmjs.org/lx-music-mobile-version-info/latest', 'npm'],\n  [`https://cdn.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],\n  [`https://fastly.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],\n  [`https://gcore.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],\n  ['https://registry.npmmirror.com/lx-music-mobile-version-info/latest', 'npm'],\n  ['https://gitee.com/lyswhut/lx-music-mobile-versions/raw/master/version.json', 'direct'],\n  ['http://cdn.stsky.cn/lx-music/mobile/version.json', 'direct'],\n]\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\nconst getTargetAbi = async() => {\n  const supportedAbis = await getSupportedAbis()\n  for (const abi of abis) {\n    if (supportedAbis.includes(abi)) return abi\n  }\n  return abis[abis.length - 1]\n}\nlet downloadJobId = null\nconst noop = (total, download) => {}\nlet apkSavePath\n\nexport const downloadNewVersion = async(version, onDownload = noop) => {\n  const abi = await getTargetAbi()\n  const url = `https://github.com/${author.name}/${name}/releases/download/v${version}/${name}-v${version}-${abi}.apk`\n  let savePath = temporaryDirectoryPath + '/lx-music-mobile.apk'\n\n  if (downloadJobId) stopDownload(downloadJobId)\n\n  const { jobId, promise } = downloadFile(url, savePath, {\n    progressInterval: 500,\n    connectionTimeout: 20000,\n    readTimeout: 30000,\n    begin({ statusCode, contentLength }) {\n      onDownload(contentLength, 0)\n      // switch (statusCode) {\n      //   case 200:\n      //   case 206:\n      //     break\n      //   default:\n      //     onDownload(null, contentLength, 0)\n      //     break\n      // }\n    },\n    progress({ contentLength, bytesWritten }) {\n      onDownload(contentLength, bytesWritten)\n    },\n  })\n  downloadJobId = jobId\n  return promise.then(() => {\n    apkSavePath = savePath\n    return updateApp()\n  })\n}\n\nexport const updateApp = async() => {\n  if (!apkSavePath) throw new Error('apk Save Path is null')\n  await installApk(apkSavePath, APP_PROVIDER_NAME)\n}\n"
  },
  {
    "path": "src/utils/windowSizeTools.ts",
    "content": "import { Dimensions, StatusBar } from 'react-native'\nimport { getWindowSize as getWindowSizeRaw } from './nativeModules/utils'\n// import { log } from './log'\n\nexport type SizeHandler = (size: { width: number, height: number }) => void\nexport const getWindowSize = async() => {\n  return getWindowSizeRaw().then((size) => {\n    const scale = Dimensions.get('window').scale\n    size.width = size.width / scale\n    size.height = size.height / scale\n    return size\n  })\n}\n\nexport const windowSizeTools = {\n  size: {\n    width: 0,\n    height: 0,\n  },\n  listeners: [] as SizeHandler[],\n  getSize() {\n    return this.size\n  },\n  onSizeChanged(handler: SizeHandler) {\n    this.listeners.push(handler)\n\n    return () => {\n      this.listeners.splice(this.listeners.indexOf(handler), 1)\n    }\n  },\n  async init() {\n    // Dimensions.addEventListener('change', () => {\n    //   void getWindowSize().then((size) => {\n    //     if (!size.width) return\n    //     const scale = Dimensions.get('screen').scale\n    //     size.width = Math.round(size.width / scale)\n    //     size.height = Math.round(size.height / scale) + (StatusBar.currentHeight ?? 0)\n    //     this.size = size\n    //     for (const handler of this.listeners) handler(size)\n    //   })\n    // })\n    const size = await getWindowSize()\n    // log.info('win size', size)\n    if (size.width) {\n      this.size = size\n    } else {\n      const window = Dimensions.get('window')\n      // log.info('Dimensions window size', window)\n      this.size = {\n        width: Math.round(window.width),\n        height: Math.round(window.height) + (StatusBar.currentHeight ?? 0),\n      }\n    }\n    // console.log('init windowSizeTools')\n    return size\n  },\n  setWindowSize(width: number, height: number) {\n    this.size = {\n      width: Math.round(width),\n      height: Math.round(height),\n    }\n    for (const handler of this.listeners) handler(this.size)\n  },\n}\n"
  },
  {
    "path": "test.js",
    "content": "const { publicEncrypt, privateDecrypt, generateKeyPair, createCipheriv, createDecipheriv, constants } = require('crypto')\n\n\nconst generateRsaKey = () => new Promise((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\n// generateRsaKey().then(({ publicKey, privateKey }) => {\n//   console.log(publicKey)\n//   console.log(privateKey)\n// })\n\nconst rsaEncrypt = (buffer, key) => {\n  return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')\n}\nconst rsaDecrypt = (buffer, key) => {\n  return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)\n}\n\nconst publicKey = `\n-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0rggENU2JbXgYGoQBIyqlBJP76mgMKh8\n5gRIUsAwoq/Oj7qEoKYX9jqtnRgAwEPIV7aLMQxGryfm9fGlohDcUPtcF6za5l6L9Szd+0McOCxZ\nSY98/pPFdTYnBHRHPrHHYqzqs4y5wPqpFFNrt2z312YS4xy3SYHkooNPxL0OscxejeG9KtmXQmMd\nejm2MxOIuItlqGHpdwInlvY8Wm/gOMvBmPVffsMaNB412xSZA25D3gRNZRO6O28+S2pXRdSbmFX6\nDLWQ/xRDJW1QnfbtjbAJ7Xo1X1anS/NEKRpZqHidjjWI43rL/LhcIAt45a1MkxpBEO+1yCivaNCF\nE5jyQwIDAQAB\n-----END PUBLIC KEY-----\n`\nconst privateKey = `\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSuCAQ1TYlteBgahAEjKqUEk/v\nqaAwqHzmBEhSwDCir86PuoSgphf2Oq2dGADAQ8hXtosxDEavJ+b18aWiENxQ+1wXrNrmXov1LN37\nQxw4LFlJj3z+k8V1NicEdEc+scdirOqzjLnA+qkUU2u3bPfXZhLjHLdJgeSig0/EvQ6xzF6N4b0q\n2ZdCYx16ObYzE4i4i2WoYel3AieW9jxab+A4y8GY9V9+wxo0HjXbFJkDbkPeBE1lE7o7bz5LaldF\n1JuYVfoMtZD/FEMlbVCd9u2NsAntejVfVqdL80QpGlmoeJ2ONYjjesv8uFwgC3jlrUyTGkEQ77XI\nKK9o0IUTmPJDAgMBAAECggEAUCLE0xH6pym9XH1JfSlvv6MdMkQ8jvSslx8+z/WWKXCZqjBhOuUf\njL0xBGK5+mRsvurFkZAdG4SdwZ+2AWXPG58UHU7X6q8/I6z9+I3DoBtBk4eVng1dlR9UhE2iQJYE\ngSLImSLmC51oCrpU5ytyL8D6YtOrYrGa3TD595R8j6G8ElDuvgeqIeFdb5CQUm/4/v+kZ8i8QzGY\nlZ65N/mIyl6xB+DnZmWtPRk9HXDdtAnNQlj7uUrKdChq4JNIbwOmrjdmfVkpk5f5JBrCLt8YjGDM\nTf0bSfQcX/PGtm5RHb95XsAp4nGMhVnV44Dnw7kHLIElHRYtt3oXu/SHo3/BDQKBgQDrEpmFaYNS\niVJczAcqmOR+rLHCZnx3GUrKq2udIE6Zb/s2FTzmYHqcSQ66xsn4XzueaCrj9tqUuYIjYnnVM4lL\nux7CgGAwB6moBlUVcDbPZEMJEXXvDE6+oWg3eLGp1S5U7Q8ab0/9laC+c1ncMrgYhBNJ9RhvzimC\n/+LPNbdmDQKBgQDleoBdsCDPIpbJVGjkgwN1T6aDPfs+A2lKx0z8Grzmv17HlhYqjy5Jvdx8bulR\n0xY81YJ+h8dFD9e9fHdlxpfGj+gtHB8oGzjdN6JWfob6im+XAmg2ftWd1pHx9KBXkpL34NpqleJF\nEH7LjGcMjbnceVAM6FEAiPI8BE6RrM91jwKBgQCMzIkzxa0oxKOWfYZVV1qVHS8jt2sZkwafOemt\nJWqusMoQ7MubWXJXJdMywFq8752wFciK3pKxviNaumMq9kFoIN4dtfLnEc/mmlRgEORjeDRGvDSd\nSAvqVpcrkpknlk64A32mYcHRq8uqB0FtiNuHo6RCChHm9d8bXdUmM5B0CQKBgQCM6xYq8j6jlHUO\nO2SSdxXHk1sImyZO5Z9iCVNwOScpd/lHDRadmgFtzUa5rw5ebgbo4qBY/R5Ufa8ZMHbNrA+GItcL\n5IoJgfYAeuqYvOg8sIhoLlU6qdaaL6q972ALhvnzeEQIUfR6Pu/uJVEet2WcS27qDju33WELlAV/\nlaRsZwKBgBzjfllho66+JLamWnthlDHqgEKZDiZF+1fZipOITd7jaA3W8teGc8v9YmSABG4b9IGd\nM8XtAFeDYN/MHMKRq+nmLF6hvEQYHUgqYGZGkxX2HcKnRHUssbUOTWJynvRVNPL/k9g9q0d6PeSS\nDaE3OO3AUN8voDBaHDII1YscbEBg\n-----END PRIVATE KEY-----\n`\n\n// const encrypted = rsaEncrypt('123', publicKey)\n// console.log(encrypted)\n// // const encrypted = 'lJVUCFSV2K90ZdCeosUbtek/wZPmqmKR7ShsP2vfheldde6o9e2Qrmj1QojEwsZtjvq61FCmwpX46LkbsLY/jpM17PUZeqQHhqCy4Rz/hIyMCyIQTPwH5907pIwcpQH2XpJ45/hrjkhLhGU9pkZXtr3qkJiRTi0nllu7z6p6Qf0Hx/zYGxe41VVVnq/9t5xkoUyAfknEn1LMAJyJVft4pD43vTn4tz34+cf7GuzlC4xPiUyKC/trDGBW0kEQBPIaRpd7q1ab9x5fg8mffhBSDR3o+PvVuq3UP02MwpoMDs2bnnwzYawuGv87VNsvEHcvTkZDnh8ME9vtbQboLWVD5w=='\n\n\n// console.log(rsaDecrypt(Buffer.from(encrypted, 'base64'), privateKey).toString())\n\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\n\n// const aesKey = Buffer.from('123456789abcdefg')\n// const iv = Buffer.from('012345678901234a')\n\n// // const encryptedAes = aesEncrypt(Buffer.from('hello'), 'aes-128-cbc', aesKey, iv)\n// // console.log(encryptedAes)\n// // // const encryptedAes = '4zbNfntuHPrHtPvhEVC10Q=='\n\n// // console.log(aesDecrypt(Buffer.from(encryptedAes, 'base64'), 'aes-128-cbc', aesKey, iv).toString())\n\n// // const encryptedAes = aesEncrypt(Buffer.from('hello'), 'aes-128-ecb', aesKey, '')\n// // console.log(encryptedAes)\n// const encryptedAes = 'oEShKajDOUILq3cVoRv0iw=='\n\n// console.log(aesDecrypt(Buffer.from(encryptedAes, 'base64'), 'aes-128-ecb', aesKey, '').toString())\n\nconst text = '{\"id\":\"3779629\",\"n\":100000,\"p\":1}'\nconst iv = Buffer.from('0102030405060708')\nconst presetKey = Buffer.from('0CoJUm6Qyw8W8jud')\nconst secretKey = [56, 70, 110, 77, 99, 98, 51, 117, 67, 98, 85, 73, 118, 80, 104, 70]\n\n// Rn061YcbiMv3hQJlOLNklgQqbcUEF2YyiShXN8kevX3z+iU8j1qHhNEVEoNTNTPQ\n// 4dbKbQjGbYdp/Q0bEwCTHcoB8vEQZNc5OUxz6VxScq4AuCYNHwWY44GJrfYuMV7GqQlC/88WdKg4w9ILJGAx5w==\n\nconst r1 = aesEncrypt(text, 'aes-128-cbc', presetKey, iv)\nconsole.log(r1.toString('base64'))\nconsole.log(aesEncrypt(r1, 'aes-128-cbc', Buffer.from(secretKey), iv).toString('base64'))\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@react-native/typescript-config/tsconfig.json\",     /* Recommended React Native TSConfig base */\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    \"module\": \"ESNext\",\n    \"types\": [\"react-native\", \"node\"],\n    /* Completeness */\n    \"skipLibCheck\": true,                                 /* Skip type checking all .d.ts files. */\n\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      // \"@config\": [\"src/config\"],\n      // \"@store\": [\"src/store\"],\n      // \"@components\": [\"src/components\"],\n      // \"@navigation\": [\"src/navigation\"],\n      // \"@screens\": [\"src/screens\"],\n      // \"@theme\": [\"src/theme\"],\n      // \"@utils\": [\"src/utils\"],\n    }\n  },\n  \"exclude\": [\"node_modules\", \"build\", \"dist\"]\n}\n"
  }
]